First conversion batch: auth, password-reset, resend-verification. verify-email is left alone — it already used semantic locators and duplicated nothing, and rewriting it to prove a point would be churn. Four of the nine copies of "register a customer" go here. auth.spec.ts and password-reset.spec.ts each carried their own `register`, and resend-verification.spec.ts its own `registerCustomer`, all three re-explaining the same bcrypt wait and the same "the header, not the URL, proves the session exists" reasoning in slightly different words. Two also carried their own `logout`, and two their own `uniqueEmail` with different prefixes. Most of those tests were not about registering. They needed an account to exist so they could test logging out, resetting a password, or resending a verification email, and they paid for a bcrypt round-trip through the form to get one. Those now take the `customer` fixture, which registers through the API. The three tests that genuinely are about the registration form still drive it, because the thing under test has to be the thing exercised. The batch drops from 47s to 26s as a side effect, which is the cost of that round-trip made visible. password-reset.spec.ts loses its inline pg.Client. The reasoning for reading the database directly is unchanged and still right — an endpoint returning a reset token for an arbitrary address is account takeover if it is ever reachable — but it now lives in support/db.ts where it cannot be copied into the next spec wanting a shortcut. It also stops defaulting to port 55432, which is the integration suite's disposable Postgres rather than the database the app under test is connected to, and is Hyper-V-reserved on at least one machine here. Both tests in that file previously failed with a bare ECONNREFUSED unless TEST_PGPORT was set by hand; they now pass with no environment at all. New PasswordResetPages object covers both halves of recovery — requesting a link, and using one — because they are one flow and a test usually crosses between them. One lint decision worth recording. Requesting a Playwright fixture IS using it: destructuring `customer` is what makes the account exist, whether or not the body then reads the address. The linter cannot see that side effect and reports every such fixture as an unused variable. The first attempt at appeasing it was a `void customer;` line per test, which is noise standing in for a comment — and sonarjs flags that too, so it traded one warning for another. `no-unused-vars` is now configured with `args: 'none'` for tests only, with the reason written next to it. Variables are still checked; only parameters are exempt. Verified: 26/26 in the converted batch, and 127 passed in the full suite with four failures — three in the known #116 flaky family, and resend-verification's rate-limit test, which passes 9/9 across three repeats in isolation and is timing-sensitive under parallel load rather than changed by this commit. Refs #137
147 lines
6.5 KiB
JavaScript
147 lines
6.5 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',
|
|
|
|
// Requesting a Playwright fixture IS using it — destructuring `customer`
|
|
// is what makes the account exist, whether or not the body then reads the
|
|
// address. The linter cannot see that side effect and reports every such
|
|
// fixture as an unused variable. Left on, the alternative is a `void
|
|
// customer;` line in each test, which is noise standing in for a comment.
|
|
// Variables are still checked; only parameters are exempt.
|
|
'@typescript-eslint/no-unused-vars': ['warn', { args: 'none' }],
|
|
},
|
|
}
|
|
);
|