Foundation only. No spec is converted in this commit, so the suite behaves exactly as before — the conversions follow in themed batches, each leaving the suite green. The suite had grown by copy-paste. Registering a customer was implemented nine times, as `register` in five files and `registerCustomer` in four more, each carrying its own re-explanation of the same bcrypt wait and the same "the header, not the URL, proves the session exists" reasoning. `uniqueEmail` was reinvented per file with a different prefix and a different encoding each time. Thirteen locators reached into antd's internals — `.ant-tabs-tab-active .ant-tabs-tab-btn`, `.ant-select-item-option[title=...]`, `.ant-col` — spread across seven files, so an antd upgrade breaks tests that have nothing to do with it. Page objects hold named locators and the actions that operate on them; assertions stay in the specs, so a test reads as its own statement of what it verifies. The exception is an action waiting for its own completion — registering waits for the account button, opening an admin tab waits for its panel — because that wait is the action's contract, and pushing it to callers would recreate the duplication being removed. Fixtures carry the setup rather than the specs. `customer` registers through the API rather than the form: nine specs drove the registration form purely to arrive at a signed-in session, so a broken form failed a hundred tests that were not about it, and each paid for a bcrypt round-trip through the UI. `page.request` shares the browser context's cookie jar, so the session belongs to `page`. The specs that are genuinely about registration drive the form properly through `authModal`. `adminApi` takes its base URL from the Playwright config. One spec built its own request context against a hardcoded http://localhost:5173, so changing the port in the config would have moved every test except that one. The inline pg.Client in password-reset.spec.ts moves to support/db.ts. The reasoning for reading the database directly is unchanged and still right — an endpoint that returns a reset token for an arbitrary address is account takeover if it is ever reachable, and an environment gate is a thin thing to stand between that and production — but it no longer sits in a spec where it can be copied into the next one wanting a shortcut. Its default port becomes 55500, the local stack's, rather than 55432: that is the integration suite's disposable Postgres, a different database with different credentials that the app under test is not connected to, and it is Hyper-V-reserved on at least one machine here, so the spec failed with a bare ECONNREFUSED naming a port nobody had chosen. tsconfig.test.json type-checks the tree and runs as part of `npm run build`. It is separate from tsconfig.json rather than widening its `include`, because scripts/check-sonar-tsconfig.js compares the two configs' include arrays, and pulling the Playwright suite into SonarQube's analysis program is a different decision from type-checking it. The whole existing suite type-checks clean on the first run. Lint now covers tests/ with `project` rather than `projectService` — the service resolves a file to the nearest tsconfig.json, which for tests/ is the one that excludes them, and every file then errors as not part of a project. no-floating-promises is an error here: Playwright's API is almost entirely promises, and a missing await on an assertion does not fail, it passes having asserted nothing. Four rule families are switched off for tests rather than left as warnings. Bringing these files in scope added 45, of which none were defects, and #60's argument is that a gate nobody reads is not a gate. There is no React in this directory, and the hooks rules fire on ordinary functions whose parameter is named `use` — which Playwright fixtures are, by its own API. Test credentials are the point of a test and the project's own rule is that they live only in test paths, which is here. Math.random builds unique fixture names so parallel workers do not collide, and a cryptographic generator would say something untrue about what the value is for. The count is back to the 30 that src carried before. Refs #137
139 lines
5.9 KiB
JavaScript
139 lines
5.9 KiB
JavaScript
import js from '@eslint/js';
|
|
import tseslint from 'typescript-eslint';
|
|
import reactHooks from 'eslint-plugin-react-hooks';
|
|
import jsxA11y from 'eslint-plugin-jsx-a11y';
|
|
import sonarjs from 'eslint-plugin-sonarjs';
|
|
import globals from 'globals';
|
|
|
|
// Named `.mjs` because this package is CommonJS — `eslint.config.js` would be
|
|
// parsed as CJS and the imports above would fail.
|
|
//
|
|
// Policy: every preset is downgraded to advisory, and the rules that actually
|
|
// fail the build are listed once at the bottom. That way the CI gate is
|
|
// readable in one place rather than inferred from four presets' defaults.
|
|
// The reasoning behind the split, and the measurements it rests on, are in
|
|
// docs/superpowers/specs/2026-08-19-eslint-design.md.
|
|
|
|
/**
|
|
* Rewrites a preset's enabled rules to `warn`, preserving each rule's options.
|
|
* Rules the preset explicitly turned off stay off — a preset that disables a
|
|
* rule means it, and flipping those to `warn` turns the whole of SonarJS's
|
|
* opt-in catalogue (file headers, naming conventions) into daily noise.
|
|
*/
|
|
const advisory = (config) => ({
|
|
...config,
|
|
rules: Object.fromEntries(
|
|
Object.entries(config.rules ?? {}).map(([rule, level]) => {
|
|
const severity = Array.isArray(level) ? level[0] : level;
|
|
if (severity === 'off' || severity === 0) return [rule, level];
|
|
return [rule, Array.isArray(level) ? ['warn', ...level.slice(1)] : 'warn'];
|
|
})
|
|
),
|
|
});
|
|
|
|
export default tseslint.config(
|
|
{ ignores: ['dist/**', 'playwright-report/**', 'test-results/**', 'eslint.config.mjs'] },
|
|
|
|
...[
|
|
js.configs.recommended,
|
|
...tseslint.configs.recommended,
|
|
sonarjs.configs.recommended,
|
|
// v7 of this plugin ships the React Compiler rule set alongside the two
|
|
// classic rules. This is React 18 with no compiler in the build, so those
|
|
// extra rules advise against a stricter model than the code was written
|
|
// for — worth seeing (`purity` catches a real `Date.now()` in render), not
|
|
// worth failing a build over.
|
|
reactHooks.configs.flat['recommended-latest'],
|
|
{ rules: jsxA11y.flatConfigs.recommended.rules },
|
|
].map(advisory),
|
|
|
|
{ plugins: { 'jsx-a11y': jsxA11y } },
|
|
|
|
{
|
|
files: ['src/**/*.{ts,tsx}'],
|
|
languageOptions: {
|
|
globals: globals.browser,
|
|
parserOptions: {
|
|
projectService: true,
|
|
tsconfigRootDir: import.meta.dirname,
|
|
},
|
|
},
|
|
rules: {
|
|
// Hooks called conditionally break React outright.
|
|
'react-hooks/rules-of-hooks': 'error',
|
|
// The stale-closure rule. PR #11 fixed a cart badge that did not update
|
|
// after account creation, which is precisely what this catches.
|
|
'react-hooks/exhaustive-deps': 'error',
|
|
// An unawaited promise fails silently — the same class of defect as the
|
|
// unwrapped async routes in #59, and what the project's Playwright notes
|
|
// already warn about for missing `await`.
|
|
'@typescript-eslint/no-floating-promises': 'error',
|
|
// `attributes: false` because `onClick={async () => ...}` is idiomatic
|
|
// React and safe when the handler catches its own errors. Left at the
|
|
// default this rule flags every antd button in the admin screens — 25 of
|
|
// its 28 hits here — and a rule that is 89% noise gets switched off.
|
|
'@typescript-eslint/no-misused-promises': [
|
|
'error',
|
|
{ checksVoidReturn: { attributes: false } },
|
|
],
|
|
// A storefront image with no alt text is unusable in a screen reader, and
|
|
// the fix is one attribute.
|
|
'jsx-a11y/alt-text': 'error',
|
|
},
|
|
},
|
|
|
|
{
|
|
// The Playwright suite. Out of scope until #137, because tsconfig.json
|
|
// includes only `src` and type-aware linting had no program for these
|
|
// files. tsconfig.test.json is that program.
|
|
//
|
|
// `project` rather than `projectService`: the service resolves a file to the
|
|
// nearest tsconfig.json, which for tests/ is the one that excludes them, and
|
|
// every file then errors as not part of a project.
|
|
files: ['tests/**/*.ts'],
|
|
languageOptions: {
|
|
globals: globals.node,
|
|
parserOptions: {
|
|
project: ['./tsconfig.test.json'],
|
|
tsconfigRootDir: import.meta.dirname,
|
|
},
|
|
},
|
|
rules: {
|
|
// The one that matters most here. Playwright's API is almost entirely
|
|
// promises, and a missing `await` on an assertion does not fail — it
|
|
// passes, having asserted nothing, which is the worst outcome a test can
|
|
// have. The project's own Playwright notes already warn about it; this
|
|
// enforces it.
|
|
'@typescript-eslint/no-floating-promises': 'error',
|
|
|
|
// Switched off for tests rather than left as warnings. #60's whole
|
|
// argument is that a gate nobody reads is not a gate, and bringing these
|
|
// files in scope added 45 warnings of which none were defects. A rule
|
|
// that cannot be true here is noise that hides the rules that can.
|
|
//
|
|
// There is no React in this directory. The hooks rules fire on ordinary
|
|
// functions whose parameter happens to be named `use` — which Playwright
|
|
// fixtures are, by its own API.
|
|
'react-hooks/rules-of-hooks': 'off',
|
|
'react-hooks/exhaustive-deps': 'off',
|
|
'react-hooks/set-state-in-effect': 'off',
|
|
'react-hooks/purity': 'off',
|
|
|
|
// Test credentials are the point of a test, and the project's own rule is
|
|
// that they must live only in test paths — which is here. Flagging them
|
|
// where they belong trains the reader to ignore the rule where they do
|
|
// not.
|
|
'sonarjs/no-hardcoded-passwords': 'off',
|
|
|
|
// Math.random builds unique fixture names so parallel workers do not
|
|
// collide. Nothing here is a secret, and a cryptographic generator would
|
|
// say something untrue about what the value is for.
|
|
'sonarjs/pseudo-random': 'off',
|
|
|
|
// Page objects hold locators built in the constructor and never
|
|
// reassigned. Flagging them as mutable props does not apply to a class.
|
|
'sonarjs/prefer-read-only-props': 'off',
|
|
},
|
|
}
|
|
);
|