Files
TREK/client/tests/unit/scripts/eslint-rules.test.ts
Maurice c266a5e1a7 chore(client): freeze typed promise, hook, size, store and global rules in the ESLint baseline
The client lint ran untyped with two react-hooks rules, so nothing saw a
floating promise, an async handler where a void one is expected, a whole-store
Zustand subscription or a window event bus, all of which client/CLAUDE.md
forbids. eslint.config.mjs now lints with the project service and adds, as
warnings: no-floating-promises, no-misused-promises (void returns in JSX
attributes included), the rest of react-hooks v7's recommended set,
complexity 20, max-depth 4, max-params 5 and max-lines-per-function (120 in a
.ts function, 300 in a .tsx one, whose body also holds its markup),
no-console and no-non-null-assertion. Three local rules in
scripts/lib/eslint-rules.mjs cover what no stock rule sees: an empty
.catch() callback, use*Store() without a selector (the message points to
useShallow, since an object selector without it loops on Zustand 5), and
window.__* globals and window.dispatchEvent with an event of our own.
scripts/eslint-baseline.json takes today's count of each new rule, so the
existing code is frozen, not changed, and each new instance fails
lint:warnings. The CI step gets the 8 GB heap typed linting needs.
2026-10-10 21:32:23 +02:00

67 lines
2.5 KiB
TypeScript

import { RuleTester } from 'eslint';
import tseslint from 'typescript-eslint';
import { describe, it } from 'vitest';
import trek from '../../../scripts/lib/eslint-rules.mjs';
// RuleTester reports through the test framework's describe/it.
RuleTester.describe = describe;
RuleTester.it = it;
const tester = new RuleTester({
languageOptions: {
parser: tseslint.parser,
parserOptions: { ecmaFeatures: { jsx: true } },
},
});
describe('trek/no-swallowed-catch', () => {
tester.run('no-swallowed-catch', trek.rules['no-swallowed-catch'], {
valid: [
'load().catch((err) => toast.error(err.message))',
'load().catch(() => setFailed(true))',
'promise.then(() => {})',
'try { a() } catch { b() }',
],
invalid: [
{ code: 'load().catch(() => {})', errors: [{ messageId: 'swallowed' }] },
{ code: 'load().catch(() => undefined)', errors: [{ messageId: 'swallowed' }] },
{ code: 'load().catch(() => null)', errors: [{ messageId: 'swallowed' }] },
{ code: 'load().catch(function () {})', errors: [{ messageId: 'swallowed' }] },
],
});
});
describe('trek/store-without-selector', () => {
tester.run('store-without-selector', trek.rules['store-without-selector'], {
valid: [
'const id = useTripStore((s) => s.trip.id)',
'const { a, b } = useTripStore(useShallow((s) => ({ a: s.a, b: s.b })))',
'useTripStore.getState().loadTrip(1)',
'const t = useTranslation()',
'const s = useStore()',
],
invalid: [
{ code: 'const store = useTripStore()', errors: [{ messageId: 'whole', data: { name: 'useTripStore' } }] },
{ code: 'const { settings } = useSettingsStore()', errors: [{ messageId: 'whole' }] },
],
});
});
describe('trek/no-window-globals', () => {
tester.run('no-window-globals', trek.rules['no-window-globals'], {
valid: [
"window.dispatchEvent(new Event('resize'))",
"window.addEventListener('resize', onResize)",
'window.location.reload()',
'const data = dragSession.current',
],
invalid: [
{ code: 'window.__dragData = { placeId: 1 }', errors: [{ messageId: 'global', data: { name: '__dragData' } }] },
{ code: "window.__addToast?.('saved', 'success')", errors: [{ messageId: 'global' }] },
{ code: '(window as any).__dragData = null', errors: [{ messageId: 'global' }] },
{ code: "window.dispatchEvent(new CustomEvent('accommodations:refresh'))", errors: [{ messageId: 'bus' }] },
{ code: "window.dispatchEvent(new Event('collab-files-changed'))", errors: [{ messageId: 'bus' }] },
],
});
});