@@ -39,10 +39,22 @@ jobs:
|
||||
- name: Assert no banned vi.mock patterns
|
||||
shell: bash
|
||||
run: |
|
||||
# Literal pdfjs-dist (libLoader pattern — ADR 012)
|
||||
if grep -rF "vi.mock('pdfjs-dist'" frontend/src/; then
|
||||
echo "FAIL: banned vi.mock('pdfjs-dist') pattern found — see ADR 012. Use the libLoader prop injection pattern instead."
|
||||
exit 1
|
||||
fi
|
||||
# Async factory with dynamic import in body (named mechanism — ADR 012 / #553).
|
||||
# Multiline PCRE matches `vi.mock(<arg>, async ... { ... await import(...) ... })`
|
||||
# across line breaks. __meta__ is excluded because it contains fixture strings
|
||||
# demonstrating the very pattern this check is meant to forbid.
|
||||
if grep -rPzln 'vi\.mock\([^)]+,\s*async[^{]*\{[\s\S]*?await\s+import\s*\(' \
|
||||
--include='*.spec.ts' --include='*.test.ts' \
|
||||
--exclude-dir='__meta__' \
|
||||
frontend/src/; then
|
||||
echo "FAIL: banned async vi.mock factory with dynamic import in body — see ADR 012 / #553. Use a synchronous factory + vi.hoisted instead."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Run unit and component tests with coverage
|
||||
shell: bash
|
||||
|
||||
64
.gitea/workflows/coverage-flake-probe.yml
Normal file
64
.gitea/workflows/coverage-flake-probe.yml
Normal file
@@ -0,0 +1,64 @@
|
||||
name: Coverage Flake Probe
|
||||
|
||||
# Manually-triggered probe for the birpc teardown race documented in ADR 012
|
||||
# / #553. Runs the full coverage suite 20× in parallel against a single SHA
|
||||
# and asserts zero `[birpc] rpc is closed` lines across every cell. Verifies
|
||||
# the acceptance criterion that the race no longer surfaces under coverage.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
coverage-flake-probe:
|
||||
name: Coverage flake probe (run ${{ matrix.run }})
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.58.2-noble
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
run: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Cache node_modules
|
||||
id: node-modules-cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: frontend/node_modules
|
||||
key: node-modules-${{ hashFiles('frontend/package-lock.json') }}
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.node-modules-cache.outputs.cache-hit != 'true'
|
||||
run: npm ci
|
||||
working-directory: frontend
|
||||
|
||||
- name: Compile Paraglide i18n
|
||||
run: npx @inlang/paraglide-js compile --project ./project.inlang --outdir ./src/lib/paraglide
|
||||
working-directory: frontend
|
||||
|
||||
- name: Run unit and component tests with coverage
|
||||
shell: bash
|
||||
run: |
|
||||
set -eo pipefail
|
||||
npm run test:coverage 2>&1 | tee /tmp/coverage-test-${{ github.run_id }}-${{ matrix.run }}.log
|
||||
working-directory: frontend
|
||||
env:
|
||||
TZ: Europe/Berlin
|
||||
|
||||
- name: Assert no birpc teardown race
|
||||
shell: bash
|
||||
if: always()
|
||||
run: |
|
||||
if grep -qF "[birpc] rpc is closed" /tmp/coverage-test-${{ github.run_id }}-${{ matrix.run }}.log 2>/dev/null; then
|
||||
echo "FAIL: [birpc] rpc is closed teardown race detected in run ${{ matrix.run }}"
|
||||
grep -F "[birpc] rpc is closed" /tmp/coverage-test-${{ github.run_id }}-${{ matrix.run }}.log
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Upload coverage log on failure
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage-log-run-${{ matrix.run }}
|
||||
path: /tmp/coverage-test-${{ github.run_id }}-${{ matrix.run }}.log
|
||||
@@ -1,8 +1,8 @@
|
||||
# ADR 012 — Browser-Mode Test Mocking Strategy
|
||||
|
||||
**Status:** Accepted
|
||||
**Date:** 2026-05-11
|
||||
**Issue:** [#535 — Unit & Component Tests job exits 1 from vitest-browser teardown race](https://git.raddatz.cloud/marcel/familienarchiv/issues/535)
|
||||
**Date:** 2026-05-11 (revised 2026-05-12)
|
||||
**Issues:** [#535 — original incident](https://git.raddatz.cloud/marcel/familienarchiv/issues/535) · [#553 — revision](https://git.raddatz.cloud/marcel/familienarchiv/issues/553)
|
||||
|
||||
---
|
||||
|
||||
@@ -65,22 +65,57 @@ For components that fetch data or call services, the `*.test-host.svelte` patter
|
||||
|
||||
---
|
||||
|
||||
## Residual exceptions
|
||||
## Binding invariant: factory bodies must be synchronous (#553)
|
||||
|
||||
The following `vi.mock(module, factory)` calls in browser specs are **acceptable** because the mocked modules are loaded statically at spec module-eval time and cannot produce a teardown race:
|
||||
The original revision of this ADR allowed `vi.mock(virtualModule, factory)` for SvelteKit/Vite virtual modules on the argument that their consumer imports were resolved at static-import time. **That reasoning is wrong.** What matters is what the **factory body** does, not where the mocked module is consumed.
|
||||
|
||||
| Module | Why it stays as vi.mock |
|
||||
|--------|------------------------|
|
||||
| `$app/navigation` | SvelteKit virtual module — no DI seam |
|
||||
| `$app/forms` | SvelteKit virtual module — no DI seam |
|
||||
| `$app/state` | SvelteKit virtual module — no DI seam |
|
||||
| `$app/stores` | SvelteKit virtual module — no DI seam |
|
||||
| `$env/static/public` | Vite env virtual module — no DI seam |
|
||||
`EnrichmentBlock.svelte.spec.ts` (issue #553) was statically imported and still produced the race: its `vi.mock('$app/stores', async () => { const mod = await import(...); return mod; })` factory performed a dynamic import in its body, and that body was invoked asynchronously when Chromium fetched the manually-mocked module — sometimes after the worker's birpc channel had already closed.
|
||||
|
||||
These modules are resolved at static import time (before any test runs). Their `vi.mock` factories are served by birpc synchronously during module graph resolution, not after worker teardown.
|
||||
**Therefore: under `**/*.svelte.{test,spec}.ts`, every `vi.mock` factory body must be synchronous. No `await`, no `import(...)`.**
|
||||
|
||||
If a factory needs to share state with the spec (a mutable ref, a `vi.fn`, a writable store), use `vi.hoisted()` to lift the reference above `vi.mock`'s implicit hoist:
|
||||
|
||||
```ts
|
||||
const { mockNavigating } = vi.hoisted(() => ({
|
||||
mockNavigating: { type: null as string | null }
|
||||
}));
|
||||
|
||||
vi.mock('$app/state', () => ({
|
||||
get navigating() {
|
||||
return mockNavigating;
|
||||
}
|
||||
}));
|
||||
```
|
||||
|
||||
The getter defers the read until consumption time; `vi.hoisted` guarantees the reference is initialised before the (also hoisted) `vi.mock` factory runs. See `DropZone.svelte.spec.ts:9`, `NotificationBell.svelte.spec.ts:6-10`, and `EnrichmentBlock.svelte.spec.ts` for canonical examples.
|
||||
|
||||
### Architectural follow-on: prefer `$app/state` over `$app/stores`
|
||||
|
||||
`$app/stores` is the deprecated subscription-based store API; `$app/state` is the modern reactive proxy. New components should import from `$app/state`. As part of #553 we migrated `EnrichmentBlock.svelte` from `$app/stores.navigating` to `$app/state.navigating` with `!!navigating.type` — matching the pattern already established in `routes/aktivitaeten/+page.svelte:117` and `routes/documents/+page.svelte:261`. Migration eliminated the *need* to mock a store at all in that spec.
|
||||
|
||||
**Pattern note:** When an overlay or dropdown triggers a navigation action, use `<button type="button">` with an `onclick` handler that calls `goto(path)` — do **not** use `<a href="…">` with `e.preventDefault()`. SvelteKit registers its link interceptor as a capture-phase `document` listener, so it fires before the component's bubble-phase `onclick`. By the time `e.preventDefault()` runs the router has already initiated navigation, which tears down the vitest-browser Playwright orchestrator iframe. A `<button>` carries no `href`, so the capture-phase interceptor never fires. See `NotificationDropdown.svelte` for the canonical example.
|
||||
|
||||
**Pattern note (#553):** Browser-mode tests run with `data-sveltekit-preload-data="off"` (set in `src/test-setup.ts` via the client project's `setupFiles`). Hover-prefetch otherwise fires real fetch requests for route loader chunks; those requests go through the same Playwright route handler that serves mocked modules. An in-flight prefetch landing after iframe teardown can hit the handler with a closed birpc channel, raising an unhandled rejection.
|
||||
|
||||
---
|
||||
|
||||
## Binding invariant: one canonical ID per mocked module (#553 — duplicate-id hazard)
|
||||
|
||||
The sync-factory invariant above closes one named trigger of the `[birpc] rpc is closed` race. Investigation of a follow-up flake revealed a second, independent trigger: **the same resolved module URL mocked under two distinct ID strings** across or within spec files.
|
||||
|
||||
`@vitest/browser-playwright` registers a Playwright `page.context().route(...)` handler per `vi.mock` call. The predicate matches on the module's resolved URL. When two `vi.mock` calls reference the same module under different IDs — for example `'$lib/foo.svelte'` and `'$lib/foo.svelte.js'` (both resolve to the same Svelte rune-module URL) — the registry stores both predicates but the cleanup map only tracks the latest. The orphan route survives session teardown. When the next session loads the same module, the orphan fires, calls `await module.resolve()` against a closed birpc channel, and crashes the run.
|
||||
|
||||
This is fixed upstream in [vitest PR #10267](https://github.com/vitest-dev/vitest/pull/10267) (issue [#9957](https://github.com/vitest-dev/vitest/issues/9957)). Until that fix reaches a published `@vitest/browser-playwright` release, we close the gap from two sides:
|
||||
|
||||
**The rule.** Every mocked module must be referenced under exactly one ID string across the entire client test suite. Pick the spelling production code uses. For Svelte 5 rune modules (`*.svelte.ts`), the canonical form is the no-extension import (`'$lib/foo.svelte'`) — matches the source file basename and matches Svelte 5 convention. Never mix `.svelte.js` and `.svelte` for the same module across specs.
|
||||
|
||||
**Enforcement layers** (added in #553's second cycle, extending the four-layer chain above):
|
||||
|
||||
5. **In-suite meta-test** at `frontend/src/__meta__/no-duplicate-mock-ids.test.ts` globs `src/**/*.svelte.{test,spec}.ts`, extracts every `vi.mock` first-arg string, canonicalises by stripping a trailing `.js`/`.ts` after `.svelte`, and fails if any canonical ID is referenced under two or more distinct spellings. Same shape as `no-async-mock-factories.test.ts`.
|
||||
6. **`patch-package` backport** of PR #10267 at `frontend/patches/@vitest+browser-playwright+4.1.0.patch`. Applied automatically by the `postinstall` hook. Closes the race at the route-handler level — even if a contributor reintroduces a duplicate-ID, the patched `register` handler unroutes the existing predicate before installing the new one.
|
||||
|
||||
**When to remove the patch.** Once `@vitest/browser-playwright` ships a release containing PR #10267, delete `patches/@vitest+browser-playwright+4.1.0.patch`. Bump the dependency to the version containing the fix. The in-suite meta-test stays — it's a cheap permanent guard against the contributor-facing pattern, independent of upstream library version.
|
||||
|
||||
---
|
||||
|
||||
## Consequences
|
||||
@@ -88,5 +123,12 @@ These modules are resolved at static import time (before any test runs). Their `
|
||||
- New browser-mode specs that need to stub an external library **must not** use `vi.mock(externalLib, factory)`. Add a loader/factory parameter to the underlying hook or service instead.
|
||||
- The CI `unit-tests` job includes a permanent grep guard that fails the build if `rpc is closed` appears in any coverage run log. This catches regressions before they reach the acceptance criterion.
|
||||
- Acceptance criterion for #535: 60 consecutive green `workflow_dispatch` CI runs against `main` after the fix is merged, with zero `rpc is closed` lines in any log.
|
||||
- **Enforcement:** An ESLint `no-restricted-syntax` rule in `eslint.config.js` (scoped to `**/*.spec.ts` and `**/*.test.ts`) flags any `vi.mock('pdfjs-dist', ...)` call with a message referencing this ADR. This turns a ~2-minute CI wait into an immediate lint error on save. The CI birpc grep guard remains as belt-and-suspenders for the coverage run. A CI static grep step (added in issue #546) also catches the pattern before the test suite launches.
|
||||
- **Enforcement (six layers, defence in depth):**
|
||||
1. **ESLint `no-restricted-syntax`** in `eslint.config.js` (scoped to `**/*.{spec,test}.ts`) flags two patterns: (a) the literal `vi.mock('pdfjs-dist', ...)` — enforces the libLoader pattern — and (b) any `vi.mock(..., async () => { ... await import(...) ... })` — enforces the synchronous-factory invariant. Both messages point at this ADR. Failure surfaces at save time.
|
||||
2. **CI grep guard** in `.gitea/workflows/ci.yml` runs before the test suite launches. Mirrors the ESLint patterns with `grep -Pzn`. ~10s round-trip.
|
||||
3. **In-suite meta-test** at `frontend/src/__meta__/no-async-mock-factories.test.ts` globs `src/**/*.svelte.{test,spec}.ts` and asserts none match the banned pattern. Catches at every vitest invocation — the layer hardest to disable.
|
||||
4. **CI birpc assert** runs after the coverage step and fails the build if `[birpc] rpc is closed` appears in any log line. Catches the symptom even if all the upstream layers were bypassed.
|
||||
5. **In-suite duplicate-ID meta-test** at `frontend/src/__meta__/no-duplicate-mock-ids.test.ts` enforces the one-canonical-ID-per-module rule from the duplicate-id-hazard section above.
|
||||
6. **`patch-package` backport** at `frontend/patches/@vitest+browser-playwright+4.1.0.patch` closes the upstream race itself, applied via `postinstall`. To be removed when `@vitest/browser-playwright` releases [vitest PR #10267](https://github.com/vitest-dev/vitest/pull/10267).
|
||||
- **Acceptance verification:** `coverage-flake-probe.yml` is a `workflow_dispatch`-triggered matrix workflow that runs the coverage suite 20× in parallel against a single SHA and asserts zero birpc lines. One fire, parallel cost, deterministic signal — replaces accumulating 20 sequential push events.
|
||||
- **When to revisit the LibLoader home:** If three or more components adopt this pattern, consider extracting a shared `$lib/types/lib-loader.ts` or a generic `DynamicImportLoader<T>` type to avoid parallel type definitions across modules.
|
||||
|
||||
@@ -82,6 +82,17 @@ export default defineConfig(
|
||||
"CallExpression[callee.object.name='vi'][callee.property.name='mock'] > Literal[value=/^pdfjs-dist/]",
|
||||
message:
|
||||
"Banned: vi.mock('pdfjs-dist', factory) causes a birpc teardown race in browser-mode specs — see ADR 012. Use the libLoader prop injection pattern instead."
|
||||
},
|
||||
{
|
||||
// ADR 012 / #553. The named mechanism: an async vi.mock factory whose
|
||||
// body performs `await import(...)` produces a late birpc roundtrip
|
||||
// during worker teardown. The factory body must be synchronous; if
|
||||
// you need to share state between the spec and the mock, use
|
||||
// `vi.hoisted` (see DropZone.svelte.spec.ts).
|
||||
selector:
|
||||
"CallExpression[callee.object.name='vi'][callee.property.name='mock'][arguments.1.type='ArrowFunctionExpression'][arguments.1.async=true]:has(AwaitExpression > ImportExpression)",
|
||||
message:
|
||||
'Banned: vi.mock(..., async () => { await import(...) }) causes a birpc teardown race in browser-mode specs — see ADR 012. Use a synchronous factory + vi.hoisted instead.'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
464
frontend/package-lock.json
generated
464
frontend/package-lock.json
generated
@@ -7,6 +7,7 @@
|
||||
"": {
|
||||
"name": "frontend",
|
||||
"version": "0.0.1",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@tiptap/core": "3.22.5",
|
||||
"@tiptap/extension-mention": "3.22.5",
|
||||
@@ -40,6 +41,7 @@
|
||||
"eslint-plugin-svelte": "^3.13.0",
|
||||
"globals": "^16.5.0",
|
||||
"openapi-typescript": "^7.8.0",
|
||||
"patch-package": "^8.0.0",
|
||||
"playwright": "^1.56.1",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier-plugin-svelte": "^3.4.0",
|
||||
@@ -3939,6 +3941,13 @@
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@yarnpkg/lockfile": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz",
|
||||
"integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.16.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
|
||||
@@ -4185,6 +4194,56 @@
|
||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz",
|
||||
"integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"get-intrinsic": "^1.3.0",
|
||||
"set-function-length": "^1.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bound": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
||||
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"get-intrinsic": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/callsites": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||
@@ -4266,6 +4325,22 @@
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/ci-info": {
|
||||
"version": "3.9.0",
|
||||
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
|
||||
"integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/sibiraj-s"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
@@ -4478,6 +4553,24 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/define-data-property": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
|
||||
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-define-property": "^1.0.0",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
@@ -4513,6 +4606,21 @@
|
||||
"@types/trusted-types": "^2.0.7"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.353",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.353.tgz",
|
||||
@@ -4546,6 +4654,26 @@
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-module-lexer": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz",
|
||||
@@ -4553,6 +4681,19 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz",
|
||||
@@ -5084,6 +5225,16 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/find-yarn-workspace-root": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz",
|
||||
"integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"micromatch": "^4.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/flat-cache": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
|
||||
@@ -5105,6 +5256,21 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/fs-extra": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
|
||||
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.0",
|
||||
"jsonfile": "^6.0.1",
|
||||
"universalify": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
@@ -5140,6 +5306,45 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/get-tsconfig": {
|
||||
"version": "4.14.0",
|
||||
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz",
|
||||
@@ -5179,6 +5384,19 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
@@ -5218,6 +5436,32 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/has-property-descriptors": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
|
||||
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-define-property": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
@@ -5350,6 +5594,22 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-docker": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
|
||||
"integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"is-docker": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
@@ -5406,6 +5666,26 @@
|
||||
"@types/estree": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/is-wsl": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
|
||||
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-docker": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/isarray": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
|
||||
"integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
@@ -5579,6 +5859,26 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-stable-stringify": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz",
|
||||
"integrity": "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.8",
|
||||
"call-bound": "^1.0.4",
|
||||
"isarray": "^2.0.5",
|
||||
"jsonify": "^0.0.1",
|
||||
"object-keys": "^1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/json-stable-stringify-without-jsonify": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
|
||||
@@ -5599,6 +5899,29 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonfile": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz",
|
||||
"integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"universalify": "^2.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonify": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz",
|
||||
"integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==",
|
||||
"dev": true,
|
||||
"license": "Public Domain",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
@@ -5609,6 +5932,16 @@
|
||||
"json-buffer": "3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/klaw-sync": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz",
|
||||
"integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.1.11"
|
||||
}
|
||||
},
|
||||
"node_modules/kleur": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
|
||||
@@ -6004,6 +6337,16 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/mdn-data": {
|
||||
"version": "2.27.1",
|
||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz",
|
||||
@@ -6160,6 +6503,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/object-keys": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
|
||||
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/obug": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
|
||||
@@ -6171,6 +6524,23 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/open": {
|
||||
"version": "7.4.2",
|
||||
"resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz",
|
||||
"integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-docker": "^2.0.0",
|
||||
"is-wsl": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/openapi-fetch": {
|
||||
"version": "0.13.8",
|
||||
"resolved": "https://registry.npmjs.org/openapi-fetch/-/openapi-fetch-0.13.8.tgz",
|
||||
@@ -6319,6 +6689,36 @@
|
||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/patch-package": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.1.tgz",
|
||||
"integrity": "sha512-VsKRIA8f5uqHQ7NGhwIna6Bx6D9s/1iXlA1hthBVBEbkq+t4kXD0HHt+rJhf/Z+Ci0F/HCB2hvn0qLdLG+Qxlw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@yarnpkg/lockfile": "^1.1.0",
|
||||
"chalk": "^4.1.2",
|
||||
"ci-info": "^3.7.0",
|
||||
"cross-spawn": "^7.0.3",
|
||||
"find-yarn-workspace-root": "^2.0.0",
|
||||
"fs-extra": "^10.0.0",
|
||||
"json-stable-stringify": "^1.0.2",
|
||||
"klaw-sync": "^6.0.0",
|
||||
"minimist": "^1.2.6",
|
||||
"open": "^7.4.2",
|
||||
"semver": "^7.5.3",
|
||||
"slash": "^2.0.0",
|
||||
"tmp": "^0.2.4",
|
||||
"yaml": "^2.2.2"
|
||||
},
|
||||
"bin": {
|
||||
"patch-package": "index.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14",
|
||||
"npm": ">5"
|
||||
}
|
||||
},
|
||||
"node_modules/path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
@@ -6989,6 +7389,24 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/set-function-length": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
||||
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"define-data-property": "^1.1.4",
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-intrinsic": "^1.2.4",
|
||||
"gopd": "^1.0.1",
|
||||
"has-property-descriptors": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
@@ -7034,6 +7452,16 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/slash": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",
|
||||
"integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
@@ -7324,6 +7752,16 @@
|
||||
"integrity": "sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tmp": {
|
||||
"version": "0.2.5",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
|
||||
"integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.14"
|
||||
}
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
@@ -7486,6 +7924,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/universalify": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
||||
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/unplugin": {
|
||||
"version": "2.3.11",
|
||||
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz",
|
||||
@@ -8001,6 +8449,22 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",
|
||||
"integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/eemeli"
|
||||
}
|
||||
},
|
||||
"node_modules/yaml-ast-parser": {
|
||||
"version": "0.0.43",
|
||||
"resolved": "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz",
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"prepare": "svelte-kit sync || true && git -C .. config core.hooksPath .husky 2>/dev/null || true",
|
||||
"postinstall": "patch-package",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"format": "prettier --write .",
|
||||
@@ -54,6 +55,7 @@
|
||||
"eslint-plugin-svelte": "^3.13.0",
|
||||
"globals": "^16.5.0",
|
||||
"openapi-typescript": "^7.8.0",
|
||||
"patch-package": "^8.0.0",
|
||||
"playwright": "^1.56.1",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier-plugin-svelte": "^3.4.0",
|
||||
|
||||
62
frontend/patches/@vitest+browser-playwright+4.1.0.patch
Normal file
62
frontend/patches/@vitest+browser-playwright+4.1.0.patch
Normal file
@@ -0,0 +1,62 @@
|
||||
diff --git a/node_modules/@vitest/browser-playwright/dist/index.js b/node_modules/@vitest/browser-playwright/dist/index.js
|
||||
index 5d0d37b..821d7b4 100644
|
||||
--- a/node_modules/@vitest/browser-playwright/dist/index.js
|
||||
+++ b/node_modules/@vitest/browser-playwright/dist/index.js
|
||||
@@ -935,7 +935,7 @@ class PlaywrightBrowserProvider {
|
||||
createMocker() {
|
||||
const idPreficates = new Map();
|
||||
const sessionIds = new Map();
|
||||
- function createPredicate(sessionId, url) {
|
||||
+ function createPredicate(url) {
|
||||
const moduleUrl = new URL(url, "http://localhost");
|
||||
const predicate = (url) => {
|
||||
if (url.searchParams.has("_vitest_original")) {
|
||||
@@ -960,11 +960,7 @@ class PlaywrightBrowserProvider {
|
||||
}
|
||||
return true;
|
||||
};
|
||||
- const ids = sessionIds.get(sessionId) || [];
|
||||
- ids.push(moduleUrl.href);
|
||||
- sessionIds.set(sessionId, ids);
|
||||
- idPreficates.set(predicateKey(sessionId, moduleUrl.href), predicate);
|
||||
- return predicate;
|
||||
+ return { url: moduleUrl.href, predicate };
|
||||
}
|
||||
function predicateKey(sessionId, url) {
|
||||
return `${sessionId}:${url}`;
|
||||
@@ -972,7 +968,23 @@ class PlaywrightBrowserProvider {
|
||||
return {
|
||||
register: async (sessionId, module) => {
|
||||
const page = this.getPage(sessionId);
|
||||
- await page.context().route(createPredicate(sessionId, module.url), async (route) => {
|
||||
+ const { url: moduleUrl, predicate } = createPredicate(module.url);
|
||||
+ const key = predicateKey(sessionId, moduleUrl);
|
||||
+ // Backport of vitest PR #10267: if a route handler is already
|
||||
+ // registered for this resolved module URL in this session,
|
||||
+ // unroute it before installing the new one. Without this guard,
|
||||
+ // duplicate-id mocks (e.g. '$lib/foo.svelte' + '$lib/foo.svelte.js')
|
||||
+ // leak an orphan route whose handler crashes after the next
|
||||
+ // session's birpc channel closes.
|
||||
+ const existingPredicate = idPreficates.get(key);
|
||||
+ if (existingPredicate) {
|
||||
+ await page.context().unroute(existingPredicate);
|
||||
+ }
|
||||
+ const ids = sessionIds.get(sessionId) ?? new Set();
|
||||
+ ids.add(moduleUrl);
|
||||
+ sessionIds.set(sessionId, ids);
|
||||
+ idPreficates.set(key, predicate);
|
||||
+ await page.context().route(predicate, async (route) => {
|
||||
if (module.type === "manual") {
|
||||
const exports$1 = Object.keys(await module.resolve());
|
||||
const body = createManualModuleSource(module.url, exports$1);
|
||||
@@ -1033,8 +1045,8 @@ class PlaywrightBrowserProvider {
|
||||
},
|
||||
clear: async (sessionId) => {
|
||||
const page = this.getPage(sessionId);
|
||||
- const ids = sessionIds.get(sessionId) || [];
|
||||
- const promises = ids.map((id) => {
|
||||
+ const ids = sessionIds.get(sessionId) ?? new Set();
|
||||
+ const promises = [...ids].map((id) => {
|
||||
const key = predicateKey(sessionId, id);
|
||||
const predicate = idPreficates.get(key);
|
||||
if (predicate) {
|
||||
@@ -0,0 +1,20 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
// Browser-mode tests must run with SvelteKit's hover-prefetch disabled.
|
||||
// Hover-prefetch fires real `fetch` requests for the target route's loader
|
||||
// chunks; those go through the same Playwright route handler that serves
|
||||
// mocked modules. Even after `cleanup()` tears down the iframe, an in-flight
|
||||
// prefetch can still hit the handler — and if the worker's birpc channel has
|
||||
// closed by then, the handler raises an unhandled rejection. ADR-012 / #553.
|
||||
//
|
||||
// This test enforces that the test-setup file ran and switched preload-data
|
||||
// off on `document.body` before any spec started rendering.
|
||||
describe('browser test setup', () => {
|
||||
it('disables SvelteKit loader-data prefetch on document.body', () => {
|
||||
expect(document.body.dataset.sveltekitPreloadData).toBe('off');
|
||||
});
|
||||
|
||||
it('disables SvelteKit route-code prefetch on document.body', () => {
|
||||
expect(document.body.dataset.sveltekitPreloadCode).toBe('off');
|
||||
});
|
||||
});
|
||||
82
frontend/src/__meta__/no-async-mock-factories.test.ts
Normal file
82
frontend/src/__meta__/no-async-mock-factories.test.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { readdirSync, readFileSync } from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
// Belt-and-braces detector for the birpc teardown race named in ADR-012 / #553.
|
||||
// ESLint catches the pattern at save time, CI grep catches it before the test
|
||||
// suite launches, and this in-suite test catches it at every vitest invocation —
|
||||
// the layer hardest to disable or scope around.
|
||||
//
|
||||
// We scan source text rather than parsing AST: fast, no parser dependency,
|
||||
// good enough for the named anti-pattern. The pattern matches
|
||||
// `vi.mock(<arg>, async ... { ... await import(...) ... })`.
|
||||
|
||||
const ASYNC_MOCK_WITH_DYNAMIC_IMPORT = /vi\.mock\([^)]*,\s*async[^{]*\{[\s\S]*?await\s+import\s*\(/;
|
||||
|
||||
export function hasAsyncMockFactoryWithDynamicImport(source: string): boolean {
|
||||
return ASYNC_MOCK_WITH_DYNAMIC_IMPORT.test(source);
|
||||
}
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const SRC_ROOT = path.resolve(__dirname, '..');
|
||||
|
||||
function findBrowserSpecs(): string[] {
|
||||
const entries = readdirSync(SRC_ROOT, { recursive: true, withFileTypes: true });
|
||||
return entries
|
||||
.filter(
|
||||
(e) =>
|
||||
e.isFile() && (e.name.endsWith('.svelte.test.ts') || e.name.endsWith('.svelte.spec.ts'))
|
||||
)
|
||||
.map((e) => path.join(e.parentPath ?? (e as { path: string }).path, e.name));
|
||||
}
|
||||
|
||||
describe('scan: hasAsyncMockFactoryWithDynamicImport', () => {
|
||||
it('flags async vi.mock factory with await import in body', () => {
|
||||
const fixture = `vi.mock('$app/stores', async () => {
|
||||
const mod = await import('./__mocks__/navigatingStore');
|
||||
return { navigating: mod.navigatingStore };
|
||||
});`;
|
||||
expect(hasAsyncMockFactoryWithDynamicImport(fixture)).toBe(true);
|
||||
});
|
||||
|
||||
it('does not flag sync vi.mock factory', () => {
|
||||
const fixture = `vi.mock('$app/state', () => ({ navigating: { type: null } }));`;
|
||||
expect(hasAsyncMockFactoryWithDynamicImport(fixture)).toBe(false);
|
||||
});
|
||||
|
||||
it('does not flag async vi.mock factory without dynamic import', () => {
|
||||
const fixture = `vi.mock('foo', async () => {
|
||||
const x = await Promise.resolve(42);
|
||||
return { bar: x };
|
||||
});`;
|
||||
expect(hasAsyncMockFactoryWithDynamicImport(fixture)).toBe(false);
|
||||
});
|
||||
|
||||
it('does not flag dynamic import outside any vi.mock', () => {
|
||||
const fixture = `async function load() {
|
||||
const mod = await import('./something');
|
||||
return mod.default;
|
||||
}`;
|
||||
expect(hasAsyncMockFactoryWithDynamicImport(fixture)).toBe(false);
|
||||
});
|
||||
|
||||
it('flags async factory written as async function expression', () => {
|
||||
const fixture = `vi.mock('foo', async function () {
|
||||
const mod = await import('./bar');
|
||||
return mod;
|
||||
});`;
|
||||
expect(hasAsyncMockFactoryWithDynamicImport(fixture)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('browser specs: no async vi.mock factory contains await import', () => {
|
||||
it('every src/**/*.svelte.{test,spec}.ts file is clean', () => {
|
||||
const specFiles = findBrowserSpecs();
|
||||
expect(specFiles.length).toBeGreaterThan(0);
|
||||
const offenders = specFiles.filter((file) =>
|
||||
hasAsyncMockFactoryWithDynamicImport(readFileSync(file, 'utf-8'))
|
||||
);
|
||||
expect(offenders).toEqual([]);
|
||||
});
|
||||
});
|
||||
130
frontend/src/__meta__/no-duplicate-mock-ids.test.ts
Normal file
130
frontend/src/__meta__/no-duplicate-mock-ids.test.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { readdirSync, readFileSync } from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
// Belt-and-braces detector for the duplicate-id birpc race named in
|
||||
// ADR-012 / #553. When the same resolved module URL is mocked via two
|
||||
// distinct vi.mock id strings (e.g. '$lib/foo.svelte' and
|
||||
// '$lib/foo.svelte.js'), @vitest/browser-playwright registers two
|
||||
// Playwright routes against one cleanup slot — the orphan survives, fires
|
||||
// after the next session's birpc closes, and crashes the run with
|
||||
// "[birpc] rpc is closed, cannot call resolveManualMock".
|
||||
//
|
||||
// Fixed upstream in vitest PR #10267; until that fix reaches a published
|
||||
// release, normalisation in user-land is the practical guard. This test
|
||||
// catches the pattern at every vitest invocation — the layer hardest to
|
||||
// disable or scope around.
|
||||
|
||||
const VI_MOCK_ID = /vi\.mock\(\s*['"]([^'"]+)['"]/g;
|
||||
|
||||
function extractMockIds(source: string): string[] {
|
||||
const ids: string[] = [];
|
||||
for (const match of source.matchAll(VI_MOCK_ID)) {
|
||||
ids.push(match[1]);
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
function canonicalise(id: string): string {
|
||||
if (id.endsWith('.svelte.js')) return id.slice(0, -3);
|
||||
if (id.endsWith('.svelte.ts')) return id.slice(0, -3);
|
||||
return id;
|
||||
}
|
||||
|
||||
export function findDuplicateMockIds(
|
||||
specSources: Record<string, string>
|
||||
): Map<string, Set<string>> {
|
||||
const byCanonical = new Map<string, Set<string>>();
|
||||
for (const source of Object.values(specSources)) {
|
||||
for (const raw of extractMockIds(source)) {
|
||||
const canonical = canonicalise(raw);
|
||||
const existing = byCanonical.get(canonical) ?? new Set<string>();
|
||||
existing.add(raw);
|
||||
byCanonical.set(canonical, existing);
|
||||
}
|
||||
}
|
||||
const duplicates = new Map<string, Set<string>>();
|
||||
for (const [canonical, raws] of byCanonical) {
|
||||
if (raws.size >= 2) duplicates.set(canonical, raws);
|
||||
}
|
||||
return duplicates;
|
||||
}
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const SRC_ROOT = path.resolve(__dirname, '..');
|
||||
|
||||
function findBrowserSpecs(): string[] {
|
||||
const entries = readdirSync(SRC_ROOT, { recursive: true, withFileTypes: true });
|
||||
return entries
|
||||
.filter(
|
||||
(e) =>
|
||||
e.isFile() && (e.name.endsWith('.svelte.test.ts') || e.name.endsWith('.svelte.spec.ts'))
|
||||
)
|
||||
.map((e) => path.join(e.parentPath ?? (e as { path: string }).path, e.name));
|
||||
}
|
||||
|
||||
describe('scan: findDuplicateMockIds', () => {
|
||||
it('flags two specs mocking the same module under .svelte and .svelte.js', () => {
|
||||
const dup = findDuplicateMockIds({
|
||||
'a.spec.ts': `vi.mock('$lib/foo.svelte', () => ({}));`,
|
||||
'b.spec.ts': `vi.mock('$lib/foo.svelte.js', () => ({}));`
|
||||
});
|
||||
expect(dup.get('$lib/foo.svelte')).toEqual(new Set(['$lib/foo.svelte', '$lib/foo.svelte.js']));
|
||||
});
|
||||
|
||||
it('does not flag two specs both using $lib/foo.svelte', () => {
|
||||
const dup = findDuplicateMockIds({
|
||||
'a.spec.ts': `vi.mock('$lib/foo.svelte', () => ({}));`,
|
||||
'b.spec.ts': `vi.mock('$lib/foo.svelte', () => ({}));`
|
||||
});
|
||||
expect(dup.size).toBe(0);
|
||||
});
|
||||
|
||||
it('does not flag $app/state and $app/stores (different modules)', () => {
|
||||
const dup = findDuplicateMockIds({
|
||||
'a.spec.ts': `vi.mock('$app/state', () => ({}));`,
|
||||
'b.spec.ts': `vi.mock('$app/stores', () => ({}));`
|
||||
});
|
||||
expect(dup.size).toBe(0);
|
||||
});
|
||||
|
||||
it('does not flag $lib/foo and $lib/bar (different canonical paths)', () => {
|
||||
const dup = findDuplicateMockIds({
|
||||
'a.spec.ts': `vi.mock('$lib/foo', () => ({}));`,
|
||||
'b.spec.ts': `vi.mock('$lib/bar', () => ({}));`
|
||||
});
|
||||
expect(dup.size).toBe(0);
|
||||
});
|
||||
|
||||
it('flags both spellings within a single file', () => {
|
||||
const dup = findDuplicateMockIds({
|
||||
'a.spec.ts': `
|
||||
vi.mock('$lib/foo.svelte', () => ({}));
|
||||
vi.mock('$lib/foo.svelte.js', () => ({}));
|
||||
`
|
||||
});
|
||||
expect(dup.get('$lib/foo.svelte')?.size).toBe(2);
|
||||
});
|
||||
|
||||
it('canonicalises .svelte.ts the same way as .svelte.js', () => {
|
||||
const dup = findDuplicateMockIds({
|
||||
'a.spec.ts': `vi.mock('$lib/foo.svelte', () => ({}));`,
|
||||
'b.spec.ts': `vi.mock('$lib/foo.svelte.ts', () => ({}));`
|
||||
});
|
||||
expect(dup.get('$lib/foo.svelte')?.size).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('browser specs: no duplicate-id vi.mock calls across the suite', () => {
|
||||
it('every mocked module is referenced under exactly one id string', () => {
|
||||
const specFiles = findBrowserSpecs();
|
||||
expect(specFiles.length).toBeGreaterThan(0);
|
||||
const sources = Object.fromEntries(
|
||||
specFiles.map((file) => [file, readFileSync(file, 'utf-8')])
|
||||
);
|
||||
const duplicates = findDuplicateMockIds(sources);
|
||||
const report = Object.fromEntries([...duplicates].map(([k, v]) => [k, [...v]]));
|
||||
expect(report).toEqual({});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { navigating } from '$app/stores';
|
||||
import { navigating } from '$app/state';
|
||||
import DashboardNeedsMetadata from './DashboardNeedsMetadata.svelte';
|
||||
import UploadSuccessBanner from './UploadSuccessBanner.svelte';
|
||||
|
||||
@@ -18,7 +18,7 @@ interface Props {
|
||||
|
||||
let { topDocs, totalCount, bannerCount, onBannerClose }: Props = $props();
|
||||
|
||||
const showSkeleton = $derived(!!$navigating && topDocs.length === 0);
|
||||
const showSkeleton = $derived(!!navigating.type && topDocs.length === 0);
|
||||
const showBlock = $derived(topDocs.length > 0 || bannerCount > 0 || showSkeleton);
|
||||
</script>
|
||||
|
||||
|
||||
@@ -2,19 +2,23 @@ import { describe, it, expect, afterEach, vi } from 'vitest';
|
||||
import { cleanup, render } from 'vitest-browser-svelte';
|
||||
import { page } from 'vitest/browser';
|
||||
|
||||
// The store must live in a separate module because vi.mock factories are
|
||||
// hoisted and cannot reference top-level variables defined in this file.
|
||||
import { navigatingStore } from './__mocks__/navigatingStore';
|
||||
import EnrichmentBlock from './EnrichmentBlock.svelte';
|
||||
|
||||
vi.mock('$app/stores', async () => {
|
||||
const mod = await import('./__mocks__/navigatingStore');
|
||||
return { navigating: mod.navigatingStore };
|
||||
});
|
||||
// Hoist the mutable navigation reference so vi.mock's factory (also hoisted)
|
||||
// can read it via a getter. Sync factory, no dynamic import: ADR-012 invariant.
|
||||
const { mockNavigating } = vi.hoisted(() => ({
|
||||
mockNavigating: { type: null as string | null }
|
||||
}));
|
||||
|
||||
vi.mock('$app/state', () => ({
|
||||
get navigating() {
|
||||
return mockNavigating;
|
||||
}
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
navigatingStore.set(null);
|
||||
mockNavigating.type = null;
|
||||
});
|
||||
|
||||
type Doc = { id: string; title: string; uploadedAt: string };
|
||||
@@ -65,8 +69,8 @@ describe('EnrichmentBlock', () => {
|
||||
await expect.element(page.getByTestId('dashboard-needs-metadata')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the skeleton when $navigating is active and topDocs is empty', async () => {
|
||||
navigatingStore.set({ type: 'link' });
|
||||
it('renders the skeleton when navigation is active and topDocs is empty', async () => {
|
||||
mockNavigating.type = 'link';
|
||||
render(EnrichmentBlock, {
|
||||
topDocs: [],
|
||||
totalCount: 0,
|
||||
@@ -76,8 +80,8 @@ describe('EnrichmentBlock', () => {
|
||||
await expect.element(page.getByTestId('enrichment-block-skeleton')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render the skeleton when topDocs is non-empty even during $navigating', async () => {
|
||||
navigatingStore.set({ type: 'link' });
|
||||
it('does not render the skeleton when topDocs is non-empty even during navigation', async () => {
|
||||
mockNavigating.type = 'link';
|
||||
render(EnrichmentBlock, {
|
||||
topDocs: [doc('d1')],
|
||||
totalCount: 1,
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const navigatingStore = writable<unknown | null>(null);
|
||||
@@ -5,9 +5,6 @@ import { page } from 'vitest/browser';
|
||||
vi.mock('$lib/shared/services/confirm.svelte', () => ({
|
||||
getConfirmService: () => ({ confirm: async () => false })
|
||||
}));
|
||||
vi.mock('$lib/shared/services/confirm.svelte.js', () => ({
|
||||
getConfirmService: () => ({ confirm: async () => false })
|
||||
}));
|
||||
|
||||
const { default: TranscriptionEditView } = await import('./TranscriptionEditView.svelte');
|
||||
import type { TranscriptionBlockData } from '$lib/shared/types';
|
||||
|
||||
@@ -9,7 +9,7 @@ import NotificationBell from '$lib/notification/NotificationBell.svelte';
|
||||
import AppNav from './AppNav.svelte';
|
||||
import UserMenu from './UserMenu.svelte';
|
||||
import ConfirmDialog from '$lib/shared/primitives/ConfirmDialog.svelte';
|
||||
import { provideConfirmService } from '$lib/shared/services/confirm.svelte.js';
|
||||
import { provideConfirmService } from '$lib/shared/services/confirm.svelte';
|
||||
import { bulkSelectionStore } from '$lib/document/bulkSelection.svelte';
|
||||
|
||||
let { children, data } = $props();
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||
import { cleanup, render } from 'vitest-browser-svelte';
|
||||
import { page } from 'vitest/browser';
|
||||
|
||||
vi.mock('$lib/shared/services/confirm.svelte.js', () => ({
|
||||
vi.mock('$lib/shared/services/confirm.svelte', () => ({
|
||||
getConfirmService: () => ({ confirm: async () => false })
|
||||
}));
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
import { getConfirmService } from '$lib/shared/services/confirm.svelte.js';
|
||||
import { getConfirmService } from '$lib/shared/services/confirm.svelte';
|
||||
|
||||
type FlatTag = {
|
||||
id: string;
|
||||
|
||||
@@ -16,9 +16,6 @@ vi.mock('$app/stores', () => ({
|
||||
vi.mock('$lib/shared/services/confirm.svelte', () => ({
|
||||
getConfirmService: () => ({ confirm: async () => false })
|
||||
}));
|
||||
vi.mock('$lib/shared/services/confirm.svelte.js', () => ({
|
||||
getConfirmService: () => ({ confirm: async () => false })
|
||||
}));
|
||||
|
||||
vi.mock('$app/navigation', () => ({
|
||||
beforeNavigate: () => {},
|
||||
|
||||
@@ -5,9 +5,6 @@ import { page } from 'vitest/browser';
|
||||
vi.mock('$lib/shared/services/confirm.svelte', () => ({
|
||||
getConfirmService: () => ({ confirm: async () => false })
|
||||
}));
|
||||
vi.mock('$lib/shared/services/confirm.svelte.js', () => ({
|
||||
getConfirmService: () => ({ confirm: async () => false })
|
||||
}));
|
||||
|
||||
const { default: AdminUserEditPage } = await import('./+page.svelte');
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import { createOcrJob } from '$lib/ocr/useOcrJob.svelte';
|
||||
import { createTranscriptionBlocks } from '$lib/document/transcription/useTranscriptionBlocks.svelte';
|
||||
import { createFileLoader } from '$lib/document/viewer/useFileLoader.svelte';
|
||||
import { scrollToCommentFromQuery } from '$lib/shared/utils/deepLinkScroll';
|
||||
import { getConfirmService } from '$lib/shared/services/confirm.svelte.js';
|
||||
import { getConfirmService } from '$lib/shared/services/confirm.svelte';
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||
import { cleanup, render } from 'vitest-browser-svelte';
|
||||
|
||||
vi.mock('$lib/shared/services/confirm.svelte.js', () => ({
|
||||
vi.mock('$lib/shared/services/confirm.svelte', () => ({
|
||||
getConfirmService: () => ({ confirm: async () => false })
|
||||
}));
|
||||
|
||||
|
||||
@@ -30,9 +30,6 @@ vi.mock('$app/navigation', () => ({
|
||||
vi.mock('$lib/shared/services/confirm.svelte', () => ({
|
||||
getConfirmService: () => ({ confirm: async () => false })
|
||||
}));
|
||||
vi.mock('$lib/shared/services/confirm.svelte.js', () => ({
|
||||
getConfirmService: () => ({ confirm: async () => false })
|
||||
}));
|
||||
|
||||
const { default: DocumentDetailPage } = await import('./+page.svelte');
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
import { getConfirmService } from '$lib/shared/services/confirm.svelte.js';
|
||||
import { getConfirmService } from '$lib/shared/services/confirm.svelte';
|
||||
|
||||
interface Props {
|
||||
aliases: Array<{
|
||||
|
||||
@@ -5,9 +5,6 @@ import { page } from 'vitest/browser';
|
||||
vi.mock('$lib/shared/services/confirm.svelte', () => ({
|
||||
getConfirmService: () => ({ confirm: async () => false })
|
||||
}));
|
||||
vi.mock('$lib/shared/services/confirm.svelte.js', () => ({
|
||||
getConfirmService: () => ({ confirm: async () => false })
|
||||
}));
|
||||
|
||||
const { default: PersonEditPage } = await import('./+page.svelte');
|
||||
|
||||
|
||||
14
frontend/src/test-setup.ts
Normal file
14
frontend/src/test-setup.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
// Disable SvelteKit hover-prefetch (both data + code) in browser-mode tests.
|
||||
// ADR-012 / #553.
|
||||
//
|
||||
// Hover-prefetch fires real fetch requests for route loader chunks; those
|
||||
// requests go through the same Playwright route handler that serves mocked
|
||||
// modules. An in-flight prefetch landing after iframe teardown can hit the
|
||||
// route handler with a closed birpc channel, raising an unhandled rejection
|
||||
// that exits the run with code 1 even when every individual test was green.
|
||||
//
|
||||
// `data-sveltekit-preload-data="off"` disables loader-data prefetch;
|
||||
// `data-sveltekit-preload-code="off"` disables route-code chunk prefetch.
|
||||
// Both surfaces can produce late module fetches that hit the route handler.
|
||||
document.body.dataset.sveltekitPreloadData = 'off';
|
||||
document.body.dataset.sveltekitPreloadCode = 'off';
|
||||
@@ -77,6 +77,7 @@ export default defineConfig({
|
||||
screenshotDirectory: 'test-results/screenshots',
|
||||
screenshotFailures: true
|
||||
},
|
||||
setupFiles: ['./src/test-setup.ts'],
|
||||
include: ['src/**/*.svelte.{test,spec}.{js,ts}'],
|
||||
exclude: ['src/lib/server/**']
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ export default defineConfig({
|
||||
screenshotDirectory: 'test-results/screenshots',
|
||||
screenshotFailures: true
|
||||
},
|
||||
setupFiles: ['./src/test-setup.ts'],
|
||||
include: ['src/**/*.svelte.{test,spec}.{js,ts}'],
|
||||
exclude: ['src/lib/server/**'],
|
||||
coverage: {
|
||||
|
||||
Reference in New Issue
Block a user