fix(test): drop a committed scratch probe, and close a hole in the guard
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m3s
PR Checks / Backend Smoke (pull_request) Successful in 9s
PR Checks / Build Backend (no push) (pull_request) Successful in 11s
PR Checks / Build Frontend (no push) (pull_request) Successful in 44s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 10s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 9s

Code review, all three findings valid.

e2e/tests/__m.spec.ts was a throwaway probe used to measure the mobile
hero geometry. It asserts nothing, so it could never fail; it carried a
leftover `pick('form').constructor === Object ? null : null` that is
null either way and throws if no form matches; and it should never have
been committed. Deleted.

It survived because `rm -f e2e/tests/__m.spec.ts` ran with the shell
already inside e2e/, so the path resolved to e2e/e2e/tests/... — which
does not exist, and rm -f is silent about that. `git add -A` then swept
it in. I checked `git diff --stat` before committing, which lists only
tracked modifications and never shows an untracked file; `git status
--short` would have.

The scoping guard compared the last line of a rule's prelude against the
literal '.filterBar', so a regression written as a selector list —
`.filterBar, .other { padding }`, or the same split across two lines —
would have walked straight past the test meant to catch it. Selectors
are now split on commas and matched individually, and comments are
stripped first so a brace inside one cannot desynchronise the parse.

Verified against all three shapes: bare, inline comma list, and
multi-line comma list. Each is caught; each passes again once reverted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
This commit is contained in:
TudorandClaude Opus 5 committed 2026-08-26 21:49:48 +01:00
1 parent 0b15497c09
commit 0804566736
2 files changed
+31 -41

No files matched your search

-36
View File
@@ -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));
});
@@ -29,6 +29,31 @@ const RESET_BY_HERO_MODE = [
'background', 'border', 'border-radius', 'box-shadow', 'padding', '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[] { function mediaQueryBodies(css: string): string[] {
const bodies: string[] = []; const bodies: string[] = [];
const re = /@media[^{]*\{/g; const re = /@media[^{]*\{/g;
@@ -49,7 +74,7 @@ function mediaQueryBodies(css: string): string[] {
} }
describe('FilterBar hero-mode scoping', () => { 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', () => { it('confirms heroMode still resets the card, which is what makes this matter', () => {
const hero = css.match(/\.heroMode\s*\{([^}]*)\}/); const hero = css.match(/\.heroMode\s*\{([^}]*)\}/);
@@ -62,14 +87,15 @@ describe('FilterBar hero-mode scoping', () => {
for (const body of mediaQueryBodies(css)) { for (const body of mediaQueryBodies(css)) {
for (const rule of body.matchAll(/([^{}]+)\{([^{}]*)\}/g)) { for (const rule of body.matchAll(/([^{}]+)\{([^{}]*)\}/g)) {
const selector = rule[1].trim().split('\n').pop()!.trim(); // Only a *bare* .filterBar is dangerous, and it is dangerous wherever
// Only a *bare* .filterBar is dangerous. Scoped variants // it appears in a selector list. Scoped variants
// (`.filterBar:not(.heroMode)`) and descendants are fine. // (`.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) { for (const prop of RESET_BY_HERO_MODE) {
if (new RegExp(`(^|[;\\s])${prop}\\s*:`).test(rule[2])) { if (new RegExp(`(^|[;\\s])${prop}\\s*:`).test(rule[2])) {
offenders.push(`${selector} sets ${prop}`); offenders.push(`${rule[1].trim()} sets ${prop}`);
} }
} }
} }