Files
TREK/client/tests/unit/scripts/size-lint.test.ts
T
Maurice 2f54f04380 ci(client): lint:size counts wrapped lines and covers tests and stylesheets
lint:size counted newlines, so a file could stay under its entry by
joining lines, and the trip planner page already carried one JSX element
on a single 3760 character line. A line now counts once per 120
characters it spans, the printWidth in .prettierrc, so formatted code
measures what it shows and packed code measures what it holds.

The check also covered only .ts and .tsx sources. Stylesheets now share
the 1000 line limit, and tests get a limit of their own at 2000: they
mirror the component they cover and carry fixtures, but the largest are
past 5000 lines and growing with the god components.

The baseline is measured again with the new rule and keyed by the path
from client/, since it now spans src/ and tests/. Entries are higher in
number than before because the metric changed, not because a file grew;
every file is held at exactly what it holds today. New entries are the
ten largest tests, index.css and studio.css, the generated
countryShapes.ts (305 lines of 2600 characters each) and
JourneyDetailPageEntryEditor.tsx, which was just under the old count.

The scanning, baseline reading and lowering move to scripts/lib/ so the
next ratchets share them. A baseline that is missing or misshapen, a
directory that is not there and a scan that finds nothing now fail the
run instead of reading as empty, and the command file always runs its
check: there is no entry point guard left that could skip it.
2026-10-08 13:04:39 +02:00

88 lines
3.9 KiB
TypeScript

import { readFileSync } from 'node:fs';
import { afterEach, describe, expect, it } from 'vitest';
import { RatchetError } from '../../../scripts/lib/ratchet.mjs';
import { check, groupOf, sizeOf, WIDTH } from '../../../scripts/lib/size.mjs';
import { lines, ratchetTree, type RatchetTree } from '../../helpers/ratchetFixture';
let tree: RatchetTree;
afterEach(() => tree?.remove());
const BASELINE = 'scripts/size-baseline.json';
function run(baseline: Record<string, number>, files: Record<string, string>, update = false) {
tree = ratchetTree({ [BASELINE]: JSON.stringify(baseline), 'tests/keep.test.ts': lines(1), ...files });
return check({ root: tree.root, baselinePath: tree.path(BASELINE), update, ...tree.out });
}
describe('lint:size', () => {
it('SIZE-001: counts lines as an editor wraps them, so joining lines does not shrink a file', () => {
expect(sizeOf('')).toBe(0);
expect(sizeOf('a\nb\n')).toBe(2);
expect(sizeOf('a\nb')).toBe(2);
expect(sizeOf('a\r\nb\r\n')).toBe(2);
expect(sizeOf('x'.repeat(WIDTH))).toBe(1);
expect(sizeOf('x'.repeat(WIDTH + 1))).toBe(2);
expect(sizeOf(`${'x'.repeat(3760)}\n`)).toBe(Math.ceil(3760 / WIDTH));
expect(sizeOf('\n\n')).toBe(2);
});
it('SIZE-002: sorts files into source, stylesheet and test, each with its own limit', () => {
expect(groupOf('src/a/B.tsx')).toMatchObject({ name: 'source', limit: 1000 });
expect(groupOf('src/index.css')).toMatchObject({ name: 'stylesheet', limit: 1000 });
expect(groupOf('src/a/B.test.tsx')).toMatchObject({ name: 'test', limit: 2000 });
expect(groupOf('tests/unit/x.ts')).toMatchObject({ name: 'test', limit: 2000 });
expect(groupOf('src/a/notes.md')).toBeNull();
});
it('SIZE-003: passes files within their limits', () => {
const code = run({}, { 'src/a.ts': lines(1000), 'src/a.css': lines(1000), 'tests/a.test.ts': lines(2000) });
expect(code).toBe(0);
expect(tree.error).toEqual([]);
});
it('SIZE-004: fails a new source file past 1000 lines', () => {
expect(run({}, { 'src/big.tsx': lines(1001) })).toBe(1);
expect(tree.error.join('\n')).toMatch(/src\/big\.tsx: 1001 lines .*limit for a source file is 1000/);
});
it('SIZE-005: fails a file that packs its code into long lines', () => {
expect(run({}, { 'src/packed.tsx': `${'x'.repeat(WIDTH * 2)}\n`.repeat(600) })).toBe(1);
expect(tree.error.join('\n')).toMatch(/src\/packed\.tsx: 1200 lines/);
});
it('SIZE-006: covers stylesheets and tests', () => {
expect(run({}, { 'src/styles/a.css': lines(1001), 'src/A.test.tsx': lines(2001) })).toBe(1);
expect(tree.error.join('\n')).toMatch(/src\/styles\/a\.css/);
expect(tree.error.join('\n')).toMatch(/src\/A\.test\.tsx: 2001 lines .*test file is 2000/);
});
it('SIZE-007: holds a baselined file at its entry and fails it once it grows', () => {
expect(run({ 'src/big.ts': 1200 }, { 'src/big.ts': lines(1200) })).toBe(0);
tree.remove();
expect(run({ 'src/big.ts': 1200 }, { 'src/big.ts': lines(1201) })).toBe(1);
expect(tree.error.join('\n')).toMatch(/its baseline is 1200/);
});
it('SIZE-008: --update lowers an entry, drops one back under the limit and never raises', () => {
run(
{ 'src/shrank.ts': 1500, 'src/back.ts': 1200, 'src/grew.ts': 1100 },
{
'src/shrank.ts': lines(1300),
'src/back.ts': lines(900),
'src/grew.ts': lines(1150),
},
true
);
expect(JSON.parse(readFileSync(tree.path(BASELINE), 'utf8'))).toEqual({
'src/shrank.ts': 1300,
'src/grew.ts': 1100,
});
expect(tree.error.join('\n')).toMatch(/src\/grew\.ts: 1150 lines/);
});
it('SIZE-009: a broken baseline stops the check', () => {
tree = ratchetTree({ [BASELINE]: '{', 'src/a.ts': lines(1), 'tests/a.ts': lines(1) });
expect(() => check({ root: tree.root, baselinePath: tree.path(BASELINE), ...tree.out })).toThrow(RatchetError);
});
});