Skip to content

debt(lint): lint .playwright and .storybook, and fix what surfaced - #992

Merged
stevensacks merged 6 commits into
mainfrom
debt/980-lint-playwright-storybook-dirs
Jul 22, 2026
Merged

debt(lint): lint .playwright and .storybook, and fix what surfaced#992
stevensacks merged 6 commits into
mainfrom
debt/980-lint-playwright-storybook-dirs

Conversation

@stevensacks

@stevensacks stevensacks commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Closes #980

Problem

.playwright and .storybook were bare entries in the lint preset's default ignore list. A config object carrying only ignores is a global ignore in ESLint flat config, and a global ignore beats any later block's files, so the two blocks written to lint those directories were shadowed by the defaults they ship alongside:

  • the playwright block scopes itself to .playwright/**/*.ts?(x) and never matched a file, taking its hand-tuned expect-expect options with it
  • the storybook block kept its *.stories.* rules (those files live elsewhere) but lost its .storybook/main.* half

Every e2e spec, Playwright helper, and Storybook config file went unchecked while pnpm lint reported a clean tree and CI stayed green.

Fix

Root cause is fixed upstream in @gaia-react/lint 1.11.0, which drops both entries and adds an ignores suite that asks ESLint itself which paths survive the defaults, so a future directory-level entry that shadows a scoped block fails a test instead of shipping. This bumps to it and fixes the 78 findings that surfaced here: 59 by eslint --fix, 19 by hand.

Two of those findings were coverage, not style:

  • playwright/expect-expect counts the project's expect*() a11y helpers as assertions, retiring two hand-written disable comments that never suppressed anything
  • playwright/no-conditional-in-test caught a hand-rolled if (relevant.length > 0) { attach; throw } in the landmark spec, now an assertion over a projection of the violating rules

Upstream also gained playwright/no-skipped-test's allowConditional, so the template's own runtime-conditional skip (a language switcher that single-language projects do not render) stays legal while a bare test.skip() stays flagged.

The react-perf harness files take a file-level no-underscore-dangle disable: window.__renders / __bippyMeta / __PERF_NO_STRICT are the wire contract between the injected harness and the capture helper, not a naming choice, and app/entry.client.tsx already carries the same disable for the same reason.

A second commit closes the same gap one layer down: .lintstagedrc.json gave those directories a prettier-only entry (because running eslint on an ignored file only warns), so a lint error in an e2e spec would have passed the pre-commit gate and surfaced in CI.

Verification

Step Result
Simplify 4 angles reviewed; reuse / simplification / efficiency clean, altitude raised 3 doc findings, all applied or trimmed
Localization n/a, no user-facing strings touched
Typecheck pass
Lint pass, zero warnings, real config against published 1.11.0
Unit tests 155 passed, 1 skipped, zero console warnings
E2E 8 passed, 1 skipped (the conditional language-switch skip)
Dev server / and /privacy both 200
Build pass

The e2e run is the load-bearing one: capture.ts and harness-entry.ts carry real edits (a path default import, outputFiles.at(0), the seq counter extraction, handler methods to arrow properties), and react-perf-smoke.spec.ts exercises all of it.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

code-review-audit skipped: no files in the frontend auditor's scope, but this diff dispatches code-audit-github-workflows, which CI cannot run. GAIA-Audit is pending, not green: run the dispatched member(s) locally to clear the merge gate

stevensacks and others added 3 commits July 22, 2026 01:51
Both directories were bare entries in the lint preset's default ignore list. A
config object carrying only `ignores` is a global ignore in flat config, and a
global ignore beats any later block's `files`, so the two blocks written to lint
those directories were shadowed by the defaults they ship alongside: the
`playwright` block never matched a file, and the `storybook` block lost its
`.storybook/main.*` half. Every e2e spec, Playwright helper, and Storybook config
file went unchecked while `pnpm lint` reported a clean tree.

The root cause is fixed upstream in @gaia-react/lint 1.11.0, which drops both
entries and adds a suite that asks ESLint which paths survive the defaults. This
bumps to it and fixes the 78 findings it surfaces here, 59 by `--fix` and the
rest by hand.

Two of those were coverage, not style. `playwright/expect-expect` counts the
project's `expect*()` a11y helpers as assertions, which retires two hand-written
disable comments that never suppressed anything, and
`playwright/no-conditional-in-test` caught a hand-rolled `if (...) { throw }` in
the landmark spec, now an assertion over a projection of the violating rules.
`playwright/no-skipped-test` gains `allowConditional` upstream so the template's
own runtime-conditional skip stays legal.

The react-perf harness files take a file-level `no-underscore-dangle` disable:
`window.__renders` / `__bippyMeta` / `__PERF_NO_STRICT` are the wire contract
between the injected harness and the capture helper, not a naming choice, and
`app/entry.client.tsx` already carries the same disable for the same reason.

Closes #980

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01728LepSoRCrVZ7L2HkEoHa
`.lintstagedrc.json` gave those two directories a prettier-only entry while
`app/**` got eslint alongside it. That split existed because eslint ignored
them: running it on an ignored file only warns. With the ignore gone, the
prettier-only entry becomes the same silent non-coverage one layer down, where
a lint error in an e2e spec passes the pre-commit gate and surfaces in CI.

Give the entry the same two commands `app/**` already runs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01728LepSoRCrVZ7L2HkEoHa
…changes

Two follow-ons the audit raised on the previous commit.

The `{.storybook,.playwright}` lint-staged entry pointed eslint at the same
cache file as `app/**`. lint-staged runs glob task-chains concurrently, so a
commit staging both trees launches two eslint processes that each load the cache
at start and rewrite the whole file at exit, discarding one another's entries.
The failure mode is cache thrash rather than a false clean, since a missing
entry only means re-lint. Give the tooling entry its own cache file.

`tests.yml`'s path allowlist covered `.playwright/` but not `.storybook/`, so a
`.storybook`-only change reported the required check green without running.
That was defensible while the directory was unlinted and holds no tests; it is
not now that typecheck and lint both reach it, and Chromatic's Vite build strips
types rather than checking them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01728LepSoRCrVZ7L2HkEoHa
@stevensacks
stevensacks force-pushed the debt/980-lint-playwright-storybook-dirs branch from 3374056 to 57e32e4 Compare July 21, 2026 16:51
stevensacks and others added 3 commits July 22, 2026 02:23
The allowlist's new comment states the rule that a path typecheck and lint
reach belongs here even when it carries no tests, but the pattern only named
four config files by tool. `react-router.config.ts`, `doctor.config.ts`,
`knip.config.ts`, `prettier.config.mjs`, and `stylelint.config.mjs` are all in
the tsconfig program and all linted, so a PR touching only one of them set
`code=false` and reported the required check green without running. The
react-router case is the sharpest: that file feeds the typegen step `pnpm
typecheck` runs before `tsc`, so it can invalidate generated route types across
`app/**`, and the e2e suite exercises the SSR-rendered pages it configures.

Name every root config in the one existing alternative rather than appending a
second, and say in the comment why the list is not scoped to the tools this job
happens to run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01728LepSoRCrVZ7L2HkEoHa
…ationale

`pnpm-workspace.yaml` and `.npmrc` were missing, and the lockfile alternative
does not cover them transitively: pnpm 11 reads `allowBuilds`,
`publicHoistPattern`, `minimumReleaseAge`, and `trustPolicy` from the workspace
file, while the lockfile's own `settings:` block carries neither. A change to
either can break `pnpm install --frozen-lockfile` for the next unrelated PR
while the workspace-config PR itself merges on a green required check. Not
hypothetical: #976 changed only `pnpm-workspace.yaml` and would have skipped the
suite.

The comment also claimed every root config is in the tsconfig program. That is
true of the `.ts` ones only; `include` lists no `.mjs` glob and `allowJs` is
off, so `eslint.config.mjs`, `prettier.config.mjs`, and `stylelint.config.mjs`
are covered by lint alone. Attribute each half to the tool that actually runs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01728LepSoRCrVZ7L2HkEoHa
GAIA-Audit: 1.6.1 4d242efea3fa90cb4ec9ecf7a76e58949f7150c4e5ef8f9209ac787861d4be8e bc0190e
@stevensacks
stevensacks merged commit a4ee4b8 into main Jul 22, 2026
15 checks passed
@stevensacks
stevensacks deleted the debt/980-lint-playwright-storybook-dirs branch July 22, 2026 00:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ESLint flat config ignores .playwright and .storybook, so their configured rule blocks never run

1 participant