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
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:
1 parent
0b15497c09
commit
0804566736
2 files changed
+31
-41
No files matched your search
@@ -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',
|
||||
];
|
||||
|
||||
/**
|
||||
* 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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user