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:
@@ -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
|
||||
|
||||
|
||||
@@ -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` проходит
|
||||
|
||||
## Критерий готовности
|
||||
|
||||
|
||||
Generated
-7
@@ -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",
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
}),
|
||||
),
|
||||
);
|
||||
@@ -20,5 +20,5 @@
|
||||
"noUncheckedSideEffectImports": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
"include": ["vite.config.ts", "scripts/**/*.ts"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user