diff --git a/e2e/tests/__m.spec.ts b/e2e/tests/__m.spec.ts deleted file mode 100644 index 83240c0..0000000 --- a/e2e/tests/__m.spec.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { test } from '@playwright/test'; -test('mobile hero geometry', async ({ page }) => { - await page.setViewportSize({ width: 390, height: 844 }); - await page.goto('/'); - const out = await page.evaluate(() => { - const pick = (sel: string) => { - const el = document.querySelector(sel) as HTMLElement | null; - if (!el) return null; - const r = el.getBoundingClientRect(); - const cs = getComputedStyle(el); - return { left: Math.round(r.left), right: Math.round(r.right), - width: Math.round(r.width), height: Math.round(r.height), - pad: cs.padding, cls: el.className.toString().slice(0, 40) }; - }; - const input = document.querySelector('input[type="search"]') as HTMLElement; - const box = input?.closest('div'); - const form = input?.closest('form'); - return { - h1: pick('h1'), - lede: pick('h1 + p') ?? pick('p'), - omniBox: box ? { left: Math.round(box.getBoundingClientRect().left), - width: Math.round(box.getBoundingClientRect().width), - pad: getComputedStyle(box).padding, - dir: getComputedStyle(box).flexDirection, - cls: box.className.toString().slice(0,40) } : null, - input: pick('input[type="search"]'), - form: form ? { left: Math.round(form.getBoundingClientRect().left), - width: Math.round(form.getBoundingClientRect().width), - pad: getComputedStyle(form).padding, - cls: form.className.toString().slice(0,40) } : null, - filterBar: pick('form'). // placeholder - constructor === Object ? null : null, - }; - }); - console.log(JSON.stringify(out, null, 2)); -}); diff --git a/nextjs-app/__tests__/components/filterBarScoping.test.ts b/nextjs-app/__tests__/components/filterBarScoping.test.ts index 38ddf3b..5fcaebf 100644 --- a/nextjs-app/__tests__/components/filterBarScoping.test.ts +++ b/nextjs-app/__tests__/components/filterBarScoping.test.ts @@ -29,6 +29,31 @@ const RESET_BY_HERO_MODE = [ 'background', 'border', 'border-radius', 'box-shadow', 'padding', ]; +/** + * Comments are stripped before anything is parsed. + * + * A `{` or `}` inside a comment would otherwise desynchronise the brace walk + * below and the rule regex alike, and the selector text captured for each rule + * would carry the preceding comment along with it. + */ +function withoutComments(css: string): string { + return css.replace(/\/\*[\s\S]*?\*\//g, ''); +} + +/** + * The individual selectors in a rule's prelude. + * + * Split on commas, because a selector list is a list: `.filterBar, .other { }` + * applies to `.filterBar` just as surely as `.filterBar { }` does, and an + * earlier version of this guard compared the whole prelude against the literal + * string '.filterBar' — so writing the regression as a comma list, or across + * two lines, would have walked straight past it. + */ +function selectorsOf(prelude: string): string[] { + return prelude.split(',').map((sel) => sel.trim().replace(/\s+/g, ' ')) + .filter(Boolean); +} + function mediaQueryBodies(css: string): string[] { const bodies: string[] = []; const re = /@media[^{]*\{/g; @@ -49,7 +74,7 @@ function mediaQueryBodies(css: string): string[] { } describe('FilterBar hero-mode scoping', () => { - const css = fs.readFileSync(CSS, 'utf8'); + const css = withoutComments(fs.readFileSync(CSS, 'utf8')); it('confirms heroMode still resets the card, which is what makes this matter', () => { const hero = css.match(/\.heroMode\s*\{([^}]*)\}/); @@ -62,14 +87,15 @@ describe('FilterBar hero-mode scoping', () => { for (const body of mediaQueryBodies(css)) { for (const rule of body.matchAll(/([^{}]+)\{([^{}]*)\}/g)) { - const selector = rule[1].trim().split('\n').pop()!.trim(); - // Only a *bare* .filterBar is dangerous. Scoped variants + // Only a *bare* .filterBar is dangerous, and it is dangerous wherever + // it appears in a selector list. Scoped variants // (`.filterBar:not(.heroMode)`) and descendants are fine. - if (selector !== '.filterBar') continue; + const selectors = selectorsOf(rule[1]); + if (!selectors.includes('.filterBar')) continue; for (const prop of RESET_BY_HERO_MODE) { if (new RegExp(`(^|[;\\s])${prop}\\s*:`).test(rule[2])) { - offenders.push(`${selector} sets ${prop}`); + offenders.push(`${rule[1].trim()} sets ${prop}`); } } }