package tokenengine import ( "strings" "testing" "github.com/urpi/go-bananas/internal/domain" ) // The aggregate contract is consumed two ways: as a stylesheet the application // links, which can only be styled from :root, and as a stylesheet a control adopts // into its own shadow root, where :root matches nothing because a stylesheet in the // document does not select into a shadow tree. The bare selector forms are what make // the second use possible, and a palette preview depends on them, so their absence is // a silent failure: the selector still lists every palette and every preview renders // in whatever the host's fallbacks happen to be. // assertSelectorPair asserts that a selector list contains both halves of a pair as // distinct comma-separated entries. // // The assertion is on the joined pair rather than on each half alone, because one half // cannot tell the two forms apart: the string `[data-theme="acme"][data-mode="light"]` // occurs inside `:root[data-theme="acme"][data-mode="light"]`, so a bare Contains // reports a bare selector as present when only the rooted form was emitted. Joining // them is what discriminates, because in the rooted-only list the two halves are // separated by `:root`. func assertSelectorPair(t *testing.T, css, want string) { t.Helper() if !strings.Contains(css, want) { t.Errorf("contract does not emit %q as a pair of entries, so a palette preview cannot adopt it", want) } } func TestContractEmitsBareSelectorsForPreviewScope(t *testing.T) { theme := testTheme() theme.Slug = "acme" theme.DefaultAppearance = domain.AppearanceDark css := ContractAll([]*domain.Theme{theme}) // The palette on its own, so a preview that declares no appearance still adopts // the designer's choice. assertSelectorPair(t, css, `[data-theme="acme"], [data-bananas-theme="acme"]`) // Each appearance, in both attribute conventions. assertSelectorPair(t, css, `[data-theme="acme"][data-mode="light"], [data-bananas-theme="acme"][data-bananas-appearance="light"]`) assertSelectorPair(t, css, `[data-theme="acme"][data-mode="dark"], [data-bananas-theme="acme"][data-bananas-appearance="dark"]`) // The rooted forms have to survive: every application that links the contract // rather than adopting it styles itself from :root. assertSelectorPair(t, css, `:root[data-theme="acme"], :root[data-bananas-theme="acme"]`) assertSelectorPair(t, css, `:root[data-theme="acme"][data-mode="light"], :root[data-bananas-theme="acme"][data-bananas-appearance="light"]`) } // TestContractBareSelectorsCarryTheSameValues guards against the two forms drifting // apart, which would mean a preview shows a different palette from the one applying // to the page. Both forms are emitted from the same declarations, so this is a // statement that the pairing is not an accident worth breaking. func TestContractBareSelectorsCarryTheSameValues(t *testing.T) { css := ContractAll([]*domain.Theme{testTheme()}) for _, want := range []string{ `--bn-bg: #ffffff;`, `--bn-text: #0f172a;`, `--bn-accent: #4f46e5;`, } { if !strings.Contains(css, want) { t.Errorf("contract no longer emits %q", want) } } } // TestContractLegacyAliasesStillExist protects the compatibility promise: an // application that already uses the unprefixed names must keep working. func TestContractLegacyAliasesStillExist(t *testing.T) { css := ContractAll([]*domain.Theme{testTheme()}) for legacy := range legacyAliases { if !strings.Contains(css, "--"+legacy+":") { t.Errorf("legacy alias --%s is no longer emitted", legacy) } } }