nuxt-vitest-shared-app 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +389 -0
- package/dist/adapter/entry.d.mts +1 -0
- package/dist/adapter/entry.mjs +62 -0
- package/dist/config.d.mts +21 -0
- package/dist/config.mjs +151 -0
- package/dist/dom.d.mts +13 -0
- package/dist/dom.mjs +54 -0
- package/dist/index.d.mts +71 -0
- package/dist/index.mjs +95 -0
- package/dist/runtime/lifecycle.d.mts +1 -0
- package/dist/runtime/lifecycle.mjs +180 -0
- package/dist/setup-nuxt-id-Bb5_sSMp.mjs +9 -0
- package/dist/startup-error-C4H64PiD.mjs +13 -0
- package/dist/store-Dklvd4X3.mjs +32 -0
- package/package.json +74 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ilya Semenov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
# nuxt-vitest-shared-app
|
|
2
|
+
|
|
3
|
+
Run Nuxt component tests in Vitest on one Nuxt app per Vitest worker, shared by all test files of that worker.
|
|
4
|
+
|
|
5
|
+
The library provides:
|
|
6
|
+
|
|
7
|
+
- the shared-app lifecycle on top of `@nuxt/test-utils` (`environment: "nuxt"`, `mountSuspended`, `mockNuxtImport`);
|
|
8
|
+
- deterministic cleanup of documented Nuxt and test-owned state after each test, with explicit project resets for application-specific resources;
|
|
9
|
+
- typed per-test overrides of Nuxt auto-imports.
|
|
10
|
+
|
|
11
|
+
It does not reset the whole app state.
|
|
12
|
+
State created by startup plugins is not re-created between tests or files.
|
|
13
|
+
Tests that need plugins re-initialized or a different startup config belong in a [separate Vitest config](#startup-sensitive-tests).
|
|
14
|
+
|
|
15
|
+
- [The problem](#the-problem)
|
|
16
|
+
- [Install](#install)
|
|
17
|
+
- [Quick start](#quick-start)
|
|
18
|
+
- [Migrating an existing suite](#migrating-an-existing-suite)
|
|
19
|
+
- [Per-test overrides](#per-test-overrides)
|
|
20
|
+
- [Project resets](#project-resets)
|
|
21
|
+
- [Worker state](#worker-state)
|
|
22
|
+
- [DOM helpers](#dom-helpers)
|
|
23
|
+
- [Lifecycle](#lifecycle)
|
|
24
|
+
- [Cleanup](#cleanup)
|
|
25
|
+
- [Startup-sensitive tests](#startup-sensitive-tests)
|
|
26
|
+
- [Compatibility and upstream](#compatibility-and-upstream)
|
|
27
|
+
|
|
28
|
+
## The problem
|
|
29
|
+
|
|
30
|
+
`@nuxt/test-utils` adds its runtime entry to `test.setupFiles`.
|
|
31
|
+
The entry calls `vi.resetModules()` and starts Nuxt in `beforeAll` for every test file, so Nuxt is initialized again per file even with `isolate: false`.
|
|
32
|
+
In a project with many small component test files, most of the run time goes into Nuxt startup.
|
|
33
|
+
|
|
34
|
+
With this library, each Vitest worker starts Nuxt once, and all its test files run on that app.
|
|
35
|
+
As a data point from one application with a local workaround, [nuxt/test-utils#1750](https://github.com/nuxt/test-utils/issues/1750) reports 12 files with 22 tests going from 19.6 s to 6.6 s with one worker; your numbers will differ.
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
npm install -D nuxt-vitest-shared-app
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Supported versions:
|
|
44
|
+
|
|
45
|
+
| Package | Versions |
|
|
46
|
+
| ------------------ | ------------------------ |
|
|
47
|
+
| `@nuxt/test-utils` | 4.3.x |
|
|
48
|
+
| `nuxt` | 4.4.2 and later 4.x |
|
|
49
|
+
| `vitest` | 4.1.0 and later 4.x, 5.x |
|
|
50
|
+
| `vue` | 3.5.30 and later 3.x |
|
|
51
|
+
| `@vue/test-utils` | 2.4.2 and later 2.x |
|
|
52
|
+
| `happy-dom` | 20.0.11 and later |
|
|
53
|
+
|
|
54
|
+
CI runs the whole suite with the lowest listed versions, with the versions pinned in the lockfile, and with the latest compatible Vitest 4 release.
|
|
55
|
+
jsdom is not supported in this version.
|
|
56
|
+
|
|
57
|
+
## Quick start
|
|
58
|
+
|
|
59
|
+
Replace `defineVitestConfig` with `defineSharedNuxtVitestConfig` in the Vitest config of your Nuxt component tests:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
// vitest.config.ts
|
|
63
|
+
import { defineSharedNuxtVitestConfig } from "nuxt-vitest-shared-app/config"
|
|
64
|
+
|
|
65
|
+
export default defineSharedNuxtVitestConfig({
|
|
66
|
+
test: {
|
|
67
|
+
include: ["**/*.vitest.ts"],
|
|
68
|
+
environment: "nuxt",
|
|
69
|
+
pool: "threads",
|
|
70
|
+
isolate: false,
|
|
71
|
+
},
|
|
72
|
+
})
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Keep your existing test file patterns and other settings.
|
|
76
|
+
Existing `mountSuspended` tests run on the shared app with the built-in cleanup as they are.
|
|
77
|
+
Add a project setup file only when you need [per-test overrides](#per-test-overrides) or [project resets](#project-resets).
|
|
78
|
+
|
|
79
|
+
The wrapper passes the config to `defineVitestConfig` from `@nuxt/test-utils/config`, adds the library lifecycle, and checks the settings a shared app relies on:
|
|
80
|
+
|
|
81
|
+
- `test.environment` must be `"nuxt"` at the top level; Vitest projects and browser mode are not supported;
|
|
82
|
+
- `test.isolate` must be `false`;
|
|
83
|
+
- `test.pool` must be `"threads"` or `"forks"`; `isolate` has no effect in the VM pools;
|
|
84
|
+
- `test.sequence.setupFiles` is set to `"list"`, and `"parallel"` is rejected;
|
|
85
|
+
- `test.sequence.hooks: "parallel"` and `test.sequence.concurrent: true` are rejected.
|
|
86
|
+
|
|
87
|
+
The second argument takes options:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
defineSharedNuxtVitestConfig(config, { silenceSuspenseInfo: true })
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
With `silenceSuspenseInfo: true`, Vue's `<Suspense> is an experimental feature and its API will likely change.` message, which every `mountSuspended` call prints, is filtered through `test.onConsoleLog`; your own `onConsoleLog` still receives all other messages.
|
|
94
|
+
|
|
95
|
+
## Migrating an existing suite
|
|
96
|
+
|
|
97
|
+
Remove what the library now does:
|
|
98
|
+
|
|
99
|
+
- `enableAutoUnmount` calls in project setup files: the library installs its own, and `@vue/test-utils` rejects a second call;
|
|
100
|
+
- hooks that clean up what the library [cleans up](#cleanup), such as Nuxt state, storage, cookies, timers, and stubbed globals;
|
|
101
|
+
- local patches or workarounds that start Nuxt once per worker.
|
|
102
|
+
|
|
103
|
+
Keep project-specific cleanup, and move hooks that must run after every test into [project resets](#project-resets).
|
|
104
|
+
|
|
105
|
+
Because modules stay cached, objects created at module level live for the whole environment, not for one file.
|
|
106
|
+
A global replaced by one test file affects every file that runs after it in the worker, including objects that other modules already created from the previous global.
|
|
107
|
+
For example, a test file that imports a forced `Intl` polyfill replaces the global constructor, while a formatter created earlier at module level still belongs to the previous implementation.
|
|
108
|
+
|
|
109
|
+
- Install global polyfills once in a project setup file, before components are imported, and prefer polyfills that keep an existing implementation.
|
|
110
|
+
- Do not replace global constructors with side-effect imports in individual test files.
|
|
111
|
+
- Review module-level objects that depend on such globals, such as formatters, clients, and caches.
|
|
112
|
+
- After the migration, run the suite on one worker with shuffled files, so all files share one environment in a different order, e.g. `vitest run --maxWorkers=1 --sequence.shuffle.files --sequence.seed=1`.
|
|
113
|
+
|
|
114
|
+
## Per-test overrides
|
|
115
|
+
|
|
116
|
+
A module mock registered again in a later file does not change references that the started app already captured.
|
|
117
|
+
So the project installs a stable wrapper once with `mockNuxtImport`, and tests swap its implementation for one test.
|
|
118
|
+
|
|
119
|
+
Register overridable imports in a project setup file:
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
// vitest.config.ts
|
|
123
|
+
export default defineSharedNuxtVitestConfig({
|
|
124
|
+
test: {
|
|
125
|
+
// ...
|
|
126
|
+
setupFiles: ["./testing/nuxt/setup.ts"],
|
|
127
|
+
},
|
|
128
|
+
})
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
// testing/nuxt/setup.ts
|
|
133
|
+
import { mockNuxtImport } from "@nuxt/test-utils/runtime"
|
|
134
|
+
import { overridableNuxtImport } from "nuxt-vitest-shared-app"
|
|
135
|
+
|
|
136
|
+
import type { navigateTo, useRoute } from "#app/composables/router"
|
|
137
|
+
|
|
138
|
+
declare module "nuxt-vitest-shared-app" {
|
|
139
|
+
interface TestNuxtImports {
|
|
140
|
+
navigateTo: typeof navigateTo
|
|
141
|
+
useRoute: typeof useRoute
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
mockNuxtImport("navigateTo", overridableNuxtImport("navigateTo"))
|
|
146
|
+
mockNuxtImport("useRoute", overridableNuxtImport("useRoute"))
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Override them inside a test or a test hook:
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
import { mountSuspended } from "@nuxt/test-utils/runtime"
|
|
153
|
+
import { overrideNuxtImport, overrideNuxtRoute } from "nuxt-vitest-shared-app"
|
|
154
|
+
import { expect, it, vi } from "vitest"
|
|
155
|
+
|
|
156
|
+
import ItemPage from "~/pages/items/[id].vue"
|
|
157
|
+
|
|
158
|
+
it("opens the edit page", async () => {
|
|
159
|
+
const navigate = vi.fn()
|
|
160
|
+
overrideNuxtImport("navigateTo", navigate)
|
|
161
|
+
overrideNuxtRoute({ params: { id: "1" } })
|
|
162
|
+
|
|
163
|
+
const wrapper = await mountSuspended(ItemPage)
|
|
164
|
+
await wrapper.get("button").trigger("click")
|
|
165
|
+
|
|
166
|
+
expect(navigate).toHaveBeenCalledWith("/items/1/edit")
|
|
167
|
+
})
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Overrides apply to the current test only; the cleanup clears them.
|
|
171
|
+
Do not set them at the top level of a test file: the file is collected before the app starts, and the override would outlive the test.
|
|
172
|
+
|
|
173
|
+
Use a fallback for behavior that the app needs while it starts.
|
|
174
|
+
First add `useUser` and its type to `TestNuxtImports`, as in the setup file above, then:
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
// testing/nuxt/setup.ts
|
|
178
|
+
mockNuxtImport(
|
|
179
|
+
"useUser",
|
|
180
|
+
overridableNuxtImport("useUser", () => ref(null)),
|
|
181
|
+
)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Plugins and other startup code call auto-imports before any test hook runs, so only a fallback applies to them.
|
|
185
|
+
|
|
186
|
+
### `overridableNuxtImport(name, fallback?)`
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
overridableNuxtImport(name, fallback?): (original) => wrapper
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Creates the `mockNuxtImport` factory; `mockNuxtImport` is a compile-time macro, so its calls stay in your project setup file.
|
|
193
|
+
The wrapper calls the current test's override, or otherwise `fallback` if given, or the original import, preserving `this` and arguments.
|
|
194
|
+
|
|
195
|
+
The project augments `TestNuxtImports` once, mapping import names to their types.
|
|
196
|
+
`overridableNuxtImport` and `overrideNuxtImport` accept only registered names, and implementations are typed by the registry.
|
|
197
|
+
|
|
198
|
+
The factory can run during hoisted mock evaluation: it does not initialize Nuxt or import the mocked modules.
|
|
199
|
+
When a mock factory is evaluated again, it returns the wrapper that the app already holds.
|
|
200
|
+
|
|
201
|
+
### `overrideNuxtImport(name, implementation)`
|
|
202
|
+
|
|
203
|
+
Overrides the import for the current test.
|
|
204
|
+
It throws when no wrapper for `name` was installed, so a missing `mockNuxtImport` line does not look like a working override.
|
|
205
|
+
|
|
206
|
+
### `overrideNuxtRoute(route)`
|
|
207
|
+
|
|
208
|
+
Makes the `useRoute` wrapper return exactly `route`, a `Partial` of the registered `useRoute` return type, for the current test.
|
|
209
|
+
It requires `useRoute` in `TestNuxtImports` and an overridable `useRoute` mock.
|
|
210
|
+
|
|
211
|
+
- It does not navigate or sync the real router.
|
|
212
|
+
- It does not merge `route` with the real route: missing fields are not filled in, and the object does not satisfy the full `useRoute` contract.
|
|
213
|
+
- Mounted components do not react to it; set it before mounting.
|
|
214
|
+
|
|
215
|
+
## Project resets
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
registerNuxtTestReset(key: string, reset: () => unknown): () => void
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Registers a reset that runs after each test, see step 6 of the [cleanup](#cleanup).
|
|
222
|
+
Use it for application resources that the library does not know about, such as test API clients, subscriptions, and caches:
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
// testing/nuxt/setup.ts
|
|
226
|
+
import { registerNuxtTestReset } from "nuxt-vitest-shared-app"
|
|
227
|
+
|
|
228
|
+
import { testApiClient } from "./api-client"
|
|
229
|
+
|
|
230
|
+
registerNuxtTestReset("api-client", () => testApiClient.reset())
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Registering the same key again replaces the callback and keeps its position, so a setup file that runs for every test file does not accumulate registrations.
|
|
234
|
+
The returned function removes the registration, unless it has already been replaced.
|
|
235
|
+
|
|
236
|
+
## Worker state
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
getOrCreateWorkerState<T>(name: string, create: () => T): T
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Returns state bound to the current Nuxt environment, creating it on first use.
|
|
243
|
+
Use it for project helpers whose state must survive module re-evaluation between files, such as a test API client that the started app also uses.
|
|
244
|
+
|
|
245
|
+
## DOM helpers
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
import { stubVisibleIntersectionObserver } from "nuxt-vitest-shared-app/dom"
|
|
249
|
+
import { beforeEach } from "vitest"
|
|
250
|
+
|
|
251
|
+
beforeEach(() => {
|
|
252
|
+
stubVisibleIntersectionObserver()
|
|
253
|
+
})
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Stubs the global `IntersectionObserver` with one that reports each observed element as visible immediately in `observe()`, because happy-dom and the `@nuxt/test-utils` mock never call the observer callback.
|
|
257
|
+
It accepts elements and the environment's `window` and `document`, which VueUse passes for window scrolling, e.g. in `useInfiniteScroll(window)`; they are measured as `document.documentElement` and reported as the entry `target` unchanged.
|
|
258
|
+
Any other target throws a `TypeError`.
|
|
259
|
+
Entries include `time`, which vueuse `useElementVisibility` uses to pick the latest entry.
|
|
260
|
+
The stub is installed with `vi.stubGlobal`, so the cleanup removes it after the test; install it in `beforeEach` or in the test itself.
|
|
261
|
+
|
|
262
|
+
## Lifecycle
|
|
263
|
+
|
|
264
|
+
| What | Lifetime |
|
|
265
|
+
| ------------------------------------------------------------------------------ | ------------------------------------------------- |
|
|
266
|
+
| Started app, route baseline, project resets, overridable imports, worker state | The Nuxt environment (its `window`) of the worker |
|
|
267
|
+
| Library `beforeAll` / `beforeEach` hooks | One test file |
|
|
268
|
+
| Import overrides and per-test resources | One test |
|
|
269
|
+
|
|
270
|
+
For each test file of a worker, the library:
|
|
271
|
+
|
|
272
|
+
1. starts Nuxt if this environment has no app yet, or awaits the startup in progress;
|
|
273
|
+
2. checks that the Nuxt app is mounted in the current environment's document, and fails the file with a clear error otherwise;
|
|
274
|
+
3. captures the route baseline and the direct children of `document.body` once per environment, right after the first successful startup and before any project `beforeAll` or `beforeEach`;
|
|
275
|
+
4. fails tests that run concurrently in the worker (`test.concurrent`, `describe.concurrent`);
|
|
276
|
+
5. cleans up after each test.
|
|
277
|
+
|
|
278
|
+
Vitest drops hooks between files even with `isolate: false`, so the lifecycle hooks are registered again for every file.
|
|
279
|
+
|
|
280
|
+
Library state is bound to the environment's `window` and is never reused by another environment.
|
|
281
|
+
|
|
282
|
+
Watch mode is not tested; the guarantees in this document apply to `vitest run`.
|
|
283
|
+
|
|
284
|
+
### Tests run sequentially within a worker
|
|
285
|
+
|
|
286
|
+
All tests of a worker share one app, so they must not run concurrently.
|
|
287
|
+
Parallelism across workers is supported: each worker has its own environment and its own app.
|
|
288
|
+
|
|
289
|
+
### Module cache
|
|
290
|
+
|
|
291
|
+
Modules are evaluated once per environment and stay cached for the following files.
|
|
292
|
+
A singleton created by a plugin at startup is the same instance that components and test files see in every file of the worker.
|
|
293
|
+
Only the first file of an environment, or the next file after a failed startup, starts from a clean module graph.
|
|
294
|
+
|
|
295
|
+
### Files with another environment
|
|
296
|
+
|
|
297
|
+
Test files with another environment, such as `// @vitest-environment node`, run without the library lifecycle.
|
|
298
|
+
In such files, `overridableNuxtImport` factories return the fallback or the original import, and `registerNuxtTestReset` registers nothing, so the same project setup file works for them.
|
|
299
|
+
`overrideNuxtImport`, `overrideNuxtRoute` and `getOrCreateWorkerState` throw outside the `nuxt` environment.
|
|
300
|
+
|
|
301
|
+
## Cleanup
|
|
302
|
+
|
|
303
|
+
After each test, the library runs these steps in order.
|
|
304
|
+
They run after all `afterEach` hooks of the project, also when one of these hooks fails.
|
|
305
|
+
|
|
306
|
+
1. unmounts wrappers mounted in the test through `@vue/test-utils` (including `mountSuspended`), using `enableAutoUnmount`;
|
|
307
|
+
2. `await nextTick()` and `await flushPromises()`;
|
|
308
|
+
3. navigates the app's own router back to the route baseline with `router.replace` if the route differs, and verifies the resulting route;
|
|
309
|
+
4. clears import overrides;
|
|
310
|
+
5. `await clearError()`, `clearNuxtData()`, `clearNuxtState(undefined, { reset: false })`;
|
|
311
|
+
6. runs project resets registered with `registerNuxtTestReset`, in registration order, awaiting each;
|
|
312
|
+
7. `localStorage.clear()`, `sessionStorage.clear()`, and expires every cookie visible in `document.cookie` for path `/` of the current origin;
|
|
313
|
+
8. `vi.clearAllTimers()`, `vi.useRealTimers()`, `vi.unstubAllEnvs()`, `vi.unstubAllGlobals()`;
|
|
314
|
+
9. removes direct children of `document.body` that were not present at startup.
|
|
315
|
+
|
|
316
|
+
The exact scope:
|
|
317
|
+
|
|
318
|
+
- `flushPromises` only settles handlers of promises that are already resolved; subscriptions, requests, and other background work need their own project resets;
|
|
319
|
+
- step 3 uses the router instance of the app, never the overridable `useRoute`/`navigateTo`;
|
|
320
|
+
- DOM cleanup removes only new direct children of `document.body`; changes to nodes that existed at startup are not reverted;
|
|
321
|
+
- cookies are expired only as listed above; `HttpOnly` cookies and cookies of other paths or domains are not touched;
|
|
322
|
+
- state created by startup plugins is not re-created.
|
|
323
|
+
|
|
324
|
+
The library does not touch Vitest mocks (`vi.fn`, `vi.spyOn`, call history).
|
|
325
|
+
Configure `clearMocks`, `mockReset`, or `restoreMocks` in your Vitest config as needed.
|
|
326
|
+
|
|
327
|
+
### Failed cleanup
|
|
328
|
+
|
|
329
|
+
If a cleanup step throws or rejects, the remaining steps still run.
|
|
330
|
+
Failures are reported with their step names, and all collected errors are preserved.
|
|
331
|
+
|
|
332
|
+
If the cleanup does not finish within the Vitest hook timeout, e.g. because a project reset never settles, Vitest does not cancel it, and it may still change state later.
|
|
333
|
+
The library then restores real timers, envs, and globals right away, because project `afterEach` and `afterAll` hooks still run, but the other remaining steps are not guaranteed to run.
|
|
334
|
+
If Vitest aborts a test because the test, one of its hooks, or an `onTestFinished`/`onTestFailed` callback times out, the environment is rejected the same way, because unfinished asynchronous work may continue after Vitest stops waiting for it; the cleanup of that test still runs.
|
|
335
|
+
|
|
336
|
+
After a failed or timed out cleanup or an aborted test, the environment is in an unknown state, even if the pending work finishes later.
|
|
337
|
+
Every following test in it fails with an error that references the original failure, and following files fail before their own `beforeAll` hooks.
|
|
338
|
+
There is no recovery in this version.
|
|
339
|
+
|
|
340
|
+
### Failed startup
|
|
341
|
+
|
|
342
|
+
If Nuxt startup fails, the file fails with the original error, and a partially created app is unmounted.
|
|
343
|
+
Nuxt itself catches errors of plugins during startup and continues with a partially initialized app; the library treats such an error as a startup failure too, and reports it with the Nuxt error as its cause.
|
|
344
|
+
If unmounting fails too, both errors are reported together in an `AggregateError`, the original one first.
|
|
345
|
+
The next file of the worker tries the startup again from a clean module graph.
|
|
346
|
+
|
|
347
|
+
## Startup-sensitive tests
|
|
348
|
+
|
|
349
|
+
Tests that need startup plugins re-initialized, a different runtime config, or other startup options cannot share the app.
|
|
350
|
+
Put them in a separate Vitest config with normal isolation and run it with its own command, because the shared-app config does not support Vitest projects:
|
|
351
|
+
|
|
352
|
+
```ts
|
|
353
|
+
// vitest.isolated.config.ts
|
|
354
|
+
import { defineVitestConfig } from "@nuxt/test-utils/config"
|
|
355
|
+
|
|
356
|
+
export default defineVitestConfig({
|
|
357
|
+
test: {
|
|
358
|
+
include: ["**/*.isolated.vitest.ts"],
|
|
359
|
+
environment: "nuxt",
|
|
360
|
+
},
|
|
361
|
+
})
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
```sh
|
|
365
|
+
vitest run
|
|
366
|
+
vitest run --config vitest.isolated.config.ts
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
Exclude the isolated files from the shared-app config.
|
|
370
|
+
|
|
371
|
+
## Compatibility and upstream
|
|
372
|
+
|
|
373
|
+
Until `@nuxt/test-utils` reuses the app natively, the library replaces its runtime entry with an adapter that follows the lifecycle of [nuxt/test-utils#1821](https://github.com/nuxt/test-utils/pull/1821).
|
|
374
|
+
The adapter loads the internal `setupNuxt` from `@nuxt/test-utils/dist/runtime/shared/nuxt.mjs` through the Vitest module runner, because it shares state with `mountSuspended`.
|
|
375
|
+
This internal module is not part of the public `@nuxt/test-utils` API, so each supported release line is tested explicitly.
|
|
376
|
+
|
|
377
|
+
| Installed `@nuxt/test-utils` | Behavior |
|
|
378
|
+
| ------------------------------------------------------- | --------------------------------------------------------------- |
|
|
379
|
+
| 4.3.x | The adapter replaces the upstream runtime entry |
|
|
380
|
+
| A tested release with native reuse (none published yet) | The upstream entry is kept; the library lifecycle runs after it |
|
|
381
|
+
| Any other version | The config throws an error with the installed version |
|
|
382
|
+
|
|
383
|
+
Native reuse is detected by version, not by inspecting `@nuxt/test-utils` internals.
|
|
384
|
+
Your tests and setup files stay the same when the library switches to native reuse.
|
|
385
|
+
Upstream work on native reuse is tracked in [nuxt/test-utils#1750](https://github.com/nuxt/test-utils/issues/1750) and [nuxt/test-utils#1821](https://github.com/nuxt/test-utils/pull/1821).
|
|
386
|
+
|
|
387
|
+
[untestutils](https://github.com/s00d/untestutils) is a test harness whose Nuxt unit environment (`environment: "untestutils"`) has the same per-file startup.
|
|
388
|
+
Worker-scoped app reuse there is requested in [s00d/untestutils#1](https://github.com/s00d/untestutils/issues/1).
|
|
389
|
+
This library supports only `@nuxt/test-utils` for now; untestutils support may be added later, natively if that request is implemented.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { t as SETUP_NUXT_ID } from "../setup-nuxt-id-Bb5_sSMp.mjs";
|
|
2
|
+
import { t as getStartupError } from "../startup-error-C4H64PiD.mjs";
|
|
3
|
+
import { beforeAll, vi } from "vitest";
|
|
4
|
+
//#region src/adapter/entry.ts
|
|
5
|
+
const ADAPTER_KEY = Symbol.for("nuxt-vitest-shared-app:adapter");
|
|
6
|
+
if (typeof window !== "undefined" && window.__NUXT_VITEST_ENVIRONMENT__) {
|
|
7
|
+
const win = window;
|
|
8
|
+
const state = win[ADAPTER_KEY] ??= {};
|
|
9
|
+
if (!state.started) vi.resetModules();
|
|
10
|
+
beforeAll(async () => {
|
|
11
|
+
state.startup ??= startNuxt(win, state).finally(() => {
|
|
12
|
+
if (!state.started) delete state.startup;
|
|
13
|
+
});
|
|
14
|
+
await state.startup;
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
async function startNuxt(win, state) {
|
|
18
|
+
const { getNuxtAppCtx, tryUseNuxtApp } = await import("#app/nuxt");
|
|
19
|
+
const previousApp = tryUseNuxtApp();
|
|
20
|
+
const hasPayload = "__NUXT__" in win;
|
|
21
|
+
const payload = win.__NUXT__;
|
|
22
|
+
let failed = false;
|
|
23
|
+
let setupError;
|
|
24
|
+
try {
|
|
25
|
+
const { setupNuxt } = await import(
|
|
26
|
+
/* @vite-ignore */
|
|
27
|
+
SETUP_NUXT_ID
|
|
28
|
+
);
|
|
29
|
+
if (typeof setupNuxt !== "function") throw new TypeError("nuxt-vitest-shared-app: @nuxt/test-utils no longer exports setupNuxt from dist/runtime/shared/nuxt.mjs; this @nuxt/test-utils version is not supported.");
|
|
30
|
+
await setupNuxt();
|
|
31
|
+
} catch (error) {
|
|
32
|
+
failed = true;
|
|
33
|
+
setupError = error;
|
|
34
|
+
}
|
|
35
|
+
const createdApp = tryUseNuxtApp();
|
|
36
|
+
const app = createdApp && createdApp !== previousApp ? createdApp : void 0;
|
|
37
|
+
const startupError = app && getStartupError(app);
|
|
38
|
+
if (!failed && !startupError) {
|
|
39
|
+
state.started = true;
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const error = startupError ?? setupError;
|
|
43
|
+
let rollbackError;
|
|
44
|
+
try {
|
|
45
|
+
app?.vueApp.unmount();
|
|
46
|
+
} catch (unmountError) {
|
|
47
|
+
rollbackError = unmountError;
|
|
48
|
+
} finally {
|
|
49
|
+
if (app) {
|
|
50
|
+
const context = getNuxtAppCtx(app._id);
|
|
51
|
+
if (context.tryUse() === app) context.unset();
|
|
52
|
+
}
|
|
53
|
+
if (hasPayload) win.__NUXT__ = payload;
|
|
54
|
+
}
|
|
55
|
+
if (rollbackError === void 0) throw error;
|
|
56
|
+
throw new AggregateError([error, rollbackError], `nuxt-vitest-shared-app: Nuxt startup failed (${errorMessage(error)}), and unmounting the partially created app failed too (${errorMessage(rollbackError)}).`, { cause: error });
|
|
57
|
+
}
|
|
58
|
+
function errorMessage(error) {
|
|
59
|
+
return error instanceof Error ? error.message : String(error);
|
|
60
|
+
}
|
|
61
|
+
//#endregion
|
|
62
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { defineVitestConfig } from "@nuxt/test-utils/config";
|
|
2
|
+
//#region src/config/apply.d.ts
|
|
3
|
+
/** Config accepted by `defineVitestConfig` from `@nuxt/test-utils/config`. */
|
|
4
|
+
type NuxtVitestConfig = NonNullable<Parameters<typeof defineVitestConfig>[0]>;
|
|
5
|
+
/** Options of `defineSharedNuxtVitestConfig`. */
|
|
6
|
+
interface SharedNuxtVitestOptions {
|
|
7
|
+
/** Hide Vue's `<Suspense> is an experimental feature` console message, which every `mountSuspended` call prints. */
|
|
8
|
+
silenceSuspenseInfo?: boolean;
|
|
9
|
+
}
|
|
10
|
+
//#endregion
|
|
11
|
+
//#region src/config.d.ts
|
|
12
|
+
/**
|
|
13
|
+
* Wraps `defineVitestConfig` from `@nuxt/test-utils/config` so all test files of a Vitest worker share one Nuxt app.
|
|
14
|
+
*
|
|
15
|
+
* It starts Nuxt once per environment, adds the library lifecycle (readiness check, per-test cleanup, rejection of concurrent tests), and validates the settings a shared app relies on.
|
|
16
|
+
*
|
|
17
|
+
* @throws When the config uses an unsupported setting or the installed `@nuxt/test-utils` version was not tested.
|
|
18
|
+
*/
|
|
19
|
+
declare function defineSharedNuxtVitestConfig(config: NuxtVitestConfig, options?: SharedNuxtVitestOptions): ReturnType<typeof defineVitestConfig>;
|
|
20
|
+
//#endregion
|
|
21
|
+
export { type SharedNuxtVitestOptions, defineSharedNuxtVitestConfig };
|
package/dist/config.mjs
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import "./setup-nuxt-id-Bb5_sSMp.mjs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { defineVitestConfig } from "@nuxt/test-utils/config";
|
|
5
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
6
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
7
|
+
//#region src/adapter/setup-nuxt-plugin.ts
|
|
8
|
+
/**
|
|
9
|
+
* Creates the Vite plugin that resolves `SETUP_NUXT_ID` to the internal `setupNuxt` module of the installation.
|
|
10
|
+
*
|
|
11
|
+
* @throws When the installation has no such module.
|
|
12
|
+
*/
|
|
13
|
+
function setupNuxtPlugin(installation) {
|
|
14
|
+
const setupNuxtPath = join(installation.runtimeDir, "shared", "nuxt.mjs");
|
|
15
|
+
if (!existsSync(setupNuxtPath)) throw new Error(`nuxt-vitest-shared-app: @nuxt/test-utils ${installation.version} has no internal ${setupNuxtPath}; this version is not supported.`);
|
|
16
|
+
return {
|
|
17
|
+
name: "nuxt-vitest-shared-app:setup-nuxt",
|
|
18
|
+
enforce: "pre",
|
|
19
|
+
resolveId(id) {
|
|
20
|
+
if (id === "#nuxt-vitest-shared-app/setup-nuxt") return setupNuxtPath;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region src/config/versions.ts
|
|
26
|
+
/**
|
|
27
|
+
* Tested `@nuxt/test-utils` release lines.
|
|
28
|
+
*
|
|
29
|
+
* Native reuse is detected by version only, until upstream offers a documented public signal:
|
|
30
|
+
*
|
|
31
|
+
* - https://github.com/nuxt/test-utils/issues/1750
|
|
32
|
+
* - https://github.com/nuxt/test-utils/pull/1821
|
|
33
|
+
*/
|
|
34
|
+
const TESTED_VERSIONS = [{
|
|
35
|
+
line: "4.3",
|
|
36
|
+
mode: "adapter"
|
|
37
|
+
}];
|
|
38
|
+
/**
|
|
39
|
+
* Resolves the startup mode for an installed `@nuxt/test-utils` version.
|
|
40
|
+
*
|
|
41
|
+
* @throws For versions that were not tested, instead of guessing.
|
|
42
|
+
*/
|
|
43
|
+
function resolveStartupMode(version) {
|
|
44
|
+
const match = /^(\d+\.\d+)\.\d+$/.exec(version);
|
|
45
|
+
const tested = match && TESTED_VERSIONS.find((entry) => entry.line === match[1]);
|
|
46
|
+
if (!tested) {
|
|
47
|
+
const lines = TESTED_VERSIONS.map((entry) => `${entry.line}.x`).join(", ");
|
|
48
|
+
throw new Error(`nuxt-vitest-shared-app: @nuxt/test-utils ${version} is not supported. Supported versions: ${lines}.`);
|
|
49
|
+
}
|
|
50
|
+
return tested.mode;
|
|
51
|
+
}
|
|
52
|
+
//#endregion
|
|
53
|
+
//#region src/config/apply.ts
|
|
54
|
+
const SUSPENSE_INFO = "<Suspense> is an experimental feature and its API will likely change.";
|
|
55
|
+
/** Locates the `@nuxt/test-utils` installation that `@nuxt/test-utils/config` resolves to from `from`. */
|
|
56
|
+
function locateTestUtils(from) {
|
|
57
|
+
const require = createRequire(from);
|
|
58
|
+
const configPath = require.resolve("@nuxt/test-utils/config");
|
|
59
|
+
return {
|
|
60
|
+
version: JSON.parse(readFileSync(require.resolve("@nuxt/test-utils/package.json"), "utf8")).version,
|
|
61
|
+
runtimeDir: join(dirname(configPath), "runtime")
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Turns a config resolved by `defineVitestConfig` into a shared-app config, in place.
|
|
66
|
+
*
|
|
67
|
+
* @throws When the config is incompatible with a shared app or the installed `@nuxt/test-utils` is not supported.
|
|
68
|
+
*/
|
|
69
|
+
function applySharedApp(config, { options, installation, files, mode = resolveStartupMode(installation.version) }) {
|
|
70
|
+
const test = config.test ??= {};
|
|
71
|
+
const fail = (message) => {
|
|
72
|
+
throw new Error(`nuxt-vitest-shared-app: ${message}`);
|
|
73
|
+
};
|
|
74
|
+
if (test.environment !== "nuxt") fail("set test.environment to \"nuxt\"; Vitest projects and other environments are not supported.");
|
|
75
|
+
if (test.browser?.enabled) fail("Vitest browser mode is not supported.");
|
|
76
|
+
if (test.isolate !== false) fail("set test.isolate to false so test files share the Nuxt app.");
|
|
77
|
+
if (test.pool !== void 0 && test.pool !== "threads" && test.pool !== "forks") fail(`test.pool must be "threads" or "forks", got ${JSON.stringify(test.pool)}.`);
|
|
78
|
+
const sequence = test.sequence ??= {};
|
|
79
|
+
if (sequence.setupFiles === "parallel") fail("test.sequence.setupFiles must be \"list\".");
|
|
80
|
+
sequence.setupFiles = "list";
|
|
81
|
+
if (sequence.hooks === "parallel") fail("test.sequence.hooks must be \"stack\" or \"list\".");
|
|
82
|
+
if (sequence.concurrent) fail("test.sequence.concurrent is not supported.");
|
|
83
|
+
const root = config.root ? resolve(config.root) : process.cwd();
|
|
84
|
+
const setupFiles = toArray(test.setupFiles);
|
|
85
|
+
const upstreamEntry = join(installation.runtimeDir, "entry.mjs");
|
|
86
|
+
const entryIndexes = setupFiles.flatMap((file, index) => samePath(isAbsolute(file) ? file : resolve(root, file), upstreamEntry) ? [index] : []);
|
|
87
|
+
if (entryIndexes.length !== 1) fail(`expected exactly one @nuxt/test-utils ${installation.version} runtime entry (${upstreamEntry}) in test.setupFiles, found ${entryIndexes.length}. Setup files: ${setupFiles.join(", ") || "(none)"}.`);
|
|
88
|
+
const entryIndex = entryIndexes[0];
|
|
89
|
+
test.setupFiles = [
|
|
90
|
+
mode === "adapter" ? files.adapterEntry : setupFiles[entryIndex],
|
|
91
|
+
files.lifecycle,
|
|
92
|
+
...setupFiles.filter((_, index) => index !== entryIndex)
|
|
93
|
+
];
|
|
94
|
+
if (mode === "adapter") (config.plugins ??= []).push(setupNuxtPlugin(installation));
|
|
95
|
+
const deps = (test.server ??= {}).deps ??= {};
|
|
96
|
+
if (deps.inline !== true) deps.inline = [...deps.inline ?? [], /[\\/]node_modules[\\/]nuxt-vitest-shared-app[\\/]/];
|
|
97
|
+
if (options.silenceSuspenseInfo) {
|
|
98
|
+
const onConsoleLog = test.onConsoleLog;
|
|
99
|
+
test.onConsoleLog = (log, ...args) => {
|
|
100
|
+
if (log.includes(SUSPENSE_INFO)) return false;
|
|
101
|
+
return onConsoleLog?.(log, ...args);
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
return config;
|
|
105
|
+
}
|
|
106
|
+
function toArray(value) {
|
|
107
|
+
return value === void 0 ? [] : Array.isArray(value) ? value : [value];
|
|
108
|
+
}
|
|
109
|
+
function samePath(left, right) {
|
|
110
|
+
return realpath(left) === realpath(right);
|
|
111
|
+
}
|
|
112
|
+
function realpath(path) {
|
|
113
|
+
try {
|
|
114
|
+
return realpathSync(path);
|
|
115
|
+
} catch {
|
|
116
|
+
return resolve(path);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
//#endregion
|
|
120
|
+
//#region src/config/define.ts
|
|
121
|
+
/**
|
|
122
|
+
* Creates the config function; `mode` bypasses the version policy and is used only by this library's own tests.
|
|
123
|
+
*/
|
|
124
|
+
function createSharedNuxtVitestConfig(config, options, libraryUrl, mode) {
|
|
125
|
+
const extension = libraryUrl.endsWith(".ts") ? ".ts" : ".mjs";
|
|
126
|
+
const files = {
|
|
127
|
+
adapterEntry: fileURLToPath(new URL(`./adapter/entry${extension}`, libraryUrl)),
|
|
128
|
+
lifecycle: fileURLToPath(new URL(`./runtime/lifecycle${extension}`, libraryUrl))
|
|
129
|
+
};
|
|
130
|
+
const resolveConfig = defineVitestConfig(config);
|
|
131
|
+
return async (...args) => applySharedApp(await resolveConfig(...args), {
|
|
132
|
+
options,
|
|
133
|
+
installation: locateTestUtils(libraryUrl),
|
|
134
|
+
files,
|
|
135
|
+
mode
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
//#endregion
|
|
139
|
+
//#region src/config.ts
|
|
140
|
+
/**
|
|
141
|
+
* Wraps `defineVitestConfig` from `@nuxt/test-utils/config` so all test files of a Vitest worker share one Nuxt app.
|
|
142
|
+
*
|
|
143
|
+
* It starts Nuxt once per environment, adds the library lifecycle (readiness check, per-test cleanup, rejection of concurrent tests), and validates the settings a shared app relies on.
|
|
144
|
+
*
|
|
145
|
+
* @throws When the config uses an unsupported setting or the installed `@nuxt/test-utils` version was not tested.
|
|
146
|
+
*/
|
|
147
|
+
function defineSharedNuxtVitestConfig(config, options = {}) {
|
|
148
|
+
return createSharedNuxtVitestConfig(config, options, import.meta.url);
|
|
149
|
+
}
|
|
150
|
+
//#endregion
|
|
151
|
+
export { defineSharedNuxtVitestConfig };
|
package/dist/dom.d.mts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region src/dom.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Stubs the global `IntersectionObserver` with one that reports every observed target as visible immediately in `observe()`.
|
|
4
|
+
*
|
|
5
|
+
* Targets are elements, plus the environment's own `window` and `document`, which are measured as `document.documentElement`.
|
|
6
|
+
* Any other target throws a `TypeError`, as a browser does.
|
|
7
|
+
*
|
|
8
|
+
* happy-dom and the `@nuxt/test-utils` mock never call the observer callback, so components that wait for visibility never render their content.
|
|
9
|
+
* The stub is installed with `vi.stubGlobal`, so the per-test cleanup removes it.
|
|
10
|
+
*/
|
|
11
|
+
declare function stubVisibleIntersectionObserver(): void;
|
|
12
|
+
//#endregion
|
|
13
|
+
export { stubVisibleIntersectionObserver };
|
package/dist/dom.mjs
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { vi } from "vitest";
|
|
2
|
+
//#region src/dom.ts
|
|
3
|
+
/**
|
|
4
|
+
* Stubs the global `IntersectionObserver` with one that reports every observed target as visible immediately in `observe()`.
|
|
5
|
+
*
|
|
6
|
+
* Targets are elements, plus the environment's own `window` and `document`, which are measured as `document.documentElement`.
|
|
7
|
+
* Any other target throws a `TypeError`, as a browser does.
|
|
8
|
+
*
|
|
9
|
+
* happy-dom and the `@nuxt/test-utils` mock never call the observer callback, so components that wait for visibility never render their content.
|
|
10
|
+
* The stub is installed with `vi.stubGlobal`, so the per-test cleanup removes it.
|
|
11
|
+
*/
|
|
12
|
+
function stubVisibleIntersectionObserver() {
|
|
13
|
+
vi.stubGlobal("IntersectionObserver", VisibleIntersectionObserver);
|
|
14
|
+
}
|
|
15
|
+
var VisibleIntersectionObserver = class {
|
|
16
|
+
root;
|
|
17
|
+
rootMargin;
|
|
18
|
+
scrollMargin = "0px";
|
|
19
|
+
thresholds;
|
|
20
|
+
#callback;
|
|
21
|
+
constructor(callback, options = {}) {
|
|
22
|
+
this.#callback = callback;
|
|
23
|
+
this.root = options.root ?? null;
|
|
24
|
+
this.rootMargin = options.rootMargin ?? "0px";
|
|
25
|
+
const threshold = options.threshold ?? 0;
|
|
26
|
+
this.thresholds = Array.isArray(threshold) ? threshold : [threshold];
|
|
27
|
+
}
|
|
28
|
+
observe(target) {
|
|
29
|
+
const rect = measure(target);
|
|
30
|
+
const entry = {
|
|
31
|
+
target,
|
|
32
|
+
isIntersecting: true,
|
|
33
|
+
intersectionRatio: 1,
|
|
34
|
+
boundingClientRect: rect,
|
|
35
|
+
intersectionRect: rect,
|
|
36
|
+
rootBounds: null,
|
|
37
|
+
time: performance.now()
|
|
38
|
+
};
|
|
39
|
+
this.#callback([entry], this);
|
|
40
|
+
}
|
|
41
|
+
unobserve() {}
|
|
42
|
+
disconnect() {}
|
|
43
|
+
takeRecords() {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
function measure(target) {
|
|
48
|
+
const unknownTarget = target;
|
|
49
|
+
if (unknownTarget === window || unknownTarget === document) return document.documentElement.getBoundingClientRect();
|
|
50
|
+
if (typeof target?.getBoundingClientRect !== "function") throw new TypeError("Failed to execute 'observe' on 'IntersectionObserver': parameter 1 is not of type 'Element'.");
|
|
51
|
+
return target.getBoundingClientRect();
|
|
52
|
+
}
|
|
53
|
+
//#endregion
|
|
54
|
+
export { stubVisibleIntersectionObserver };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
//#region src/overrides.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Registry of Nuxt auto-imports that tests may override.
|
|
4
|
+
*
|
|
5
|
+
* Augment it once in the project setup file, mapping each import name to its type:
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* declare module "nuxt-vitest-shared-app" {
|
|
9
|
+
* interface TestNuxtImports {
|
|
10
|
+
* useRoute: typeof useRoute
|
|
11
|
+
* }
|
|
12
|
+
* }
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
interface TestNuxtImports {}
|
|
16
|
+
/** Name of a registered overridable auto-import. */
|
|
17
|
+
type TestNuxtImportName = keyof TestNuxtImports & string;
|
|
18
|
+
/** Route object accepted by `overrideNuxtRoute`, derived from the registered `useRoute` type. */
|
|
19
|
+
type TestNuxtRoute = TestNuxtImports extends {
|
|
20
|
+
useRoute: (...args: never[]) => infer Route;
|
|
21
|
+
} ? Partial<Route> : never;
|
|
22
|
+
/**
|
|
23
|
+
* Creates a `mockNuxtImport` factory that installs a stable overridable wrapper for `name`.
|
|
24
|
+
*
|
|
25
|
+
* The wrapper calls the implementation set with `overrideNuxtImport` for the current test, or otherwise `fallback` if given, or the original import, preserving `this` and arguments.
|
|
26
|
+
* Unlike `overrideNuxtImport` in `beforeEach`, `fallback` also applies while the app starts, e.g. to auto-imports called by plugins.
|
|
27
|
+
* Outside the Vitest `nuxt` environment, e.g. in a file with `@vitest-environment node`, the factory returns `fallback` or the original.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* mockNuxtImport("useRoute", overridableNuxtImport("useRoute"))
|
|
32
|
+
* mockNuxtImport("useUser", overridableNuxtImport("useUser", () => ref(null)))
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
declare function overridableNuxtImport<Name extends TestNuxtImportName>(name: Name, fallback?: TestNuxtImports[Name]): (original: TestNuxtImports[Name]) => TestNuxtImports[Name];
|
|
36
|
+
/**
|
|
37
|
+
* Overrides a registered Nuxt auto-import for the current test.
|
|
38
|
+
*
|
|
39
|
+
* Overrides are cleared after each test.
|
|
40
|
+
*
|
|
41
|
+
* @throws When no wrapper for `name` was installed with `mockNuxtImport(name, overridableNuxtImport(name))`.
|
|
42
|
+
*/
|
|
43
|
+
declare function overrideNuxtImport<Name extends TestNuxtImportName>(name: Name, implementation: TestNuxtImports[Name]): void;
|
|
44
|
+
/**
|
|
45
|
+
* Makes the registered `useRoute` wrapper return exactly `route` for the current test.
|
|
46
|
+
*
|
|
47
|
+
* The object is not merged with the real route, does not navigate or sync the real router, and is read by components only when they call `useRoute`, so set it before mounting.
|
|
48
|
+
*/
|
|
49
|
+
declare function overrideNuxtRoute(route: TestNuxtRoute): void;
|
|
50
|
+
//#endregion
|
|
51
|
+
//#region src/resets.d.ts
|
|
52
|
+
/**
|
|
53
|
+
* Registers a project reset that runs after each test, in registration order, and is awaited.
|
|
54
|
+
*
|
|
55
|
+
* Registering the same `key` again replaces the callback and keeps its position, so a setup file that runs for every test file does not accumulate registrations.
|
|
56
|
+
*
|
|
57
|
+
* Outside the Vitest `nuxt` environment, e.g. in a file with `@vitest-environment node`, nothing is registered.
|
|
58
|
+
*
|
|
59
|
+
* @returns A function that removes this registration, unless it has already been replaced.
|
|
60
|
+
*/
|
|
61
|
+
declare function registerNuxtTestReset(key: string, reset: () => unknown): () => void;
|
|
62
|
+
//#endregion
|
|
63
|
+
//#region src/worker-state.d.ts
|
|
64
|
+
/**
|
|
65
|
+
* Returns state bound to the current Nuxt environment, creating it with `create` on first use.
|
|
66
|
+
*
|
|
67
|
+
* Use it for project helpers whose state must survive module re-evaluation between test files, such as a test API client shared with the started app.
|
|
68
|
+
*/
|
|
69
|
+
declare function getOrCreateWorkerState<T>(name: string, create: () => T): T;
|
|
70
|
+
//#endregion
|
|
71
|
+
export { type TestNuxtImportName, type TestNuxtImports, type TestNuxtRoute, getOrCreateWorkerState, overridableNuxtImport, overrideNuxtImport, overrideNuxtRoute, registerNuxtTestReset };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { n as isNuxtEnvironment, t as getEnvironmentStore } from "./store-Dklvd4X3.mjs";
|
|
2
|
+
//#region src/overrides.ts
|
|
3
|
+
const WRAPPER_MARK = Symbol.for("nuxt-vitest-shared-app:wrapper");
|
|
4
|
+
/**
|
|
5
|
+
* Creates a `mockNuxtImport` factory that installs a stable overridable wrapper for `name`.
|
|
6
|
+
*
|
|
7
|
+
* The wrapper calls the implementation set with `overrideNuxtImport` for the current test, or otherwise `fallback` if given, or the original import, preserving `this` and arguments.
|
|
8
|
+
* Unlike `overrideNuxtImport` in `beforeEach`, `fallback` also applies while the app starts, e.g. to auto-imports called by plugins.
|
|
9
|
+
* Outside the Vitest `nuxt` environment, e.g. in a file with `@vitest-environment node`, the factory returns `fallback` or the original.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* mockNuxtImport("useRoute", overridableNuxtImport("useRoute"))
|
|
14
|
+
* mockNuxtImport("useUser", overridableNuxtImport("useUser", () => ref(null)))
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
function overridableNuxtImport(name, fallback) {
|
|
18
|
+
return (original) => {
|
|
19
|
+
if (!isNuxtEnvironment()) return fallback ?? original;
|
|
20
|
+
const store = getEnvironmentStore();
|
|
21
|
+
const existing = store.imports.get(name);
|
|
22
|
+
const defaultImplementation = unwrap(fallback ?? original);
|
|
23
|
+
if (existing) {
|
|
24
|
+
existing.defaultImplementation = defaultImplementation;
|
|
25
|
+
return existing.wrapper;
|
|
26
|
+
}
|
|
27
|
+
const entry = {
|
|
28
|
+
defaultImplementation,
|
|
29
|
+
wrapper(...args) {
|
|
30
|
+
return (store.overrides.get(name) ?? entry.defaultImplementation).apply(this, args);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
Object.defineProperty(entry.wrapper, WRAPPER_MARK, { value: entry });
|
|
34
|
+
store.imports.set(name, entry);
|
|
35
|
+
return entry.wrapper;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function unwrap(fn) {
|
|
39
|
+
const entry = fn[WRAPPER_MARK];
|
|
40
|
+
return entry ? entry.defaultImplementation : fn;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Overrides a registered Nuxt auto-import for the current test.
|
|
44
|
+
*
|
|
45
|
+
* Overrides are cleared after each test.
|
|
46
|
+
*
|
|
47
|
+
* @throws When no wrapper for `name` was installed with `mockNuxtImport(name, overridableNuxtImport(name))`.
|
|
48
|
+
*/
|
|
49
|
+
function overrideNuxtImport(name, implementation) {
|
|
50
|
+
const store = getEnvironmentStore();
|
|
51
|
+
if (!store.imports.has(name)) throw new Error(`nuxt-vitest-shared-app: "${name}" is not overridable. Add mockNuxtImport("${name}", overridableNuxtImport("${name}")) to the project setup file.`);
|
|
52
|
+
store.overrides.set(name, implementation);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Makes the registered `useRoute` wrapper return exactly `route` for the current test.
|
|
56
|
+
*
|
|
57
|
+
* The object is not merged with the real route, does not navigate or sync the real router, and is read by components only when they call `useRoute`, so set it before mounting.
|
|
58
|
+
*/
|
|
59
|
+
function overrideNuxtRoute(route) {
|
|
60
|
+
overrideNuxtImport("useRoute", (() => route));
|
|
61
|
+
}
|
|
62
|
+
//#endregion
|
|
63
|
+
//#region src/resets.ts
|
|
64
|
+
/**
|
|
65
|
+
* Registers a project reset that runs after each test, in registration order, and is awaited.
|
|
66
|
+
*
|
|
67
|
+
* Registering the same `key` again replaces the callback and keeps its position, so a setup file that runs for every test file does not accumulate registrations.
|
|
68
|
+
*
|
|
69
|
+
* Outside the Vitest `nuxt` environment, e.g. in a file with `@vitest-environment node`, nothing is registered.
|
|
70
|
+
*
|
|
71
|
+
* @returns A function that removes this registration, unless it has already been replaced.
|
|
72
|
+
*/
|
|
73
|
+
function registerNuxtTestReset(key, reset) {
|
|
74
|
+
if (!isNuxtEnvironment()) return () => {};
|
|
75
|
+
const store = getEnvironmentStore();
|
|
76
|
+
const registration = { reset };
|
|
77
|
+
store.resets.set(key, registration);
|
|
78
|
+
return () => {
|
|
79
|
+
if (store.resets.get(key) === registration) store.resets.delete(key);
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
//#endregion
|
|
83
|
+
//#region src/worker-state.ts
|
|
84
|
+
/**
|
|
85
|
+
* Returns state bound to the current Nuxt environment, creating it with `create` on first use.
|
|
86
|
+
*
|
|
87
|
+
* Use it for project helpers whose state must survive module re-evaluation between test files, such as a test API client shared with the started app.
|
|
88
|
+
*/
|
|
89
|
+
function getOrCreateWorkerState(name, create) {
|
|
90
|
+
const { workerState } = getEnvironmentStore();
|
|
91
|
+
if (!workerState.has(name)) workerState.set(name, create());
|
|
92
|
+
return workerState.get(name);
|
|
93
|
+
}
|
|
94
|
+
//#endregion
|
|
95
|
+
export { getOrCreateWorkerState, overridableNuxtImport, overrideNuxtImport, overrideNuxtRoute, registerNuxtTestReset };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { n as isNuxtEnvironment, t as getEnvironmentStore } from "../store-Dklvd4X3.mjs";
|
|
2
|
+
import { t as getStartupError } from "../startup-error-C4H64PiD.mjs";
|
|
3
|
+
import { beforeAll, beforeEach, vi } from "vitest";
|
|
4
|
+
import { disableAutoUnmount, enableAutoUnmount, flushPromises } from "@vue/test-utils";
|
|
5
|
+
import { nextTick } from "vue";
|
|
6
|
+
//#region src/runtime/router.ts
|
|
7
|
+
/**
|
|
8
|
+
* Returns the router instance of the app.
|
|
9
|
+
*
|
|
10
|
+
* Unlike the auto-imported `useRouter`/`useRoute`/`navigateTo`, it cannot be replaced by a project's `mockNuxtImport`.
|
|
11
|
+
*/
|
|
12
|
+
function getRealRouter(nuxtApp) {
|
|
13
|
+
return nuxtApp.$router;
|
|
14
|
+
}
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/runtime/cleanup.ts
|
|
17
|
+
/** Thrown after cleanup when one or more steps failed; `errors` holds every original error. */
|
|
18
|
+
var CleanupError = class extends AggregateError {
|
|
19
|
+
failures;
|
|
20
|
+
constructor(failures) {
|
|
21
|
+
super(failures.map((failure) => failure.error), `nuxt-vitest-shared-app: cleanup failed.\n${failures.map((failure) => `- ${failure.step}: ${errorMessage(failure.error)}`).join("\n")}`);
|
|
22
|
+
this.name = "CleanupError";
|
|
23
|
+
this.failures = failures;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Restores documented Nuxt and test-owned state after a test.
|
|
28
|
+
*
|
|
29
|
+
* Every step runs even if an earlier independent step failed.
|
|
30
|
+
*
|
|
31
|
+
* @throws {CleanupError} With all step failures after all steps have run.
|
|
32
|
+
*/
|
|
33
|
+
async function cleanupAfterTest(store, baseline, unmountWrappers) {
|
|
34
|
+
const failures = [];
|
|
35
|
+
async function step(name, run) {
|
|
36
|
+
try {
|
|
37
|
+
await run();
|
|
38
|
+
} catch (error) {
|
|
39
|
+
failures.push({
|
|
40
|
+
step: name,
|
|
41
|
+
error
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
await step("unmount wrappers", () => unmountWrappers?.());
|
|
46
|
+
await step("flush promises", async () => {
|
|
47
|
+
await nextTick();
|
|
48
|
+
await flushPromises();
|
|
49
|
+
});
|
|
50
|
+
await step("restore route", async () => {
|
|
51
|
+
const { useNuxtApp } = await importNuxt();
|
|
52
|
+
const router = getRealRouter(useNuxtApp());
|
|
53
|
+
if (router.currentRoute.value.fullPath !== baseline.route) {
|
|
54
|
+
await router.replace(baseline.route);
|
|
55
|
+
const route = router.currentRoute.value.fullPath;
|
|
56
|
+
if (route !== baseline.route) throw new Error(`route is ${route} after navigating back to ${baseline.route}`);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
await step("clear overrides", () => store.overrides.clear());
|
|
60
|
+
await step("clearError", async () => {
|
|
61
|
+
const { clearError } = await importNuxt();
|
|
62
|
+
await clearError();
|
|
63
|
+
});
|
|
64
|
+
await step("clearNuxtData", async () => {
|
|
65
|
+
const { clearNuxtData } = await importNuxt();
|
|
66
|
+
clearNuxtData();
|
|
67
|
+
});
|
|
68
|
+
await step("clearNuxtState", async () => {
|
|
69
|
+
const { clearNuxtState } = await importNuxt();
|
|
70
|
+
clearNuxtState(void 0, { reset: false });
|
|
71
|
+
});
|
|
72
|
+
for (const [key, { reset }] of store.resets) await step(`project reset "${key}"`, reset);
|
|
73
|
+
await step("clear localStorage", () => localStorage.clear());
|
|
74
|
+
await step("clear sessionStorage", () => sessionStorage.clear());
|
|
75
|
+
await step("expire cookies", expireCookies);
|
|
76
|
+
for (const [name, run] of RUNNER_STEPS) await step(name, run);
|
|
77
|
+
await step("remove body children", () => {
|
|
78
|
+
for (const child of Array.from(document.body.children)) if (!baseline.bodyChildren.has(child)) child.remove();
|
|
79
|
+
});
|
|
80
|
+
if (failures.length) throw new CleanupError(failures);
|
|
81
|
+
}
|
|
82
|
+
function importNuxt() {
|
|
83
|
+
return import("nuxt/app");
|
|
84
|
+
}
|
|
85
|
+
const RUNNER_STEPS = [
|
|
86
|
+
["clear timers", () => vi.clearAllTimers()],
|
|
87
|
+
["use real timers", () => vi.useRealTimers()],
|
|
88
|
+
["unstub envs", () => vi.unstubAllEnvs()],
|
|
89
|
+
["unstub globals", () => vi.unstubAllGlobals()]
|
|
90
|
+
];
|
|
91
|
+
/** Restores timers, envs and globals as far as possible, ignoring failures, for an environment that is already contaminated. */
|
|
92
|
+
function restoreTestRunner() {
|
|
93
|
+
for (const [, run] of RUNNER_STEPS) try {
|
|
94
|
+
run();
|
|
95
|
+
} catch {}
|
|
96
|
+
}
|
|
97
|
+
/** Expires every cookie visible in `document.cookie` for path `/` of the current origin. */
|
|
98
|
+
function expireCookies() {
|
|
99
|
+
for (const cookie of document.cookie.split(";")) {
|
|
100
|
+
const name = cookie.split("=", 1)[0].trim();
|
|
101
|
+
if (name) document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function errorMessage(error) {
|
|
105
|
+
return error instanceof Error ? error.message : String(error);
|
|
106
|
+
}
|
|
107
|
+
//#endregion
|
|
108
|
+
//#region src/runtime/readiness.ts
|
|
109
|
+
/**
|
|
110
|
+
* Checks that `nuxtApp` is mounted in `document`, whichever entry started it.
|
|
111
|
+
*
|
|
112
|
+
* The global Nuxt context may still hold an app started in another environment of the same worker, so existence alone is not enough.
|
|
113
|
+
*
|
|
114
|
+
* @throws When the app is missing, unmounted, or mounted in another document.
|
|
115
|
+
*/
|
|
116
|
+
function assertAppRunning(nuxtApp, document) {
|
|
117
|
+
const vueApp = nuxtApp?.vueApp;
|
|
118
|
+
const container = vueApp?._container;
|
|
119
|
+
const problem = !nuxtApp ? "no Nuxt app exists" : !container || container.__vue_app__ !== vueApp ? "the Nuxt app is not mounted" : container.ownerDocument !== document || !container.isConnected ? "the Nuxt app is mounted in another document" : void 0;
|
|
120
|
+
if (problem) throw new Error(`nuxt-vitest-shared-app: the Nuxt app is not running in the current test environment (${problem}). Make sure the config is created with defineSharedNuxtVitestConfig and the Nuxt startup succeeded.`);
|
|
121
|
+
return nuxtApp;
|
|
122
|
+
}
|
|
123
|
+
//#endregion
|
|
124
|
+
//#region src/runtime/lifecycle.ts
|
|
125
|
+
if (isNuxtEnvironment()) installLifecycle();
|
|
126
|
+
function installLifecycle() {
|
|
127
|
+
let unmountWrappers;
|
|
128
|
+
disableAutoUnmount();
|
|
129
|
+
enableAutoUnmount((unmount) => {
|
|
130
|
+
unmountWrappers = unmount;
|
|
131
|
+
});
|
|
132
|
+
beforeAll(async () => {
|
|
133
|
+
const store = getEnvironmentStore();
|
|
134
|
+
assertNotContaminated(store);
|
|
135
|
+
const { tryUseNuxtApp } = await import("nuxt/app");
|
|
136
|
+
const nuxtApp = assertAppRunning(tryUseNuxtApp(), document);
|
|
137
|
+
if (store.startupFailure) throw new Error("nuxt-vitest-shared-app: Nuxt startup failed earlier in this environment.", { cause: store.startupFailure });
|
|
138
|
+
if (!store.baseline) {
|
|
139
|
+
const startupError = getStartupError(nuxtApp);
|
|
140
|
+
if (startupError) {
|
|
141
|
+
store.startupFailure = startupError;
|
|
142
|
+
throw startupError;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
store.baseline ??= {
|
|
146
|
+
route: getRealRouter(nuxtApp).currentRoute.value.fullPath,
|
|
147
|
+
bodyChildren: new Set(document.body.children)
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
beforeEach(({ task, signal, onTestFinished }) => {
|
|
151
|
+
const store = getEnvironmentStore();
|
|
152
|
+
assertNotContaminated(store);
|
|
153
|
+
if (task.concurrent) throw new Error("nuxt-vitest-shared-app: concurrent tests are not supported, because all tests of a worker share one Nuxt app. Remove test.concurrent / describe.concurrent.");
|
|
154
|
+
const onAbort = () => {
|
|
155
|
+
store.contamination ??= new Error(store.cleanupPending ? "nuxt-vitest-shared-app: the cleanup after an earlier test did not finish in time." : "nuxt-vitest-shared-app: an earlier test was aborted, e.g. by a timeout, and its work may still be running.", { cause: signal.reason });
|
|
156
|
+
restoreTestRunner();
|
|
157
|
+
};
|
|
158
|
+
if (signal.aborted) onAbort();
|
|
159
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
160
|
+
onTestFinished(async () => {
|
|
161
|
+
if (!store.baseline) return;
|
|
162
|
+
store.cleanupPending = true;
|
|
163
|
+
try {
|
|
164
|
+
await cleanupAfterTest(store, store.baseline, unmountWrappers);
|
|
165
|
+
} catch (error) {
|
|
166
|
+
store.contamination ??= error;
|
|
167
|
+
throw error;
|
|
168
|
+
} finally {
|
|
169
|
+
store.cleanupPending = false;
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
/** @throws When a failed or unfinished cleanup left the environment in an unknown state. */
|
|
175
|
+
function assertNotContaminated(store) {
|
|
176
|
+
if (store.cleanupPending) store.contamination ??= /* @__PURE__ */ new Error("nuxt-vitest-shared-app: the cleanup after an earlier test did not finish in time.");
|
|
177
|
+
if (store.contamination) throw new Error("nuxt-vitest-shared-app: this environment is contaminated by an earlier test and cannot run more tests.", { cause: store.contamination });
|
|
178
|
+
}
|
|
179
|
+
//#endregion
|
|
180
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
//#region src/adapter/setup-nuxt-id.ts
|
|
2
|
+
/**
|
|
3
|
+
* Module id imported by the adapter entry to load the internal `setupNuxt`.
|
|
4
|
+
*
|
|
5
|
+
* The `#` prefix keeps it inlined by the `@nuxt/test-utils` config, so the module runner resolves it through the adapter plugin instead of importing it natively.
|
|
6
|
+
*/
|
|
7
|
+
const SETUP_NUXT_ID = "#nuxt-vitest-shared-app/setup-nuxt";
|
|
8
|
+
//#endregion
|
|
9
|
+
export { SETUP_NUXT_ID as t };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region src/runtime/startup-error.ts
|
|
2
|
+
/**
|
|
3
|
+
* Returns the error that Nuxt caught while starting `nuxtApp`, wrapped with the Nuxt error as its cause.
|
|
4
|
+
*
|
|
5
|
+
* Nuxt catches plugin and mount errors during startup, stores them in `payload.error`, and continues with a partially initialized app.
|
|
6
|
+
*/
|
|
7
|
+
function getStartupError(nuxtApp) {
|
|
8
|
+
const error = nuxtApp.payload.error;
|
|
9
|
+
if (!error) return void 0;
|
|
10
|
+
return new Error(`nuxt-vitest-shared-app: Nuxt startup failed: ${error.message}`, { cause: error });
|
|
11
|
+
}
|
|
12
|
+
//#endregion
|
|
13
|
+
export { getStartupError as t };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
//#region src/store.ts
|
|
2
|
+
const STORE_KEY = Symbol.for("nuxt-vitest-shared-app:store");
|
|
3
|
+
/** Whether the current test file runs in the Vitest `nuxt` environment. */
|
|
4
|
+
function isNuxtEnvironment() {
|
|
5
|
+
return !!globalThis.window?.__NUXT_VITEST_ENVIRONMENT__;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Returns the store bound to the current environment `window`, creating it on first use.
|
|
9
|
+
*
|
|
10
|
+
* @throws Outside the Vitest `nuxt` environment.
|
|
11
|
+
*/
|
|
12
|
+
function getEnvironmentStore() {
|
|
13
|
+
const win = globalThis.window;
|
|
14
|
+
if (!win?.__NUXT_VITEST_ENVIRONMENT__) throw new Error("nuxt-vitest-shared-app: not running in the Vitest \"nuxt\" environment.");
|
|
15
|
+
const existing = win[STORE_KEY];
|
|
16
|
+
if (existing && existing.window === win && existing.document === win.document) return existing;
|
|
17
|
+
const store = {
|
|
18
|
+
window: win,
|
|
19
|
+
document: win.document,
|
|
20
|
+
imports: /* @__PURE__ */ new Map(),
|
|
21
|
+
overrides: /* @__PURE__ */ new Map(),
|
|
22
|
+
resets: /* @__PURE__ */ new Map(),
|
|
23
|
+
workerState: /* @__PURE__ */ new Map()
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(win, STORE_KEY, {
|
|
26
|
+
value: store,
|
|
27
|
+
configurable: true
|
|
28
|
+
});
|
|
29
|
+
return store;
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
export { isNuxtEnvironment as n, getEnvironmentStore as t };
|
package/package.json
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nuxt-vitest-shared-app",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Run Nuxt component tests in Vitest on one Nuxt app per worker, with deterministic per-test cleanup and typed auto-import overrides.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"nuxt",
|
|
7
|
+
"nuxt-test-utils",
|
|
8
|
+
"testing",
|
|
9
|
+
"vitest",
|
|
10
|
+
"vue"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/IlyaSemenov/nuxt-vitest-shared-app#readme",
|
|
13
|
+
"bugs": "https://github.com/IlyaSemenov/nuxt-vitest-shared-app/issues",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"author": "Ilya Semenov",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/IlyaSemenov/nuxt-vitest-shared-app.git"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"README.md",
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"type": "module",
|
|
25
|
+
"types": "./dist/index.d.mts",
|
|
26
|
+
"imports": {
|
|
27
|
+
"#src/*": "./src/*.ts"
|
|
28
|
+
},
|
|
29
|
+
"exports": {
|
|
30
|
+
".": "./dist/index.mjs",
|
|
31
|
+
"./config": "./dist/config.mjs",
|
|
32
|
+
"./dom": "./dist/dom.mjs",
|
|
33
|
+
"./package.json": "./package.json"
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public",
|
|
37
|
+
"provenance": true
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsdown",
|
|
41
|
+
"lint": "oxlint --fix && oxfmt",
|
|
42
|
+
"lint:check": "oxlint && oxfmt --check",
|
|
43
|
+
"prepare": "lefthook install",
|
|
44
|
+
"prepublishOnly": "bun run build",
|
|
45
|
+
"test": "vitest run",
|
|
46
|
+
"test:consumer": "bun run build && bun tests/consumer/run.ts",
|
|
47
|
+
"types": "tsc && tsc -p tests/tsconfig.json && tsc -p tests/types/tsconfig.route-unregistered.json"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@arethetypeswrong/core": "^0.18.5",
|
|
51
|
+
"@changesets/cli": "^2.31.1",
|
|
52
|
+
"@nuxt/test-utils": "4.3.2",
|
|
53
|
+
"@types/node": "^24.0.0",
|
|
54
|
+
"@vue/test-utils": "^2.4.8",
|
|
55
|
+
"@vueuse/core": "^15.0.0",
|
|
56
|
+
"happy-dom": "^20.14.5",
|
|
57
|
+
"nuxt": "~4.5.2",
|
|
58
|
+
"oxfmt": "^0.67.0",
|
|
59
|
+
"oxlint": "^1.82.0",
|
|
60
|
+
"publint": "^0.3.22",
|
|
61
|
+
"tsdown": "^0.22.14",
|
|
62
|
+
"typescript": "^7.0.2",
|
|
63
|
+
"vitest": "^5.0.2",
|
|
64
|
+
"vue": "^3.5.43"
|
|
65
|
+
},
|
|
66
|
+
"peerDependencies": {
|
|
67
|
+
"@nuxt/test-utils": "~4.3.0",
|
|
68
|
+
"@vue/test-utils": "^2.4.2",
|
|
69
|
+
"nuxt": "^4.4.2",
|
|
70
|
+
"vitest": "^4.1.0 || ^5.0.0",
|
|
71
|
+
"vue": "^3.5.30"
|
|
72
|
+
},
|
|
73
|
+
"packageManager": "bun@1.4.2"
|
|
74
|
+
}
|