Run client typecheck before vitest in npm test.

Duplicate keys in strings.ts were green under vitest and red only on build. Drop the self file: dependency loop so npm test is enough.
This commit is contained in:
Leonid Pershin
2026-08-20 12:57:16 +03:00
parent 9a6cbff55a
commit 45059b927f
7 changed files with 136 additions and 24 deletions
+7 -4
View File
@@ -46,8 +46,9 @@ dotnet test tests/HSchool.People.Tests --filter FullyQualifiedName~YearlyIntake
npm --prefix src/HSchool.Client test -- src/ui/peoplePanel.test.ts
```
Solution-wide `dotnet test` and a full client `npm test` are CI, or when the user asked. Agents
scope the run — Testing policy.
Client `npm test` runs `tsc -b` (`typecheck`) then vitest; extra args after `--` go to vitest.
`test:watch` is vitest only. Solution-wide `dotnet test` and a full client `npm test` are CI, or
when the user asked. Agents scope the run — Testing policy.
```bash
npm --prefix src/HSchool.Client run build
@@ -126,7 +127,7 @@ say so explicitly in the change description.
## Testing policy
**What to run.** One project from the list below, filtered to the new or changed class while
iterating. Client Vitest only if `src/HSchool.Client` changed. `HSchool.AppHost.Tests` only if
iterating. Client `npm test` (typecheck + Vitest) only if `src/HSchool.Client` changed. `HSchool.AppHost.Tests` only if
the change is HTTP, WebSocket, or host wiring — it boots a server. Do not `dotnet test` the
solution, do not run client and .NET together "to be sure", do not re-run after a merge that
only resolved a status line. Solution-wide is CI.
@@ -155,7 +156,9 @@ only resolved a status line. Solution-wide is CI.
- Screen logic is covered in Vitest under happy-dom (`ui/*.test.ts`): people filters and the
pager, the create dialog (core stays on, map reset, submit busy), planner rejection text, and
the payroll-cap message. Layout and styles are still verified by running the app. Dictionaries
and date formatting stay in `i18n/strings.test.ts` and `format/gameTime.test.ts`.
and date formatting stay in `i18n/strings.test.ts` and `format/gameTime.test.ts`. A duplicate
key in `strings.ts` is a typecheck failure (`TS1117` / `TS2300`); client `npm test` runs tsc
before vitest, so it is red without `npm run build`.
## Dependencies
+8 -8
View File
@@ -11,20 +11,20 @@
## Задачи
- [ ] `package.json``"test":` сначала `tsc -b` (`typecheck`), потом `vitest run`. Без
- [x] `package.json``"test":` сначала `tsc -b` (`typecheck`), потом `vitest run`. Без
`&&` скрывать код выхода typecheck
- [ ] Убрать петлю `"hschool-client": "file:"` из `dependencies`; `npm test` и `npm run build`
- [x] Убрать петлю `"hschool-client": "file:"` из `dependencies`; `npm test` и `npm run build`
без неё
- [ ] `AGENTS.md` Commands и Testing policy: клиентский `npm test` включает typecheck
- [ ] `test:watch` по-прежнему только vitest — typecheck на каждый файл в watch не нужен
- [x] `AGENTS.md` Commands и Testing policy: клиентский `npm test` включает typecheck
- [x] `test:watch` по-прежнему только vitest — typecheck на каждый файл в watch не нужен
## Тесты, без которых фаза не закрыта
- [ ] `npm --prefix src/HSchool.Client test` гоняет tsc и vitest и проходит на текущем дереве
- [ ] Дубликат ключа в одном объекте `strings.ts` (ru или en) делает `npm test` красным без
- [x] `npm --prefix src/HSchool.Client test` гоняет tsc и vitest и проходит на текущем дереве
- [x] Дубликат ключа в одном объекте `strings.ts` (ru или en) делает `npm test` красным без
`npm run build`
- [ ] В `package.json` нет зависимости `hschool-client`
- [ ] `npm --prefix src/HSchool.Client run build` проходит
- [x] В `package.json` нет зависимости `hschool-client`
- [x] `npm --prefix src/HSchool.Client run build` проходит
## Критерий готовности
-7
View File
@@ -7,9 +7,6 @@
"": {
"name": "hschool-client",
"version": "0.1.0",
"dependencies": {
"hschool-client": "file:"
},
"devDependencies": {
"@types/node": "^24.10.1",
"happy-dom": "^20.11.2",
@@ -597,10 +594,6 @@
"node": ">=20.0.0"
}
},
"node_modules/hschool-client": {
"resolved": "",
"link": true
},
"node_modules/lightningcss": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
+1 -4
View File
@@ -11,7 +11,7 @@
"build": "tsc -b && vite build",
"preview": "vite preview",
"typecheck": "tsc -b",
"test": "vitest run",
"test": "node ./scripts/run-test.mjs",
"test:watch": "vitest"
},
"devDependencies": {
@@ -20,8 +20,5 @@
"typescript": "~5.9.3",
"vite": "^8.2.1",
"vitest": "^4.1.10"
},
"dependencies": {
"hschool-client": "file:"
}
}
@@ -0,0 +1,81 @@
import { spawnSync } from 'node:child_process';
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { createRequire } from 'node:module';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
const clientRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const require = createRequire(import.meta.url);
const tscBin = require.resolve('typescript/bin/tsc');
const pkg = JSON.parse(readFileSync(path.join(clientRoot, 'package.json'), 'utf8')) as {
scripts: Record<string, string>;
dependencies?: Record<string, string>;
};
describe('npm test script', () => {
it('runs typecheck then vitest, without swallowing tsc', () => {
expect(pkg.scripts.test).toBe('node ./scripts/run-test.mjs');
const runner = readFileSync(path.join(clientRoot, 'scripts', 'run-test.mjs'), 'utf8');
expect(runner).toContain("'-b'");
expect(runner).toContain("'run'");
expect(runner).toMatch(/if \(typecheck !== 0\)/);
expect(pkg.scripts.typecheck).toBe('tsc -b');
});
it('keeps watch as vitest only', () => {
expect(pkg.scripts['test:watch']).toBe('vitest');
expect(pkg.scripts['test:watch']).not.toMatch(/typecheck|tsc/);
});
it('does not depend on the hschool-client file: loop', () => {
expect(pkg.dependencies?.['hschool-client']).toBeUndefined();
});
});
describe('duplicate dictionary keys', () => {
it('makes tsc fail on a strings.ts ru object without vite build', () => {
const dir = mkdtempSync(path.join(tmpdir(), 'hschool-dup-'));
try {
const original = readFileSync(path.join(clientRoot, 'src', 'i18n', 'strings.ts'), 'utf8');
const needle = " createSchool: 'Создать школу',";
expect(original).toContain(needle);
const duplicate = original.replace(
needle,
`${needle}\n createSchool: 'дубликат',`,
);
writeFileSync(path.join(dir, 'strings.ts'), duplicate);
writeFileSync(
path.join(dir, 'locale.ts'),
readFileSync(path.join(clientRoot, 'src', 'i18n', 'locale.ts'), 'utf8'),
);
writeFileSync(
path.join(dir, 'tsconfig.json'),
JSON.stringify({
compilerOptions: {
strict: true,
noEmit: true,
target: 'ES2022',
module: 'ESNext',
moduleResolution: 'bundler',
allowImportingTsExtensions: true,
verbatimModuleSyntax: true,
lib: ['ES2022', 'DOM'],
},
include: ['strings.ts', 'locale.ts'],
}),
);
const result = spawnSync(process.execPath, [tscBin, '-p', dir], {
encoding: 'utf8',
});
const output = `${result.stdout ?? ''}${result.stderr ?? ''}`;
expect(result.status).not.toBe(0);
expect(output).toMatch(/TS1117|TS2300|TS1118/);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
});
+38
View File
@@ -0,0 +1,38 @@
// `tsc -b && vitest run` on Windows npm (tsc.cmd) can drop the typecheck exit code.
import { spawnSync } from 'node:child_process';
import { createRequire } from 'node:module';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const clientRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const require = createRequire(import.meta.url);
const win = process.platform === 'win32';
function statusOf(result) {
if (result.error) {
console.error(result.error);
return 1;
}
return result.status ?? 1;
}
const typecheck = statusOf(
spawnSync(process.execPath, [require.resolve('typescript/bin/tsc'), '-b'], {
cwd: clientRoot,
stdio: 'inherit',
}),
);
if (typecheck !== 0) {
process.exit(typecheck);
}
const vitest = path.join(clientRoot, 'node_modules', '.bin', win ? 'vitest.cmd' : 'vitest');
process.exit(
statusOf(
spawnSync(vitest, ['run', ...process.argv.slice(2)], {
cwd: clientRoot,
stdio: 'inherit',
shell: win,
}),
),
);
+1 -1
View File
@@ -20,5 +20,5 @@
"noUncheckedSideEffectImports": true,
"types": ["node"]
},
"include": ["vite.config.ts"]
"include": ["vite.config.ts", "scripts/**/*.ts"]
}