test: run the suite and benchmark with Bun's native test runner
Replace Vitest with bun test across the whole suite. A preload compiles components through Angular's JIT transform with inlined templates, sets up jsdom, TestBed and zoneless change detection, and a small shim covers the Vitest APIs bun:test lacks. Files run isolated, real-worker specs spawn the running Bun, and line coverage from lcov is converted to the existing HTML and Cobertura reports. The CI benchmark replaces its heap cap with a container memory limit. Vitest, Jest, Nx and their Vite configuration are removed. Refs #291 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
558c70aa24
commit
94d57add2c
44 files changed
+446
-6481
No files matched your search
+4
-3
@@ -3,11 +3,12 @@
|
||||
All AI agents (including Cline, Antigravity, Cursor, and Copilot) must adhere to the following rules before declaring a task complete:
|
||||
|
||||
1. **Test Coverage (>= 80%)**:
|
||||
- The overall project code coverage (Statements, Lines, and Functions) must be maintained at **80% or higher**.
|
||||
- The overall project line coverage must be maintained at **80% or higher**. Bun reports line data only; statement, branch and function coverage are no longer measured.
|
||||
- Verify coverage by running:
|
||||
```bash
|
||||
bun scripts/generate-langs.js
|
||||
NODE_OPTIONS=--max-old-space-size=2048 bunx vitest run --coverage --maxWorkers=1 --exclude='src/app/benchmark/**'
|
||||
bun test --config=bunfig.coverage.toml --isolate --path-ignore-patterns='**/benchmark/**'
|
||||
bun scripts/coverage-reports.mjs
|
||||
```
|
||||
- If your changes introduce new code or modify existing code in a way that drops the coverage below 80%, you must write appropriate unit tests in `.spec.ts` files to bring it back up.
|
||||
|
||||
@@ -33,4 +34,4 @@ All AI agents (including Cline, Antigravity, Cursor, and Copilot) must adhere to
|
||||
- **Unit Tests and Coverage**: generate the language registry, then run the coverage command in rule 1, matching the Gitea functional-test job.
|
||||
- **CI policy and publication**: `node scripts/ci-policy.test.mjs` and `node scripts/ci-image.test.mjs`
|
||||
- **Production Build**: `bun run build -- --configuration production`
|
||||
- The separate performance benchmark is a required CI job with a relative gate: each scenario's budget is a multiple of the frozen engine's two-year calibration measured in the same run. Run it outside coverage with `NODE_OPTIONS=--max-old-space-size=2048 bunx vitest run src/app/benchmark/ --maxWorkers=1` when verifying performance; see `docs/benchmark/README.md`.
|
||||
- The separate performance benchmark is a required CI job with a relative gate: each scenario's budget is a multiple of the frozen engine's two-year calibration measured in the same run. Run it outside coverage with `timeout 600s bun test src/app/benchmark --timeout=600000` when verifying performance; see `docs/benchmark/README.md`.
|
||||
+10
-9
@@ -52,18 +52,19 @@ jobs:
|
||||
name: test
|
||||
runs-on: ubuntu-latest
|
||||
container: node:24.21.0
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=2048
|
||||
steps:
|
||||
- uses: https://gitea.com/actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- run: npm install --global bun@1.4.2
|
||||
- run: npm install --global bun@1.4.3
|
||||
- run: bun install --frozen-lockfile
|
||||
- run: bun scripts/generate-langs.js
|
||||
- name: Functional coverage
|
||||
run: bunx vitest run --coverage --maxWorkers=1 --exclude='src/app/benchmark/**'
|
||||
run: bun test --config=bunfig.coverage.toml --isolate --path-ignore-patterns='**/benchmark/**'
|
||||
- name: Coverage report
|
||||
if: ${{ always() }}
|
||||
run: bun scripts/coverage-reports.mjs
|
||||
- name: Upload coverage
|
||||
if: ${{ always() }}
|
||||
uses: https://gitea.com/actions/upload-artifact@v3
|
||||
with:
|
||||
@@ -75,18 +76,18 @@ jobs:
|
||||
benchmark:
|
||||
name: benchmark
|
||||
runs-on: ubuntu-latest
|
||||
container: node:24.21.0
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=2048
|
||||
container:
|
||||
image: node:24.21.0
|
||||
options: --memory=3g
|
||||
steps:
|
||||
- uses: https://gitea.com/actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- run: npm install --global bun@1.4.2
|
||||
- run: npm install --global bun@1.4.3
|
||||
- run: bun install --frozen-lockfile
|
||||
- run: bun scripts/generate-langs.js
|
||||
- name: Benchmark protocol with relative gate
|
||||
run: timeout 600s bunx vitest run src/app/benchmark/ --maxWorkers=1
|
||||
run: timeout 600s bun test src/app/benchmark --timeout=600000
|
||||
|
||||
build:
|
||||
name: build
|
||||
|
||||
@@ -162,7 +162,7 @@ Incomplete outcomes explain the retained plan and unfinished phases; conflicts i
|
||||
- Web Workers for planner isolation
|
||||
- `date-holidays` for the French public calendar
|
||||
- Bun for dependency management and project scripts
|
||||
- Vitest through the Angular test builder
|
||||
- `bun test` with jsdom and Angular TestBed (JIT)
|
||||
- ESLint and Prettier
|
||||
- Angular server build hosted by Express
|
||||
- Gitea repository hosting and a multi-stage Docker image
|
||||
@@ -209,22 +209,25 @@ Open <http://localhost:4200>. The development server reloads when source files c
|
||||
|
||||
### Useful commands
|
||||
|
||||
| Command | Purpose |
|
||||
| ----------------------------------------- | ------------------------------------------------------------ |
|
||||
| `bun start` | Generate the i18n registry and start the development server. |
|
||||
| `bun run test --watch=false` | Run the unit and integration suite once. |
|
||||
| `bun run lint` | Run ESLint. |
|
||||
| `bun run format:check` | Check Prettier formatting. |
|
||||
| `bunx tsc --noEmit -p tsconfig.app.json` | Type-check application code. |
|
||||
| `bunx tsc --noEmit -p tsconfig.spec.json` | Type-check tests. |
|
||||
| `bun run build` | Create the optimized browser and server bundles in `dist/`. |
|
||||
| `bun run serve:ssr:vac-optimizer` | Serve a completed production build on port 4000. |
|
||||
| Command | Purpose |
|
||||
| ------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
||||
| `bun start` | Generate the i18n registry and start the development server. |
|
||||
| `bun run test` | Run the unit, integration and benchmark suites once. |
|
||||
| `bun test --isolate --path-ignore-patterns='**/benchmark/**'` | Run the functional suite only. |
|
||||
| `bun test src/app/benchmark --timeout=600000` | Run the planning benchmark (no coverage). |
|
||||
| `bun test --config=bunfig.coverage.toml --isolate --path-ignore-patterns='**/benchmark/**' && bun scripts/coverage-reports.mjs` | Functional coverage: text, `lcov.info`, HTML and Cobertura in `coverage/`. Line coverage only; frozen engine excluded. |
|
||||
| `bun run lint` | Run ESLint. |
|
||||
| `bun run format:check` | Check Prettier formatting. |
|
||||
| `bunx tsc --noEmit -p tsconfig.app.json` | Type-check application code. |
|
||||
| `bunx tsc --noEmit -p tsconfig.spec.json` | Type-check tests. |
|
||||
| `bun run build` | Create the optimized browser and server bundles in `dist/`. |
|
||||
| `bun run serve:ssr:vac-optimizer` | Serve a completed production build on port 4000. |
|
||||
|
||||
## Performance benchmark
|
||||
|
||||
Build or start the application, then open `/benchmark`. The isolated harness covers a representative year, typed obligations, chained earned entitlements, a deliberate conflict, a loaded year, and two- and three-year horizons with dated annual balances. It reports preparation time, planning latency, planning-work time, total wall time through explanation delivery, UI timer delay, available main-thread heap measurements, outcome, last completed stage, placement count, and determinism.
|
||||
|
||||
The release protocol, recorded desktop measurements, and hardware limitations are documented in [docs/benchmark/README.md](docs/benchmark/README.md). No reference-phone run or individual multi-year duration ceiling is established. CI requires the complete seven-scenario Node protocol to finish within 300 seconds; individual scenario timings remain diagnostic evidence.
|
||||
The release protocol, recorded desktop measurements, and hardware limitations are documented in [docs/benchmark/README.md](docs/benchmark/README.md). No reference-phone run or individual multi-year duration ceiling is established. CI runs the complete seven-scenario protocol with `bun test` and requires each scenario to stay within its budget relative to a frozen-engine calibration, with a 600-second hang guard. Bun measurements do not establish browser or reference-phone speedups.
|
||||
|
||||
## Docker
|
||||
|
||||
@@ -239,9 +242,9 @@ The image builds the Angular application with Bun and Node, then runs the genera
|
||||
|
||||
[`.gitea/workflows/ci.yml`](.gitea/workflows/ci.yml) defines Gitea Actions validation, functional coverage, production builds, security scans, and Docker image publication. [Issue #268](https://gitea.nessar.fr/nessar/vac-optimizer/issues/268) tracks activation. Actions is enabled with owner approval; publisher credentials and live workflow validation are still pending. The [activation guide](docs/gitea-actions.md) records runner, credential, permission, and merge-gate requirements.
|
||||
|
||||
The workflow runs on branch/tag pushes, PR opening/reopening/synchronization/edits, and manual dispatch. Manual runs validate without publishing images. Required jobs cover policy regression tests, GitFlow and Conventional Commits, ESLint, Prettier, application/test type checks, Vitest coverage, the production build, Semgrep SAST, and Trivy dependency/secret scans. Coverage, build output, and SARIF reports are retained for seven days.
|
||||
The workflow runs on branch/tag pushes, PR opening/reopening/synchronization/edits, and manual dispatch. Manual runs validate without publishing images. Required jobs cover policy regression tests, GitFlow and Conventional Commits, ESLint, Prettier, application/test type checks, Bun coverage, the production build, Semgrep SAST, and Trivy dependency/secret scans. Coverage, build output, and SARIF reports are retained for seven days.
|
||||
|
||||
Functional tests use one worker and a 2 GiB Node heap. The separate required benchmark job runs without coverage and keeps its five-minute protocol timeout, one warm-up and two measured repetitions of all seven scenarios. [#78](https://gitea.nessar.fr/nessar/vac-optimizer/issues/78) restores this gate after a complete local pass in 298.6 seconds; see [the measurements and hardware limits](docs/benchmark/issue-78.md). Benchmark success is required for image publication and the aggregate `required` job. The narrow local timing margin still needs confirmation on the CI runner.
|
||||
Functional tests run with `bun test --isolate`; coverage is line-level only (Bun reports no branch or function data). The separate required benchmark job runs without coverage in a container limited to 3 GiB (JavaScriptCore has no heap-size flag, so this replaces the former 2 GiB Node heap cap) and keeps its 600-second hang guard, one warm-up and two measured repetitions of all seven scenarios. [#78](https://gitea.nessar.fr/nessar/vac-optimizer/issues/78) restores this gate after a complete local pass in 298.6 seconds; see [the measurements and hardware limits](docs/benchmark/issue-78.md). Benchmark success is required for image publication and the aggregate `required` job. The narrow local timing margin still needs confirmation on the CI runner.
|
||||
|
||||
The `required` job aggregates successful validation and expected image publication, accepting the image job's intentional skip for fork PRs and manual runs. Image jobs require successful validation and are limited to pushes and same-repository PRs. Registry credentials are confined to the publishing step.
|
||||
|
||||
|
||||
@@ -85,9 +85,6 @@
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular/build:extract-i18n"
|
||||
},
|
||||
"test": {
|
||||
"builder": "@analogjs/vitest-angular:test"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
[test]
|
||||
preload = ["./src/test-setup.ts"]
|
||||
root = "src"
|
||||
coverage = true
|
||||
coverageReporter = ["text", "lcov"]
|
||||
coverageDir = "coverage"
|
||||
coverageSkipTestFiles = true
|
||||
coveragePathIgnorePatterns = ["**/frozen-engine/**", "**/testing/**", "**/test-setup.ts"]
|
||||
@@ -0,0 +1,3 @@
|
||||
[test]
|
||||
preload = ["./src/test-setup.ts"]
|
||||
root = "src"
|
||||
@@ -68,6 +68,18 @@ the loaded year, while every completed scenario was within budget. The CI
|
||||
only hang guards and do not judge speed; #286 lowers them once the speed
|
||||
target is met.
|
||||
|
||||
The protocol now runs on Bun's test runner, outside coverage:
|
||||
`timeout 600s bun test src/app/benchmark --timeout=600000`. JavaScriptCore has
|
||||
no equivalent of Node's `--max-old-space-size`, so the CI benchmark job enforces
|
||||
memory with a 3 GiB container limit instead of the former 2 GiB heap cap.
|
||||
Scenarios, calibration, warm-up, repetitions, determinism checks and
|
||||
multipliers are unchanged. One local run (Bun 1.4.3, Node unavailable) took
|
||||
268 seconds and passed every scenario; the frozen-engine calibration was
|
||||
3.79 s and the ratios were 0.14 (representative year), 1.43 (typed
|
||||
obligations), 0.01 (chained earned entitlements), 0.00 (deliberate conflict),
|
||||
17.50 (loaded year), 0.64 (two years) and 2.18 (three years). These are Bun
|
||||
measurements; they do not establish browser or reference-phone speedups.
|
||||
|
||||
The post-removal functional integration checks, browser QA and unchanged-input
|
||||
handoff for #78 are recorded in [the #77 verification report](../trace-free-planning-verification.md).
|
||||
|
||||
@@ -136,12 +148,12 @@ bun build src/app/services/leave-planner.core.ts --target=node --external date-h
|
||||
A failure prints the seed and the full request as JSON. To run only that seed:
|
||||
|
||||
```sh
|
||||
FROZEN_ENGINE_SEED=<seed> bunx vitest run src/app/services/frozen-engine.spec.ts
|
||||
FROZEN_ENGINE_SEED=<seed> bun test --isolate src/app/services/frozen-engine.spec.ts
|
||||
```
|
||||
|
||||
The check runs in the functional suite. On the development laptop it takes
|
||||
about 32 seconds without coverage and 38 seconds with coverage
|
||||
(`NODE_OPTIONS=--max-old-space-size=2048 bunx vitest run src/app/services/frozen-engine.spec.ts --maxWorkers=1`).
|
||||
The check runs in the functional suite. On the development laptop with Bun
|
||||
1.4.3 it takes about 54 seconds with or without coverage
|
||||
(`bun test --config=bunfig.coverage.toml --isolate src/app/services/frozen-engine.spec.ts`; coverage is line-level only).
|
||||
A flipped objective direction in `compareObjectiveValues` made 26 of its tests
|
||||
fail, starting at seed 280002; the change was reverted.
|
||||
|
||||
|
||||
+12
-12
@@ -16,16 +16,16 @@ synchronization, and title/body edits. Manual dispatch runs validation without
|
||||
publishing images. There is no schedule or deployment job in the preserved
|
||||
configuration; the Compose files remain manual deployment options.
|
||||
|
||||
| Job | Behavior |
|
||||
| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `policy` | Tests the policy and publisher, then validates the checked-out commit, GitFlow, PR title, commits, and stable release version. |
|
||||
| `validate` | Runs lint, formatting, and application/test type checks. |
|
||||
| `test` | Runs functional coverage with one worker and a 2 GiB Node heap; uploads coverage for seven days even on failure. |
|
||||
| `benchmark` | Required seven-scenario protocol (one warm-up, two measured repetitions) outside coverage with one worker and a 2 GiB Node heap. It first times the frozen engine on `two years` (best of three) and fails any scenario whose mean planning time exceeds its multiplier times that calibration; the 600-second timeout is only a hang guard. |
|
||||
| `build` | Builds browser/server production bundles and retains `dist` for seven days. |
|
||||
| `security` | Runs Semgrep JavaScript/TypeScript SAST and Trivy dependency/secret scans; retains SARIF reports for seven days. Scanner execution must succeed; findings remain reports rather than a new severity gate. |
|
||||
| `images` | Publishes branch/SHA images after required validation, including the benchmark, or promotes an existing SHA image for a stable tag. Runs only for pushes and same-repository PRs. |
|
||||
| `required` | Fails on failed, cancelled, or skipped required validation, including the benchmark. Requires image publication when expected; accepts its intentional skip for fork PRs and manual dispatch. |
|
||||
| Job | Behavior |
|
||||
| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `policy` | Tests the policy and publisher, then validates the checked-out commit, GitFlow, PR title, commits, and stable release version. |
|
||||
| `validate` | Runs lint, formatting, and application/test type checks. |
|
||||
| `test` | Runs functional coverage with `bun test --isolate` (line coverage only), converts it to HTML and Cobertura, and uploads coverage for seven days even on failure. |
|
||||
| `benchmark` | Required seven-scenario protocol (one warm-up, two measured repetitions) outside coverage in a container limited to 3 GiB. It first times the frozen engine on `two years` (best of three) and fails any scenario whose mean planning time exceeds its multiplier times that calibration; the 600-second timeout is only a hang guard. |
|
||||
| `build` | Builds browser/server production bundles and retains `dist` for seven days. |
|
||||
| `security` | Runs Semgrep JavaScript/TypeScript SAST and Trivy dependency/secret scans; retains SARIF reports for seven days. Scanner execution must succeed; findings remain reports rather than a new severity gate. |
|
||||
| `images` | Publishes branch/SHA images after required validation, including the benchmark, or promotes an existing SHA image for a stable tag. Runs only for pushes and same-repository PRs. |
|
||||
| `required` | Fails on failed, cancelled, or skipped required validation, including the benchmark. Requires image publication when expected; accepts its intentional skip for fork PRs and manual dispatch. |
|
||||
|
||||
The benchmark runs on both push and pull_request events: the push run gates image
|
||||
publication (`images.needs` includes `benchmark`) and the `required` aggregate.
|
||||
@@ -41,13 +41,13 @@ branch slug; stable promotions share one group and are not cancelled in progress
|
||||
|
||||
## Runners and permissions
|
||||
|
||||
Validation uses Node 24.21.0 and Bun 1.4.2 with the frozen lockfile. The image
|
||||
Validation uses Node 24.21.0 and Bun 1.4.2 with the frozen lockfile; the test and benchmark jobs use Bun 1.4.3. The image
|
||||
job uses Docker 29.8.2 and its matching Docker-in-Docker service. Scanner
|
||||
versions are Semgrep 1.180.0 and Trivy 0.75.0; Trivy downloads are checksum-checked.
|
||||
Trivy downloads its vulnerability database from Aqua's public ECR mirror because
|
||||
the default registries returned a missing database blob and a download timeout.
|
||||
Jobs select the `ubuntu-latest` label and require Linux container-job support.
|
||||
Allow at least the preserved 2 GiB Node heap plus runner and container overhead.
|
||||
Allow at least the benchmark job's 3 GiB container memory limit plus runner overhead.
|
||||
|
||||
The instance currently has an online inherited runner named `srvhome`, labelled
|
||||
`ubuntu-latest` and `ubuntu-24.04`. Repository runners, secrets, and variables
|
||||
|
||||
@@ -35,9 +35,9 @@ If Cancel is accepted just before a queued natural result reaches the facade, th
|
||||
Run the focused checks with the project's installed Bun and Node:
|
||||
|
||||
```sh
|
||||
bunx vitest run src/app/services/leave-planner-cancellation.spec.ts
|
||||
bunx vitest run src/app/services/leave-planner.worker.spec.ts
|
||||
bunx vitest run src/app/components/calendar-year/calendar-year-planning.spec.ts
|
||||
bun test --isolate src/app/services/leave-planner-cancellation.spec.ts
|
||||
bun test --isolate src/app/services/leave-planner.worker.spec.ts
|
||||
bun test --isolate src/app/components/calendar-year/calendar-year-planning.spec.ts
|
||||
bunx tsc --noEmit -p tsconfig.app.json
|
||||
bunx tsc --noEmit -p tsconfig.spec.json
|
||||
```
|
||||
|
||||
+7
-15
@@ -6,7 +6,7 @@
|
||||
"start": "bun scripts/generate-langs.js && ng serve",
|
||||
"build": "bun scripts/generate-langs.js && ng build",
|
||||
"watch": "bun scripts/generate-langs.js && ng build --watch --configuration development",
|
||||
"test": "bun scripts/generate-langs.js && ng test",
|
||||
"test": "bun scripts/generate-langs.js && bun test --isolate",
|
||||
"serve:ssr:vac-optimizer": "bun dist/vac-optimizer/server/server.mjs",
|
||||
"lint": "bun scripts/generate-langs.js && eslint \"src/**/*.ts\"",
|
||||
"lint:fix": "bun scripts/generate-langs.js && eslint \"src/**/*.ts\" --fix",
|
||||
@@ -46,33 +46,25 @@
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@analogjs/platform": "^2.7.5",
|
||||
"@analogjs/vite-plugin-angular": "^2.7.5",
|
||||
"@analogjs/vitest-angular": "^2.7.5",
|
||||
"@angular/build": "22.2.2",
|
||||
"@angular/cli": "22.2.2",
|
||||
"@angular/compiler-cli": "22.2.1",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@nx/devkit": "^23.0.0",
|
||||
"@nx/vite": "^23.0.0",
|
||||
"@types/bun": "1.4.3",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/jsdom": "^21.1.7",
|
||||
"@types/node": "^26.0.0",
|
||||
"@vitest/coverage-istanbul": "^5.0.0",
|
||||
"@vitest/coverage-v8": "^5.0.0",
|
||||
"@vitest/ui": "^5.0.0",
|
||||
"eslint": "^10.4.1",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.6",
|
||||
"fast-check": "4.10.2",
|
||||
"jest": "^30.4.2",
|
||||
"jest-preset-angular": "^17.0.0",
|
||||
"istanbul-lib-coverage": "^3.2.2",
|
||||
"istanbul-lib-report": "^3.0.1",
|
||||
"istanbul-reports": "^3.2.0",
|
||||
"jsdom": "^30.0.0",
|
||||
"prettier": "^3.8.3",
|
||||
"typescript": "~6.0.3",
|
||||
"typescript-eslint": "^8.60.0",
|
||||
"vite": "^8.0.0",
|
||||
"vite-tsconfig-paths": "^6.0.0",
|
||||
"vitest": "^5.0.0"
|
||||
"typescript-eslint": "^8.60.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Converts coverage/lcov.info (Bun) to HTML and Cobertura. Run: bun scripts/coverage-reports.mjs
|
||||
// shortcut: line coverage only, Bun's lcov has no usable branch/function data; revisit if Bun adds it.
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import libCoverage from 'istanbul-lib-coverage';
|
||||
import libReport from 'istanbul-lib-report';
|
||||
import reports from 'istanbul-reports';
|
||||
|
||||
const map = libCoverage.createCoverageMap({});
|
||||
const loc = (line) => ({ start: { line, column: 0 }, end: { line, column: 1 } });
|
||||
for (const record of readFileSync('coverage/lcov.info', 'utf8').split('end_of_record')) {
|
||||
const file = /^SF:(.+)$/m.exec(record)?.[1];
|
||||
if (!file) continue;
|
||||
const statementMap = {};
|
||||
const s = {};
|
||||
for (const [, line, hits] of record.matchAll(/^DA:(\d+),(\d+)/gm)) {
|
||||
statementMap[line] = loc(+line);
|
||||
s[line] = +hits;
|
||||
}
|
||||
map.addFileCoverage({
|
||||
path: resolve(file),
|
||||
statementMap,
|
||||
s,
|
||||
fnMap: {},
|
||||
f: {},
|
||||
branchMap: {},
|
||||
b: {},
|
||||
});
|
||||
}
|
||||
const context = libReport.createContext({ dir: 'coverage', coverageMap: map });
|
||||
for (const name of ['html', 'cobertura']) reports.create(name).execute(context);
|
||||
@@ -7,7 +7,7 @@ import { MatOptionHarness } from '@angular/material/core/testing';
|
||||
import { MatDialogHarness } from '@angular/material/dialog/testing';
|
||||
import { MatSelectHarness } from '@angular/material/select/testing';
|
||||
import { MatSlideToggleHarness } from '@angular/material/slide-toggle/testing';
|
||||
import { type MockInstance, vi } from 'vitest';
|
||||
import { type MockInstance, vi } from '../testing/vitest-compat';
|
||||
import { App } from './app';
|
||||
import { CalendarService } from './services/calendar-service';
|
||||
import { calculatePlanningInternal } from './services/leave-planner.core';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { vi } from 'vitest';
|
||||
import { vi } from '../../testing/vitest-compat';
|
||||
import { calculatePlanningAsyncInternal } from '../services/frozen-engine/leave-planner.core.frozen.mjs';
|
||||
import { createPlanningCalculation } from '../services/leave-planner.service';
|
||||
import {
|
||||
@@ -78,7 +78,7 @@ describe('leave planner benchmark protocol', () => {
|
||||
await calculatePlanningAsyncInternal({ ...request, calculationDate: '2024-01-01' });
|
||||
calibrationMs = Math.min(calibrationMs, performance.now() - start);
|
||||
}
|
||||
// Direct writes skip Vitest's per-call "stdout |" headers and appear immediately.
|
||||
// Direct stdout writes show each row as soon as its scenario finishes.
|
||||
const print = (text: string) => process.stdout.write(`${text}\n`);
|
||||
print(
|
||||
`\nCalibration: frozen engine on "two years", best of 3 = ${calibrationMs.toFixed(1)} ms` +
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createOptimizationPresetRules } from '../../services/rule-service';
|
||||
import { provideZonelessChangeDetection } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { vi } from 'vitest';
|
||||
import { vi } from '../../../testing/vitest-compat';
|
||||
import { CalendarYear } from './calendar-year';
|
||||
import { UserInput } from '../user-input/user-input';
|
||||
import {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { TranslationService } from '../../services/translation-service';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { CdkDragDrop, CdkDropList } from '@angular/cdk/drag-drop';
|
||||
import { LiveAnnouncer } from '@angular/cdk/a11y';
|
||||
import { vi } from 'vitest';
|
||||
import { vi } from '../../../testing/vitest-compat';
|
||||
import { OPTIMIZATION_PRESETS } from '../../services/planning-objectives';
|
||||
import { MatExpansionPanel } from '@angular/material/expansion';
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { provideZonelessChangeDetection } from '@angular/core';
|
||||
import { TestBed, type ComponentFixture } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { MatExpansionPanel } from '@angular/material/expansion';
|
||||
import { vi } from 'vitest';
|
||||
import { vi } from '../../../testing/vitest-compat';
|
||||
import { RuleService, RuleType } from '../../services/rule-service';
|
||||
import { TranslationService } from '../../services/translation-service';
|
||||
import { calculatePlanningInternal } from '../../services/leave-planner.core';
|
||||
|
||||
@@ -7,7 +7,7 @@ import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatDialogHarness } from '@angular/material/dialog/testing';
|
||||
import { MatSelectHarness } from '@angular/material/select/testing';
|
||||
import { MatSlideToggleHarness } from '@angular/material/slide-toggle/testing';
|
||||
import { vi, type MockInstance } from 'vitest';
|
||||
import { type MockInstance, vi } from '../../../testing/vitest-compat';
|
||||
import { UserInput } from './user-input';
|
||||
import { UserInputService } from '../../services/user-input-service';
|
||||
import { CalendarService } from '../../services/calendar-service';
|
||||
|
||||
@@ -4,7 +4,7 @@ import { tmpdir } from 'node:os';
|
||||
import { join, resolve } from 'node:path';
|
||||
import { promisify } from 'node:util';
|
||||
import { Worker as NodeWorker } from 'node:worker_threads';
|
||||
import { vi } from 'vitest';
|
||||
import { vi } from '../../testing/vitest-compat';
|
||||
import { createPlanningCalculation } from './leave-planner.service';
|
||||
import type { OptimizationPlanningRule } from './leave-planner.core';
|
||||
import { OPTIMIZATION_PRESETS } from './planning-objectives';
|
||||
@@ -28,7 +28,13 @@ await import(${JSON.stringify(resolve('src/app/services/leave-planner.worker.ts'
|
||||
parentPort.on('message', (data) => globalThis.self.onmessage({ data }));
|
||||
`,
|
||||
);
|
||||
await promisify(execFile)('bun', ['build', entry, '--target=node', '--outfile', bundle]);
|
||||
await promisify(execFile)(process.execPath, [
|
||||
'build',
|
||||
entry,
|
||||
'--target=node',
|
||||
'--outfile',
|
||||
bundle,
|
||||
]);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { provideZonelessChangeDetection } from '@angular/core';
|
||||
import { CalendarSettingsService } from './calendar-settings-service';
|
||||
import { vi } from 'vitest';
|
||||
import { vi } from '../../testing/vitest-compat';
|
||||
|
||||
describe('CalendarSettingsService', () => {
|
||||
let service: CalendarSettingsService;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { afterAll, describe, expect, it } from 'vitest';
|
||||
import { afterAll, describe, expect, it } from 'bun:test';
|
||||
import { createOptimizationPresetRules } from './rule-service';
|
||||
import { frenchPublicHolidaysForYear, isWorkingPlanningDate } from './declared-non-working-days';
|
||||
import { OPTIMIZATION_CATALOG } from './planning-objectives';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import {
|
||||
analyzeCounterfactualInternal,
|
||||
calculatePlanningInternal,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createOptimizationPresetRules } from './rule-service';
|
||||
import { vi } from 'vitest';
|
||||
import { vi } from '../../testing/vitest-compat';
|
||||
import { createPlanningCalculation, type PlanningIncompleteOutcome } from './leave-planner.service';
|
||||
|
||||
const request = {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createOptimizationPresetRules } from './rule-service';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
||||
import { vi } from '../../testing/vitest-compat';
|
||||
import { createPlanningCalculation } from './leave-planner.service';
|
||||
|
||||
const balance = (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createOptimizationPresetRules } from './rule-service';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import { calculatePlanningInternal, type PlanningArtifacts } from './leave-planner.core';
|
||||
import {
|
||||
evaluateOptimization,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { vi } from 'vitest';
|
||||
import { vi } from '../../testing/vitest-compat';
|
||||
import {
|
||||
calculatePlanningAsyncInternal,
|
||||
calculatePlanningInternal,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { vi } from 'vitest';
|
||||
import { vi } from '../../testing/vitest-compat';
|
||||
import {
|
||||
calculatePlanningInternal,
|
||||
type OptimizationPlanningRule,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { vi } from 'vitest';
|
||||
import { vi } from '../../testing/vitest-compat';
|
||||
import {
|
||||
createPlanningCalculation,
|
||||
type LeavePlacement,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createOptimizationPresetRules } from './rule-service';
|
||||
import { vi } from 'vitest';
|
||||
import { vi } from '../../testing/vitest-compat';
|
||||
import {
|
||||
PlanningOutcome,
|
||||
PlanningRequest,
|
||||
@@ -1562,7 +1562,11 @@ describe('planLeave', () => {
|
||||
const { isWorkingPlanningDate } = await import('./declared-non-working-days');
|
||||
const { addPlanningDays } = await import('./planning-date');
|
||||
const declaredDays: NonNullable<PlanningRequest['declaredDays']>[number][] = [];
|
||||
for (let date = declaredStart; date < declaredEnd; date = addPlanningDays(date, 1))
|
||||
for (
|
||||
let date: string = declaredStart;
|
||||
date < declaredEnd;
|
||||
date = addPlanningDays(date, 1)
|
||||
)
|
||||
if (isWorkingPlanningDate(date)) declaredDays.push({ date, type: 'other' });
|
||||
const result = isPlan(
|
||||
await runInNonBrowser({ horizon, balances: [], rules: [], declaredDays }),
|
||||
|
||||
@@ -4,7 +4,7 @@ import { tmpdir } from 'node:os';
|
||||
import { join, resolve } from 'node:path';
|
||||
import { Worker as NodeWorker } from 'node:worker_threads';
|
||||
import { promisify } from 'node:util';
|
||||
import { vi } from 'vitest';
|
||||
import { vi } from '../../testing/vitest-compat';
|
||||
import {
|
||||
createPlanningCalculation,
|
||||
type PlanningCalculationContext,
|
||||
@@ -57,7 +57,13 @@ await import(${JSON.stringify(resolve('src/app/services/leave-planner.worker.ts'
|
||||
parentPort.on('message', (data) => globalThis.self.onmessage({ data }));
|
||||
`,
|
||||
);
|
||||
await promisify(execFile)('bun', ['build', entry, '--target=node', '--outfile', bundle]);
|
||||
await promisify(execFile)(process.execPath, [
|
||||
'build',
|
||||
entry,
|
||||
'--target=node',
|
||||
'--outfile',
|
||||
bundle,
|
||||
]);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import {
|
||||
calculatePlanningInternal,
|
||||
type PlanningArtifacts,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { vi } from 'vitest';
|
||||
import { vi } from '../../testing/vitest-compat';
|
||||
import { calculatePlanningInternal } from './leave-planner.core';
|
||||
import { createPlanningCalculation } from './leave-planner.service';
|
||||
import type { PlanningRequest } from './leave-planner.service';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import fc from 'fast-check';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import { frenchPublicHolidaysForYear } from './declared-non-working-days';
|
||||
import {
|
||||
addDays,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { provideZonelessChangeDetection } from '@angular/core';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { vi } from 'vitest';
|
||||
import { vi } from '../../testing/vitest-compat';
|
||||
import { CalendarService } from './calendar-service';
|
||||
import { CalendarSettingsService } from './calendar-settings-service';
|
||||
import type {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import {
|
||||
compareObjectiveLists,
|
||||
compareObjectiveValues,
|
||||
@@ -174,7 +174,7 @@ describe('composable planning objectives', () => {
|
||||
|
||||
it.each([
|
||||
null,
|
||||
[],
|
||||
[[]],
|
||||
{ ...OPTIMIZATION_PRESETS.shortSequenceDeficit, conditions: ['internal', 'internal'] },
|
||||
{ ...OPTIMIZATION_PRESETS.longestWorkSequence, direction: 'up' },
|
||||
{ ...OPTIMIZATION_PRESETS.unplacedAtRisk, conditions: new Array(1) },
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { RuleService, RuleType } from './rule-service';
|
||||
import { vi } from 'vitest';
|
||||
import { vi } from '../../testing/vitest-compat';
|
||||
import { CalendarSettingsService } from './calendar-settings-service';
|
||||
import { PlanningCalculationService } from './planning-calculation.service';
|
||||
import { UserInputService } from './user-input-service';
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from './rule-service';
|
||||
import { SelectableDayType } from './calendar-service';
|
||||
import { OPTIMIZATION_PRESETS } from './planning-objectives';
|
||||
import { vi } from 'vitest';
|
||||
import { vi } from '../../testing/vitest-compat';
|
||||
|
||||
describe('RuleService', () => {
|
||||
let service: RuleService;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/// <reference types="vite/client" />
|
||||
import { Injectable, inject, Injector, computed } from '@angular/core';
|
||||
import { storedSignal } from '../shared/utils/stored-signal';
|
||||
import type { PlanOutcome, RemainingBalance } from './leave-planner.service';
|
||||
|
||||
@@ -2,7 +2,7 @@ import { TestBed } from '@angular/core/testing';
|
||||
import { provideZonelessChangeDetection } from '@angular/core';
|
||||
import { UserInputService } from './user-input-service';
|
||||
import { SelectableDayType, VacDay } from './calendar-service';
|
||||
import { vi } from 'vitest';
|
||||
import { vi } from '../../testing/vitest-compat';
|
||||
|
||||
describe('UserInputService', () => {
|
||||
const savedBalance = {
|
||||
|
||||
+110
-7
@@ -1,13 +1,116 @@
|
||||
import '@angular/compiler';
|
||||
import { vi } from 'vitest';
|
||||
import '@analogjs/vitest-angular/setup-snapshots';
|
||||
import '@analogjs/vitest-angular/setup-serializers';
|
||||
import { setupTestBed } from '@analogjs/vitest-angular/setup-testbed';
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { plugin } from 'bun';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import * as bt from 'bun:test';
|
||||
// shortcut: imports Analog's internal jitTransform by path; replace when Angular or Analog exports a JIT transform.
|
||||
import { jitTransform } from '../node_modules/@analogjs/vite-plugin-angular/src/lib/compiler/jit-transform.js';
|
||||
import { vi } from './testing/vitest-compat';
|
||||
|
||||
setupTestBed();
|
||||
// Inline templateUrl/styleUrl(s) before Bun transpiles: JIT has no resource loader.
|
||||
plugin({
|
||||
name: 'angular-jit',
|
||||
setup(build) {
|
||||
build.onLoad({ filter: /src\/app\/(?!.*\.spec\.ts$).*\.ts$/ }, ({ path }) => {
|
||||
let src = readFileSync(path, 'utf8');
|
||||
if (!/@(Component|Directive|Injectable|Pipe|NgModule)\(/.test(src)) {
|
||||
return { contents: src, loader: 'ts' };
|
||||
}
|
||||
const read = (p: string) =>
|
||||
JSON.stringify(readFileSync(resolve(dirname(path), p), 'utf8'));
|
||||
src = src.replace(/templateUrl:\s*'([^']+)'/, (_, p) => `template: ${read(p)}`);
|
||||
// Styles are not compiled; they do not matter to the tests.
|
||||
src = src.replace(/styleUrl:\s*'([^']+)'/, 'styles: []');
|
||||
src = src.replace(/styleUrls:\s*\[[^\]]*\]/, 'styles: []');
|
||||
// Signal input()/output()/viewChild() become decorators and @Component becomes ɵcompileComponent.
|
||||
return { contents: jitTransform(src, path).code, loader: 'ts' };
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { JSDOM } = await import('jsdom');
|
||||
const dom = new JSDOM('<!doctype html><html><body></body></html>', {
|
||||
url: 'http://localhost:3000/',
|
||||
pretendToBeVisual: true,
|
||||
});
|
||||
const win = dom.window as any;
|
||||
const keep = new Set([
|
||||
'console',
|
||||
'globalThis',
|
||||
'undefined',
|
||||
'Infinity',
|
||||
'NaN',
|
||||
'Object',
|
||||
'Function',
|
||||
'Array',
|
||||
'Error',
|
||||
'Promise',
|
||||
'Symbol',
|
||||
'Map',
|
||||
'Set',
|
||||
'JSON',
|
||||
'Math',
|
||||
'Date',
|
||||
'process',
|
||||
'window',
|
||||
'self',
|
||||
]);
|
||||
const force = new Set([
|
||||
'document',
|
||||
'navigator',
|
||||
'location',
|
||||
'localStorage',
|
||||
'sessionStorage',
|
||||
'history',
|
||||
'getComputedStyle',
|
||||
]);
|
||||
const domNames =
|
||||
/^(HTML|SVG|DOM|Mutation|Node|Element|Event|Text|Comment|Document|Range|Selection|CSS|Shadow|Custom|Keyboard|Mouse|Focus|Input|UI|Wheel|Pointer|Touch|Animation|File|Blob|FormData|Window|Storage|Headers|Abort)/;
|
||||
for (const k of Object.getOwnPropertyNames(win)) {
|
||||
if (keep.has(k) || (k in globalThis && !force.has(k) && !domNames.test(k))) continue;
|
||||
// Live accessors: spying on window.x also affects bare x (jsdom window !== globalThis here).
|
||||
Object.defineProperty(globalThis, k, {
|
||||
configurable: true,
|
||||
get: () => {
|
||||
const v = win[k];
|
||||
return typeof v === 'function' && /^[a-z]/.test(k) ? v.bind(win) : v;
|
||||
},
|
||||
set: (v) => {
|
||||
win[k] = v;
|
||||
},
|
||||
});
|
||||
}
|
||||
for (const k of ['window', 'self']) {
|
||||
Object.defineProperty(globalThis, k, { configurable: true, writable: true, value: win });
|
||||
}
|
||||
|
||||
Object.assign(globalThis, {
|
||||
vi,
|
||||
describe: bt.describe,
|
||||
it: bt.it,
|
||||
test: bt.test,
|
||||
expect: bt.expect,
|
||||
beforeEach: bt.beforeEach,
|
||||
afterEach: bt.afterEach,
|
||||
beforeAll: bt.beforeAll,
|
||||
afterAll: bt.afterAll,
|
||||
});
|
||||
|
||||
await import('@angular/compiler');
|
||||
const { getTestBed, ɵgetCleanupHook } = await import('@angular/core/testing');
|
||||
const { BrowserTestingModule, platformBrowserTesting } =
|
||||
await import('@angular/platform-browser/testing');
|
||||
const { provideZonelessChangeDetection } = await import('@angular/core');
|
||||
getTestBed().initTestEnvironment([BrowserTestingModule], platformBrowserTesting(), {
|
||||
teardown: { destroyAfterEach: true },
|
||||
});
|
||||
getTestBed().configureTestingModule({ providers: [provideZonelessChangeDetection()] });
|
||||
bt.beforeEach(ɵgetCleanupHook(false));
|
||||
bt.afterEach(ɵgetCleanupHook(true));
|
||||
|
||||
// Bun has a native Worker; the app must see none, as under jsdom.
|
||||
delete (globalThis as any).Worker;
|
||||
vi.stubGlobal('Worker', undefined);
|
||||
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: vi.fn().mockImplementation((query: string) => ({
|
||||
|
||||
Vendored
+18
@@ -0,0 +1,18 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars */
|
||||
import 'bun:test';
|
||||
|
||||
// Vitest APIs that the compat shim adds to bun:test.
|
||||
declare module 'bun:test' {
|
||||
interface Expect {
|
||||
// Vitest-style loose matcher arguments: Bun's strict ones reject readonly and widened values.
|
||||
(actual?: unknown): Matchers<any>;
|
||||
poll<T>(
|
||||
fn: () => T | Promise<T>,
|
||||
options?: { timeout?: number; interval?: number },
|
||||
): Matchers<Awaited<T>> & { not: Matchers<Awaited<T>> } & Record<string, unknown>;
|
||||
}
|
||||
interface Matchers<T> {
|
||||
toHaveBeenCalledOnce(): void;
|
||||
toHaveBeenCalledExactlyOnceWith(...args: unknown[]): void;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import * as bt from 'bun:test';
|
||||
|
||||
const stubbed = new Map<string, PropertyDescriptor | undefined>();
|
||||
const realUseFake = bt.vi.useFakeTimers.bind(bt.vi);
|
||||
const flush = () => new Promise<void>((r) => setImmediate(r));
|
||||
type AnyFn = (...args: any[]) => any;
|
||||
type VitestCompat = Omit<typeof bt.vi, 'spyOn' | 'useFakeTimers'> & {
|
||||
setSystemTime(now?: Date | number): VitestCompat;
|
||||
spyOn<T extends object, K extends keyof T>(
|
||||
obj: T,
|
||||
method: K,
|
||||
): bt.Mock<T[K] extends AnyFn ? T[K] : AnyFn>;
|
||||
useFakeTimers(options?: { toFake?: string[] }): VitestCompat;
|
||||
stubGlobal(name: string, value: unknown): VitestCompat;
|
||||
unstubAllGlobals(): VitestCompat;
|
||||
advanceTimersByTimeAsync(ms: number): Promise<void>;
|
||||
runAllTicks(): void;
|
||||
waitFor<T>(
|
||||
fn: () => T | Promise<T>,
|
||||
options?: { timeout?: number; interval?: number },
|
||||
): Promise<T>;
|
||||
};
|
||||
export const vi = Object.assign(bt.vi, {
|
||||
setSystemTime: (d?: Date | number) => bt.setSystemTime(d as any),
|
||||
// Bun ignores toFake and fakes every timer; Date-only is emulated by setSystemTime alone.
|
||||
useFakeTimers(o?: { toFake?: string[] }) {
|
||||
if (o?.toFake?.length === 1 && o.toFake[0] === 'Date') return vi;
|
||||
const now = Date.now();
|
||||
realUseFake();
|
||||
bt.setSystemTime(now);
|
||||
return vi;
|
||||
},
|
||||
stubGlobal(name: string, value: unknown) {
|
||||
if (!stubbed.has(name))
|
||||
stubbed.set(name, Object.getOwnPropertyDescriptor(globalThis, name));
|
||||
Object.defineProperty(globalThis, name, { value, configurable: true, writable: true });
|
||||
return vi;
|
||||
},
|
||||
unstubAllGlobals() {
|
||||
for (const [n, d] of stubbed) {
|
||||
if (d) Object.defineProperty(globalThis, n, d);
|
||||
else delete (globalThis as any)[n];
|
||||
}
|
||||
stubbed.clear();
|
||||
return vi;
|
||||
},
|
||||
async advanceTimersByTimeAsync(ms: number) {
|
||||
await flush();
|
||||
bt.vi.advanceTimersByTime(ms);
|
||||
await flush();
|
||||
},
|
||||
runAllTicks() {},
|
||||
async waitFor<T>(fn: () => T | Promise<T>, { timeout = 1000, interval = 50 } = {}): Promise<T> {
|
||||
const end = Date.now() + timeout;
|
||||
let last: unknown;
|
||||
for (;;) {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (e) {
|
||||
last = e;
|
||||
if (Date.now() > end) throw last;
|
||||
await new Promise((r) => setTimeout(r, interval));
|
||||
}
|
||||
}
|
||||
},
|
||||
}) as unknown as VitestCompat;
|
||||
export const { describe, it, test, expect, beforeEach, afterEach, beforeAll, afterAll } = bt;
|
||||
export type MockInstance<T extends AnyFn = AnyFn> = bt.Mock<T>;
|
||||
|
||||
// expect.poll: retry `expect(await fn())<.not>.matcher(...)` until it passes or times out.
|
||||
(bt.expect as any).poll = (fn: () => unknown, { timeout = 1000, interval = 50 } = {}) => {
|
||||
const chain = (negate: boolean): any =>
|
||||
new Proxy(
|
||||
{},
|
||||
{
|
||||
get: (_, m: string) =>
|
||||
m === 'not'
|
||||
? chain(!negate)
|
||||
: async (...args: unknown[]) => {
|
||||
const end = Date.now() + timeout;
|
||||
for (;;) {
|
||||
try {
|
||||
const e: any = bt.expect(await fn());
|
||||
return await (negate ? e.not : e)[m](...args);
|
||||
} catch (err) {
|
||||
if (Date.now() > end) throw err;
|
||||
await new Promise((r) => setTimeout(r, interval));
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
return chain(false);
|
||||
};
|
||||
|
||||
bt.expect.extend({
|
||||
toHaveBeenCalledExactlyOnceWith(fn: any, ...args: unknown[]) {
|
||||
const calls = fn.mock.calls as unknown[][];
|
||||
const pass = calls.length === 1 && this.equals(calls[0], args);
|
||||
return {
|
||||
pass,
|
||||
message: () =>
|
||||
`expected exactly one call with ${JSON.stringify(args)}, got ${JSON.stringify(calls)}`,
|
||||
};
|
||||
},
|
||||
});
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/spec",
|
||||
"types": ["vitest/globals"],
|
||||
"types": ["bun", "bun-types/test-globals", "node"],
|
||||
"target": "es2022"
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
/// <reference types="vitest" />
|
||||
|
||||
import angular from '@analogjs/vite-plugin-angular';
|
||||
|
||||
import { defineConfig } from 'vite';
|
||||
import { coverageConfigDefaults } from 'vitest/config';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(({ mode }) => {
|
||||
return {
|
||||
plugins: [angular()],
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
setupFiles: ['src/test-setup.ts'],
|
||||
include: ['**/*.spec.ts'],
|
||||
reporters: ['default'],
|
||||
pool: 'forks',
|
||||
coverage: {
|
||||
provider: 'istanbul',
|
||||
reporter: ['text', 'html', 'cobertura'],
|
||||
exclude: [...coverageConfigDefaults.exclude, '**/frozen-engine/**'],
|
||||
},
|
||||
},
|
||||
define: {
|
||||
'import.meta.vitest': mode !== 'production',
|
||||
},
|
||||
};
|
||||
});
|
||||
Reference in new issue
Block a user