vitest-auto-spy 3.10.0 → 3.12.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/AGENTS.md +48 -2
- package/README.md +83 -6
- package/dist/angular.d.ts +40 -1
- package/dist/angular.js +61 -1
- package/dist/bun-angular.d.ts +1 -1
- package/dist/bun-angular.js +1 -1
- package/dist/bun.d.ts +83 -1
- package/dist/bun.js +1 -1
- package/dist/{chunk-2U3J65A4.js → chunk-FKAXOKP5.js} +49 -1
- package/dist/eslint-plugin.cjs +178 -80
- package/dist/eslint-plugin.js +178 -80
- package/dist/index.d.ts +1 -1
- package/dist/index.js +49 -1
- package/dist/node.cjs +50 -0
- package/dist/node.d.cts +2 -0
- package/dist/node.d.ts +1 -1
- package/dist/node.js +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/svelte.d.ts +1 -1
- package/dist/svelte.js +1 -1
- package/dist/vue.d.ts +1 -1
- package/dist/vue.js +1 -1
- package/package.json +1 -1
- package/skills/vitest-auto-spy/SKILL.md +5 -2
package/AGENTS.md
CHANGED
|
@@ -77,7 +77,8 @@ Do you have a real class at runtime?
|
|
|
77
77
|
├── yes, and reads chain (a.b.c()) → mockDeep<T>(overrides?) → DeepMockProxy<T>
|
|
78
78
|
├── yes, and CALLS chain (a.b().c()) → mockDeep<T>({}, { selfReturning: true })
|
|
79
79
|
└── no, it is only READ (DTO, config, route snapshot)
|
|
80
|
-
|
|
80
|
+
├── one spec, a couple of fields → createMock<T>(partial?) → T (no spies)
|
|
81
|
+
└── many specs, one shared model → createFixtureFactory<T>(defaults) → (overrides?) => T
|
|
81
82
|
|
|
82
83
|
One standalone function? → createFunctionSpy<Fn>('name')
|
|
83
84
|
Code under test does `new Foo()`? → a real class? createSpyClass(Foo)
|
|
@@ -97,6 +98,16 @@ noise scales with the number of them.
|
|
|
97
98
|
`createMock<T>()` is the one to reach for on data shapes — it returns a plain `T`, so it satisfies a
|
|
98
99
|
`no-type-assertion` lint rule without an `eslint-disable` on every fixture.
|
|
99
100
|
|
|
101
|
+
**`createFixture<T>(defaults, overrides?)` / `createFixtureFactory<T>(defaults)` are for the model
|
|
102
|
+
that more than one spec builds.** The difference from `createMock` is the `defaults` argument: it is
|
|
103
|
+
a **complete** `T`, checked in full, in one place — so a field the model dropped fails there instead
|
|
104
|
+
of in eight copies of a hundred-line literal. Overrides are deep-partial-checked and merge leaf by
|
|
105
|
+
leaf; an overridden array replaces the default one. Every call hands back a fresh object and the
|
|
106
|
+
defaults are copied at build time, which is what keeps one test's mutation out of the next test —
|
|
107
|
+
across files, under `isolate: false`. The copy is deep through plain objects and arrays only: a
|
|
108
|
+
`Date`, a `Map` or a class instance travels by reference, because rebuilding it would strip its
|
|
109
|
+
prototype. Defaults that are a class instance with getters go through `withOverrides()` first.
|
|
110
|
+
|
|
100
111
|
**`mockDeep` builds depth on property access, not on calls** — the distinction the tree now spells
|
|
101
112
|
out, and the one that costs an afternoon otherwise. `mock.repo.user.find()` chains because every hop
|
|
102
113
|
but the last is a _read_. A node that is **called** returns what it was configured to return, and by
|
|
@@ -1539,6 +1550,37 @@ assertNgModuleScopes(DirectivesModule, PipesModule); // throws, naming the modul
|
|
|
1539
1550
|
Then declare what the spec needs in the TestBed module directly. Pass only modules you expect to
|
|
1540
1551
|
bring declarations — a providers-only module is legitimately empty.
|
|
1541
1552
|
|
|
1553
|
+
### A component whose own definition has a hole in it
|
|
1554
|
+
|
|
1555
|
+
The same bundle, one level down. A component's `providers`, `viewProviders` and compiled scope are
|
|
1556
|
+
**baked into `ɵcmp` when its module executes**, not read at `createComponent` time — so a barrel
|
|
1557
|
+
split into a chunk that has not run yet produces a definition with `undefined` in those lists.
|
|
1558
|
+
Angular finds out much later, from inside itself, with a stack that names neither the barrel nor the
|
|
1559
|
+
component:
|
|
1560
|
+
|
|
1561
|
+
```
|
|
1562
|
+
TypeError: Cannot read properties of undefined (reading 'provide')
|
|
1563
|
+
❯ resolveProvider render3/di_setup.ts:95
|
|
1564
|
+
```
|
|
1565
|
+
|
|
1566
|
+
Both obvious cures fail: `await import('@scope/lib')` at the top of `beforeEach` is already too late,
|
|
1567
|
+
and a static import at the top of the spec does not fix the order this bundler emits. Worse, the spec
|
|
1568
|
+
that breaks is one nobody touched — chunk boundaries move with file *contents*, so editing a type
|
|
1569
|
+
next door is enough.
|
|
1570
|
+
|
|
1571
|
+
```ts
|
|
1572
|
+
import { assertComponentDefIntact } from 'vitest-auto-spy/angular';
|
|
1573
|
+
|
|
1574
|
+
assertComponentDefIntact(HoverMenuComponent); // HoverMenuComponent.ɵcmp.providers[0] is undefined
|
|
1575
|
+
const fixture = TestBed.createComponent(HoverMenuComponent);
|
|
1576
|
+
```
|
|
1577
|
+
|
|
1578
|
+
It walks the three lists, nested arrays and forward-reference thunks included, and answers the
|
|
1579
|
+
related `Cannot read properties of undefined (reading 'ɵcmp')` from `imports: [Cmp]` as well — there
|
|
1580
|
+
the class reference itself never arrived, and the message names the argument position. Directives
|
|
1581
|
+
(`ɵdir`) are checked the same way. Neither this nor `assertNgModuleScopes` fixes the build; both
|
|
1582
|
+
replace a stack inside `@angular/core` with a line naming what is missing.
|
|
1583
|
+
|
|
1542
1584
|
### Four silent failures, as one setup line
|
|
1543
1585
|
|
|
1544
1586
|
```ts
|
|
@@ -1886,8 +1928,9 @@ export default [{ files: ['**/*.spec.ts'], ...autoSpy.configs.recommended }];
|
|
|
1886
1928
|
| `no-overridden-provider` | `error` | suggest | two providers for one token in one array → the earlier one never runs; the exact duplicate can be deleted |
|
|
1887
1929
|
| `no-inject-before-override` | `warn` | — | `TestBed.inject()` in a hook, in a suite that still calls `override*` |
|
|
1888
1930
|
| `no-import-time-spread` | `error` | suggest | `export const x = [...Imported]` at module scope → a `TypeError` while the bundle loads |
|
|
1931
|
+
| `no-unregistered-inject-spy` | `warn` | — | `injectSpy(X)` for a token this file never registered → the real instance, whose spy helpers exist only for the compiler |
|
|
1889
1932
|
|
|
1890
|
-
|
|
1933
|
+
Fourteen rules; two fix on their own, six offer suggestions. `no-mocked-for-spy` only ever touches a
|
|
1891
1934
|
**type position**, where a wrong rewrite is a compile error rather than a test that quietly changed
|
|
1892
1935
|
meaning — so `--fix` renames the type, adds `import type { Spy } from 'vitest-auto-spy'` and drops
|
|
1893
1936
|
the orphaned `Mocked` import. Every type position, not only a `let`: a factory's return type, a
|
|
@@ -2014,6 +2057,9 @@ packages, which a subpath export can never be.
|
|
|
2014
2057
|
| `overrideComponentProvider(…): the override did not apply` | the component injects a different token, or a later `overrideProvider` won | pass the token the component actually injects (a base class, an `InjectionToken`), and override after nothing else re-configures |
|
|
2015
2058
|
| `the test ended with N unflushed HttpTestingController request(s)` | `enableAngularDiagnostics({ pendingRequests })` — nothing answered them | flush each (`controller.expectOne(url).flush(body)`), or `controller.verify()` where absence is the assertion (§13) |
|
|
2016
2059
|
| `NgModule(s) with an empty runtime scope: …` | `ngModuleScopes` (or `assertNgModuleScopes`) — an AOT test bundle stripped `ɵɵsetNgModuleScope`, so the import contributes nothing | declare what the spec needs in the TestBed module directly; pass only modules expected to bring declarations (§13) |
|
|
2060
|
+
| `Cannot read properties of undefined (reading 'provide')`, stack inside `render3/di_setup` | a barrel chunk had not run when the component's definition was built, so a provider slot is `undefined` | `assertComponentDefIntact(Cmp)` before `createComponent` — it names the list and the index (§13) |
|
|
2061
|
+
| `assertComponentDefIntact(): argument N is undefined, which carries no ɵcmp or ɵdir` | the class reference itself never arrived — the `Cannot read properties of undefined (reading 'ɵcmp')` case | the same split barrel; import the component from its own module, not the barrel (§13) |
|
|
2062
|
+
| `TS1117: An object literal cannot have multiple properties with the same name`, in a fixture | a hundred-line model literal copied into eight specs and edited independently | one `createFixtureFactory<T>(defaults)`; note the runtime keeps the **second** key, so do not auto-fix by dropping one (§12) |
|
|
2017
2063
|
| `configureTestingModule was given N schema(s) that can never apply` | `enableAngularDiagnostics({ deadSchemas })` — `schemas` next to a standalone component, with `declarations` empty | drop the `schemas` entry and put the missing directive/pipe in the component's own `imports`, or use `createDirectiveHost` (§13) |
|
|
2018
2064
|
| `injectSpy(X): the injector returned a plain instance, not an auto-spy` | the token is provided for real; a `console.warn` on its own, a **throw** under `enableAngularDiagnostics({ unspiedProviders })` | `provideAutoSpy(X)` / `provideAutoSpyForToken(TOKEN)`, or `TestBed.inject(X)` if the real thing is what the spec wants |
|
|
2019
2065
|
| `trackInjections(...).get(X): that token is not tracked by this log` | `get` only answers for tokens whose providers the log created | add the token to the `trackInjections([...])` list, or read it from the injector directly (§13) |
|
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ identical API, with **RxJS** spies and **Angular / NestJS / React / Vue·Pinia /
|
|
|
16
16
|
[](https://www.npmjs.com/package/vitest-auto-spy)
|
|
17
17
|
[](https://www.npmjs.com/package/vitest-auto-spy)
|
|
18
18
|
[](https://github.com/ASDAlexey/vitest-auto-spy/actions/workflows/ci.yml)
|
|
19
|
-
[](#install)
|
|
20
20
|
[](https://www.npmjs.com/package/vitest-auto-spy)
|
|
21
21
|
[](https://github.com/ASDAlexey/vitest-auto-spy/actions/workflows/ci.yml)
|
|
22
22
|
[](./LICENSE)
|
|
@@ -49,7 +49,7 @@ identical API, with **RxJS** spies and **Angular / NestJS / React / Vue·Pinia /
|
|
|
49
49
|
- ⚙️ Getter / setter spies via `accessorSpies`
|
|
50
50
|
- 🧰 DI & mocking utilities — `provideAutoSpy` / `injectSpy` (Angular, NestJS, Vue), `createFunctionSpy`, `mockReadonlyProp` for signals
|
|
51
51
|
- ⚡ Angular speed & zoneless helpers — `renderShallow` (**1.7×** on real component specs), `createWithAutoSpies`, `stable` / `flushEffects`, `settleResource` for `httpResource()`, `toHaveSignalValue`, per-file `TestBed` timings
|
|
52
|
-
- 🧱 The providers a testing module cannot reach — `overrideComponentProvider` (which verifies the override actually applied), `provideAutoSpyForToken`, `assertNgModuleScopes`, `createDirectiveHost`
|
|
52
|
+
- 🧱 The providers a testing module cannot reach — `overrideComponentProvider` (which verifies the override actually applied), `provideAutoSpyForToken`, `assertNgModuleScopes`, `assertComponentDefIntact`, `createDirectiveHost`
|
|
53
53
|
- 🚨 Failures that used to be silence — `enableAngularDiagnostics()` for dead NgModule imports, dead `schemas`, an unspied provider and unflushed HTTP requests; `trackInjections` for which collaborators the code actually asked for
|
|
54
54
|
- 🔒 [Strict doubles](#strict-doubles--fail-on-a-method-nobody-configured) — `strict: true` / `onUnstubbedCall` fail on a method nobody configured, naming the class, the method and the arguments instead of answering `undefined`
|
|
55
55
|
- ♻️ `using spy = createSpyFromClass(X)` — every double carries `[Symbol.dispose]`, so the `afterEach` that only reset one spy can go
|
|
@@ -58,7 +58,7 @@ identical API, with **RxJS** spies and **Angular / NestJS / React / Vue·Pinia /
|
|
|
58
58
|
- ⏳ Waiting that is not a guess — `flushEventLoop`, `settleDynamicImport`, `flushEventLoopUntil`, and a clock that survives fake timers (`mockSystemTime`, `useCountingClock`)
|
|
59
59
|
- 🌀 `fakeAsync` / `waitForAsync` on Vitest — one import of `vitest-auto-spy/zone`; zone.js stays out of every other entry
|
|
60
60
|
- 🧩 Module mocks that prove they applied — `assertMocked`, `moduleNamespace`, for a `vi.mock()` a bundler quietly ignored
|
|
61
|
-
- 🧾 Fixtures without casts — deep-partial `createMock`, `narrow()`, `withOverrides()`, `asInstances()`, `captureArg()`
|
|
61
|
+
- 🧾 Fixtures without casts — deep-partial `createMock`, `createFixture` / `createFixtureFactory`, `narrow()`, `withOverrides()`, `asInstances()`, `captureArg()`
|
|
62
62
|
- 🚚 A migration you can verify — `compareTestRuns` on the two JSON reports, `diffByField` for the assertion the reporter collapses
|
|
63
63
|
- 📏 Lint rules and one-line test-run hygiene — fourteen rules in `vitest-auto-spy/eslint-plugin` (two `--fix`, six suggestions), `setupAutoSpy()`
|
|
64
64
|
- 🩺 [Editor diagnostics](#editor-diagnostics--webstorm--vs-code) — the same anti-patterns underlined while you type: native ESLint inspections in **WebStorm** and the other JetBrains IDEs, the ESLint extension in **VS Code**, no extra plugin either way
|
|
@@ -1171,6 +1171,40 @@ Rule of thumb: `createAutoMock` for a collaborator you **call** and assert on, `
|
|
|
1171
1171
|
data shape you **read**. `createMock` is also the one place the `as` lives, so a suite under a
|
|
1172
1172
|
`no-type-assertion` lint rule stops sprinkling `eslint-disable` over its fixtures.
|
|
1173
1173
|
|
|
1174
|
+
### A model many specs build — `createFixture` / `createFixtureFactory`
|
|
1175
|
+
|
|
1176
|
+
`createMock` answers "this spec reads two fields of a big shape". The other habit is more expensive:
|
|
1177
|
+
a model with seventeen required fields, each with its own nested interface, copied into every spec
|
|
1178
|
+
that needs one. Measured on one migration shard, those copies alone produced **28 `TS1117`**
|
|
1179
|
+
diagnostics — a duplicate key in a literal, where the runtime keeps the *second* one.
|
|
1180
|
+
|
|
1181
|
+
```ts
|
|
1182
|
+
import { createFixtureFactory } from 'vitest-auto-spy';
|
|
1183
|
+
|
|
1184
|
+
// article.fixture.ts — the model, written out once and checked in full
|
|
1185
|
+
export const anArticle = createFixtureFactory<Article>({
|
|
1186
|
+
id: '1',
|
|
1187
|
+
header: { title: '', subtitle: 'none' },
|
|
1188
|
+
tags: [],
|
|
1189
|
+
publishedAt: new Date(0),
|
|
1190
|
+
});
|
|
1191
|
+
|
|
1192
|
+
// in a spec — name only what this test is about
|
|
1193
|
+
const draft = anArticle({ header: { title: 'Draft' } }); // header.subtitle survives
|
|
1194
|
+
```
|
|
1195
|
+
|
|
1196
|
+
`defaults` is a **complete** `T`, and that is the point rather than a chore: a field the model
|
|
1197
|
+
dropped six months ago fails in one place instead of in eight copies nobody re-checks. `Partial<T>`
|
|
1198
|
+
and `as T` both delete that diagnostic. Overrides are deep-partial-checked and merge leaf by leaf; an
|
|
1199
|
+
overridden array replaces the default one outright.
|
|
1200
|
+
|
|
1201
|
+
Every call returns a **new object**, and the defaults are copied when the factory is built — a
|
|
1202
|
+
fixture shared by reference is the most common way one test's mutation decides another's outcome, and
|
|
1203
|
+
under `isolate: false` that reaches across files. The copy is deep through plain objects and arrays
|
|
1204
|
+
and stops there: a `Date`, a `Map` or a class instance is carried across by reference rather than
|
|
1205
|
+
rebuilt without its prototype. For defaults that *are* a class instance with getters, snapshot them
|
|
1206
|
+
with `withOverrides()` first.
|
|
1207
|
+
|
|
1174
1208
|
## Synchronous methods
|
|
1175
1209
|
|
|
1176
1210
|
```ts
|
|
@@ -1813,6 +1847,8 @@ single-purpose utility you can pick up independently — they all ride on the sa
|
|
|
1813
1847
|
| `createFunctionSpy(name)` | core | A single standalone function spy with the full helper set (`calledWith`, `resolveWith`, `nextWith`, …) — no class needed |
|
|
1814
1848
|
| `createAutoMock<T>(overrides?)` | core | Proxy-based spy from a **type/interface** alone ([details](#auto-mock-by-type-no-class-needed)) |
|
|
1815
1849
|
| `createMock<T>(partial?)` | core | A plain, spy-free `T` built from the fields a test seeds — for data shapes, not collaborators |
|
|
1850
|
+
| `createFixture<T>(defaults, overrides?)` | core | One `T` from a complete, checked default plus what this test changes — a fresh copy every call |
|
|
1851
|
+
| `createFixtureFactory<T>(defaults)` | core | Somewhere to put that default: returns `(overrides?) => T`, with the defaults pinned at build time |
|
|
1816
1852
|
| `createObservableWithValues(configs, opts?)` | `/rxjs` | Build a fake `Observable` emitting a precise sequence of values / errors / completion |
|
|
1817
1853
|
| `consoleInfoSpy` / `consoleWarnSpy` / … | `/console` | Silent typed spies over the global `console`, installed on import ([details](#console-spies--vitest-auto-spyconsole)) |
|
|
1818
1854
|
| `mockReadonlyProp(obj, prop, value)` | `/angular` | Overwrite a `readonly` property (incl. Angular signals) with a static value |
|
|
@@ -1850,6 +1886,7 @@ single-purpose utility you can pick up independently — they all ride on the sa
|
|
|
1850
1886
|
| `registerFocusMatchers()` | `/setup` | Adds `expect(el).toHaveFocus()`, which names _why_ focus is elsewhere |
|
|
1851
1887
|
| `overrideAutoSpy(Token, config?)` / `overrideComponentProvider(Cmp, Token, config?)` | `/angular` | Replace a dependency a component declares in its own `providers` |
|
|
1852
1888
|
| `assertNgModuleScopes(...modules)` | `/angular` | Fail early when an AOT test bundle left an NgModule with no runtime declarations |
|
|
1889
|
+
| `assertComponentDefIntact(...components)` | `/angular` | Fail before rendering when a half-loaded barrel chunk left a hole in a component's own `providers` or scope |
|
|
1853
1890
|
| `enableAngularDiagnostics(opts?)` / `assertNoPendingRequests()` | `/angular` | Dead NgModule imports, dead `schemas`, an unspied provider and unflushed HTTP requests, as failures ([details](#diagnostics--four-silent-failures-made-loud)) |
|
|
1854
1891
|
| `trackInjections(tokens, opts?)` | `/angular`, `/nestjs` | Which collaborators DI actually constructed, recorded through provider factories — with the doubles attached |
|
|
1855
1892
|
| `mockSignalProp(obj, prop, initial)` | `/angular` | Replace a signal-valued property with a real `WritableSignal`, and hand the writable handle back |
|
|
@@ -2271,6 +2308,20 @@ seven sites in an 8 673-file workspace, which is small enough to flag at the cur
|
|
|
2271
2308
|
and an instance field are deliberately not reported — they run later than the module does — while a
|
|
2272
2309
|
`static` field is.
|
|
2273
2310
|
|
|
2311
|
+
The fourteenth, `no-unregistered-inject-spy`, catches `injectSpy(X)` for a token nothing in the file
|
|
2312
|
+
registered as an auto-spy. What comes back is whatever Angular DI already had — the real service, or
|
|
2313
|
+
an object an imported testing module put there — and nothing complains, because `injectSpy` is
|
|
2314
|
+
declared to return a `Spy<T>`: the helpers are present for `tsc` and absent at run time, so the first
|
|
2315
|
+
`.mockReturnValue(…)` or `.calledWith(…)` throws on a real method. The library warns about exactly
|
|
2316
|
+
this at run time, and that warning is why the rule exists rather than why it is redundant — it does
|
|
2317
|
+
not fail the run, it scrolls past in a suite of a thousand files, and it arrives only for the tests
|
|
2318
|
+
that executed the line; in one consumer monorepo dozens of spec files print it on every CI run and it
|
|
2319
|
+
has never been acted on. Reading it at the cursor needs no types, only the file's own registrations,
|
|
2320
|
+
and the rule stays quiet unless it can read all of them: it needs at least one `provideAutoSpy`, and
|
|
2321
|
+
a spread, an unknown provider factory, `createWithAutoSpies`, `renderShallow` or
|
|
2322
|
+
`TestBed.overrideProvider` silences the file. A token handed a plain `useValue` is left to
|
|
2323
|
+
`prefer-provide-auto-spy` — two rules on one line only teach people to disable both.
|
|
2324
|
+
|
|
2274
2325
|
## Editor diagnostics — WebStorm & VS Code
|
|
2275
2326
|
|
|
2276
2327
|
The rules above are worth more while the cursor is still on the line than they are in CI, because
|
|
@@ -2280,7 +2331,7 @@ package's own — it needs its ESLint integration switched on.
|
|
|
2280
2331
|
### WebStorm and the other JetBrains IDEs
|
|
2281
2332
|
|
|
2282
2333
|
No plugin to install: WebStorm, IntelliJ IDEA Ultimate, PhpStorm, PyCharm Professional and RubyMine
|
|
2283
|
-
all run ESLint natively, so the
|
|
2334
|
+
all run ESLint natively, so the fourteen rules appear inline, in the **Problems** tool window, and
|
|
2284
2335
|
under **Code → Inspect Code** for the whole project.
|
|
2285
2336
|
|
|
2286
2337
|
```js
|
|
@@ -2297,7 +2348,7 @@ has supported flat config since 2023.3); scope the block to spec files yourself;
|
|
|
2297
2348
|
the fixes and suggestions live.
|
|
2298
2349
|
|
|
2299
2350
|
A native JetBrains plugin is **not** planned — it would duplicate an integration the IDE already has
|
|
2300
|
-
and then keep a second copy of
|
|
2351
|
+
and then keep a second copy of fourteen rules, in Kotlin, in step with the TypeScript ones.
|
|
2301
2352
|
|
|
2302
2353
|
### VS Code, Cursor, Windsurf, VSCodium
|
|
2303
2354
|
|
|
@@ -2328,6 +2379,31 @@ asSpy(TestBed.inject(CartService)); // CartService → Spy<CartService>, for the
|
|
|
2328
2379
|
|
|
2329
2380
|
Both are the same object at runtime; only the view changes.
|
|
2330
2381
|
|
|
2382
|
+
### Error → cure
|
|
2383
|
+
|
|
2384
|
+
Keyed by what the compiler prints, because that is what you have when you get there. The helper
|
|
2385
|
+
names are unfindable from these messages otherwise — no `TS2739` text contains the word `asInstance`.
|
|
2386
|
+
|
|
2387
|
+
| Message | What actually happened | Cure |
|
|
2388
|
+
| ------------------------------------------------------------------------------- | ----------------------------------------- | -------------------------------------------------- |
|
|
2389
|
+
| `TS2352 … 'Spy<X>' … Property 'accessorSpies' is missing in type 'X'` | `x as Spy<X>`, written by hand | `asSpy(x)` — never a double assertion |
|
|
2390
|
+
| `TS2739` / `TS2740`: `'Spy<X>' is missing … ` + a list of **private** fields | a spy handed to an API typed against `X` | `asInstance(spy)` |
|
|
2391
|
+
| `TS2345: Argument of type 'Spy<X>' is not assignable to parameter of type 'X'` | the same, in an argument | `asInstance(spy)`, or `asInstances(a, b, c)` |
|
|
2392
|
+
| `TS2322: Type 'Spy<X>' is not assignable to type 'Mocked<X>' …` | the variable was declared `Mocked<T>` | declare it `Spy<T>` |
|
|
2393
|
+
| `'AddPromiseSpyMethods<unknown>' is missing … from type 'WithMockReturnValue<…>'` | a generic class inferred as `Service<any>` | `asSpy<Service>(…)` / `injectSpy<Service>(…)` |
|
|
2394
|
+
| `TS2739 … 'Spy<X>' is missing …` **on a line with `injectSpy`** | the provider handed back the real object | `provideAutoSpy(X)`, or an honest `TestBed.inject` |
|
|
2395
|
+
|
|
2396
|
+
Two notes that cost real time when they are missing.
|
|
2397
|
+
|
|
2398
|
+
**The last row is word for word the second one**, and that is a property of the language rather than
|
|
2399
|
+
a flaw in the table: one message serves two different mistakes. The tell is the line it lands on.
|
|
2400
|
+
|
|
2401
|
+
**The error count does not go down monotonically.** TypeScript stops checking a call at the first bad
|
|
2402
|
+
argument, so "one error left" means "as many as there are unfixed arguments" — one file counted
|
|
2403
|
+
40 → 1 → 1 → 1 → 0. Excess-property checking stops at the first unknown key of a literal for the same
|
|
2404
|
+
reason. The useful heuristic: if a file already contains one `asInstance`, it almost certainly needs
|
|
2405
|
+
another, and the next one is in that same file.
|
|
2406
|
+
|
|
2331
2407
|
## API reference
|
|
2332
2408
|
|
|
2333
2409
|
| Export | Description |
|
|
@@ -2335,6 +2411,7 @@ Both are the same object at runtime; only the view changes.
|
|
|
2335
2411
|
| `createSpyFromClass(Class, methodsOrConfig?)` | Build a fully-typed `Spy<T>` from a class |
|
|
2336
2412
|
| `createAutoMock<T>(overrides?, config?)` | Build a `Spy<T>` from a **type/interface** alone (Proxy, no class); `{ returns, observablePropsToSpyOn }` configure it |
|
|
2337
2413
|
| `createMock<T>(partial?)` | Build a plain, spy-free `T` from the fields a test seeds — for data shapes the code under test reads |
|
|
2414
|
+
| `createFixture<T>(defaults, overrides?)` / `createFixtureFactory<T>(defaults)` | A model written out and checked once, stamped into a fresh copy per test — for the fixture eight specs would otherwise each keep a copy of |
|
|
2338
2415
|
| `mockDeep<T>(overrides?, options?)` | Build a **recursive** auto-mock — `mock.repo.user.find()` chains without seeding; `{ selfReturning: true }` chains through calls too |
|
|
2339
2416
|
| `resetAutoSpy(spy)` / `clearAutoSpy(spy)` | Reset every spy in an auto-spy at once — `reset` also reverts return-value config (`calledWith` **and** a bare `mockReturnValue`); `clear` keeps it |
|
|
2340
2417
|
| `spy[Symbol.dispose]()` | What `using spy = createSpyFromClass(X)` runs at the end of the block — `resetAutoSpy(this)`, on every double including each `mockDeep` node |
|
|
@@ -2357,7 +2434,7 @@ Both are the same object at runtime; only the view changes.
|
|
|
2357
2434
|
| `autoMocked<T>(overrides?)` | `createAutoMock` typed as `T & Spy<T>` |
|
|
2358
2435
|
| `mockSystemTime` / `withSystemTime` / `mockNow` / `useCountingClock` _(`/setup`)_ | Clock control that survives fake timers being re-installed per test |
|
|
2359
2436
|
| `registerFocusMatchers()` _(`/setup`)_ | Adds `expect(el).toHaveFocus()` |
|
|
2360
|
-
| `overrideAutoSpy` / `overrideComponentProvider` / `assertNgModuleScopes` _(Angular)_ | Override a component-level provider — and verify on the next fixture that the override applied; diagnose an NgModule with an empty runtime scope |
|
|
2437
|
+
| `overrideAutoSpy` / `overrideComponentProvider` / `assertNgModuleScopes` / `assertComponentDefIntact` _(Angular)_ | Override a component-level provider — and verify on the next fixture that the override applied; diagnose an NgModule with an empty runtime scope |
|
|
2361
2438
|
| `enableAngularDiagnostics(opts?)` / `disableAngularDiagnostics()` / `assertNoPendingRequests()` _(Angular)_ | Dead NgModule imports, dead `schemas`, an unspied provider and unflushed HTTP requests, as failures |
|
|
2362
2439
|
| `trackInjections(tokens, opts?)` _(Angular, NestJS)_ | Providers that record which collaborators DI constructed, with an auto-spy behind each token |
|
|
2363
2440
|
| `setupAutoSpy(opts?)` _(`/setup`)_ | Property restore + duplicate-copy detection + mock-registry hygiene, in one call |
|
package/dist/angular.d.ts
CHANGED
|
@@ -35,6 +35,12 @@ import 'vitest';
|
|
|
35
35
|
* landed. So {@link overrideComponentProvider} also checks, on the next `TestBed.createComponent`,
|
|
36
36
|
* that the component's own injector really answers with the spy — see {@link verifyOnNextCreate}
|
|
37
37
|
* for why that check is always on rather than an opt-in diagnostic.
|
|
38
|
+
*
|
|
39
|
+
* The same AOT bundle is behind the two assertions at the end of the file.
|
|
40
|
+
* {@link assertNgModuleScopes} covers the module whose scope the bundler stripped;
|
|
41
|
+
* {@link assertComponentDefIntact} covers the component whose own definition was built while the
|
|
42
|
+
* chunk holding one of its providers had not run yet. Neither fixes a build — both replace a stack
|
|
43
|
+
* inside `@angular/core` with a line naming the thing that is missing.
|
|
38
44
|
*/
|
|
39
45
|
|
|
40
46
|
/** The `{ useValue }` shape `TestBed.overrideProvider` expects, carrying an auto-spy. */
|
|
@@ -75,6 +81,39 @@ declare function overrideAutoSpy<T>(ObjectClass: ClassType<T>, methodsToSpyOnOrC
|
|
|
75
81
|
* runtime scope the bundler has stripped — leaving the component with none of them.
|
|
76
82
|
*/
|
|
77
83
|
declare function overrideComponentProvider<T>(component: Type<unknown>, ObjectClass: ClassType<T>, methodsToSpyOnOrConfig?: ClassSpyConfiguration<T> | OnlyMethodKeysOf<T>[]): Spy<T>;
|
|
84
|
+
/**
|
|
85
|
+
* Fail before rendering when a component's own definition has holes in it.
|
|
86
|
+
*
|
|
87
|
+
* Providers are **baked into `ɵcmp` when the component's module executes**, not read at
|
|
88
|
+
* `createComponent` time. So when a bundler splits a barrel into a chunk that has not run yet, the
|
|
89
|
+
* definition is built with `undefined` where a provider or a scope dependency should be, and Angular
|
|
90
|
+
* discovers it much later, from inside itself:
|
|
91
|
+
*
|
|
92
|
+
* ```
|
|
93
|
+
* TypeError: Cannot read properties of undefined (reading 'provide')
|
|
94
|
+
* ❯ resolveProvider render3/di_setup.ts:95
|
|
95
|
+
* ```
|
|
96
|
+
*
|
|
97
|
+
* The stack names neither the barrel, nor the symbol, nor the component — and the spec it breaks is
|
|
98
|
+
* usually one nobody touched, because chunk boundaries move with file *contents*: editing a type in
|
|
99
|
+
* a neighbouring file is enough. Both documented cures fail, too, and for the same reason: an
|
|
100
|
+
* `await import()` in `beforeEach` is already too late, and a static import at the top of the spec
|
|
101
|
+
* does not fix the order this bundler emits.
|
|
102
|
+
*
|
|
103
|
+
* ```ts
|
|
104
|
+
* assertComponentDefIntact(HoverMenuComponent);
|
|
105
|
+
* const fixture = TestBed.createComponent(HoverMenuComponent);
|
|
106
|
+
* ```
|
|
107
|
+
*
|
|
108
|
+
* The same call answers the related `Cannot read properties of undefined (reading 'ɵcmp')` from
|
|
109
|
+
* `imports: [Cmp]`, where the class reference itself is the thing that never arrived.
|
|
110
|
+
*
|
|
111
|
+
* This does not fix the build — that is a bundler configuration question — but it turns a
|
|
112
|
+
* half-hour investigation into one line, and points it away from the spec.
|
|
113
|
+
*
|
|
114
|
+
* @param components The component (or directive) classes a spec is about to render or import.
|
|
115
|
+
*/
|
|
116
|
+
declare function assertComponentDefIntact(...components: unknown[]): void;
|
|
78
117
|
/**
|
|
79
118
|
* Fail early when an NgModule imported into the TestBed contributes nothing at runtime.
|
|
80
119
|
*
|
|
@@ -576,4 +615,4 @@ declare function reportSpecTiming(timing: SpecTiming): void;
|
|
|
576
615
|
*/
|
|
577
616
|
declare function enableTestBedDiagnostics(options?: TestBedDiagnosticsOptions): void;
|
|
578
617
|
|
|
579
|
-
export { type AngularDiagnosticsOptions, type AngularTestEnvMode, type AngularTestEnvOptions, type AutoSpyOverride, type DirectiveHostOptions, type MockedResource, type ResourceDouble, type ResourceDoubleStatus, type ResourceLike, type SignalLike, type SpecTiming, type TestBedDiagnosticsOptions, assertNgModuleScopes, assertNoPendingRequests, createDirectiveHost, disableAngularDiagnostics, disableTestBedDiagnostics, enableAngularDiagnostics, enableTestBedDiagnostics, formatSpecTiming, getTestBedTiming, instrumentTestBed, mockResourceProp, mockSignalProp, overrideAutoSpy, overrideComponentProvider, registerDirectiveMatchers, registerResourceMatchers, registerSignalMatchers, reportSpecTiming, setupAngularTestEnv };
|
|
618
|
+
export { type AngularDiagnosticsOptions, type AngularTestEnvMode, type AngularTestEnvOptions, type AutoSpyOverride, type DirectiveHostOptions, type MockedResource, type ResourceDouble, type ResourceDoubleStatus, type ResourceLike, type SignalLike, type SpecTiming, type TestBedDiagnosticsOptions, assertComponentDefIntact, assertNgModuleScopes, assertNoPendingRequests, createDirectiveHost, disableAngularDiagnostics, disableTestBedDiagnostics, enableAngularDiagnostics, enableTestBedDiagnostics, formatSpecTiming, getTestBedTiming, instrumentTestBed, mockResourceProp, mockSignalProp, overrideAutoSpy, overrideComponentProvider, registerDirectiveMatchers, registerResourceMatchers, registerSignalMatchers, reportSpecTiming, setupAngularTestEnv };
|
package/dist/angular.js
CHANGED
|
@@ -1402,6 +1402,66 @@ function moduleName(module) {
|
|
|
1402
1402
|
const name = readProperty(module, "name");
|
|
1403
1403
|
return typeof name === "string" ? name : String(module);
|
|
1404
1404
|
}
|
|
1405
|
+
var DEFINITION_LISTS = ["providers", "viewProviders", "dependencies"];
|
|
1406
|
+
function resolveList(list) {
|
|
1407
|
+
if (typeof list !== "function") {
|
|
1408
|
+
return list;
|
|
1409
|
+
}
|
|
1410
|
+
try {
|
|
1411
|
+
const produced = Reflect.apply(list, void 0, []);
|
|
1412
|
+
return produced;
|
|
1413
|
+
} catch {
|
|
1414
|
+
return void 0;
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
function collectHoles(list, path, holes) {
|
|
1418
|
+
const resolved = resolveList(list);
|
|
1419
|
+
if (!Array.isArray(resolved)) {
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1422
|
+
resolved.forEach((entry, index) => {
|
|
1423
|
+
const at = `${path}[${index}]`;
|
|
1424
|
+
if (entry === void 0 || entry === null) {
|
|
1425
|
+
holes.push(at);
|
|
1426
|
+
} else if (Array.isArray(entry)) {
|
|
1427
|
+
collectHoles(entry, at, holes);
|
|
1428
|
+
}
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1431
|
+
function definitionOf(type) {
|
|
1432
|
+
const component = readProperty(type, "\u0275cmp");
|
|
1433
|
+
if (component !== void 0) {
|
|
1434
|
+
return { definition: component, key: "\u0275cmp" };
|
|
1435
|
+
}
|
|
1436
|
+
const directive = readProperty(type, "\u0275dir");
|
|
1437
|
+
return directive === void 0 ? void 0 : { definition: directive, key: "\u0275dir" };
|
|
1438
|
+
}
|
|
1439
|
+
function assertComponentDefIntact(...components) {
|
|
1440
|
+
components.forEach((component, position) => {
|
|
1441
|
+
const named = moduleName(component);
|
|
1442
|
+
const found = definitionOf(component);
|
|
1443
|
+
if (!found) {
|
|
1444
|
+
throw new Error(
|
|
1445
|
+
withDocs(
|
|
1446
|
+
`[vitest-auto-spy] assertComponentDefIntact(): argument ${position} is ${named}, which carries no \u0275cmp or \u0275dir.
|
|
1447
|
+
Either it is not a component or directive, or the chunk that defines it has not executed yet \u2014 the import resolved to nothing. A barrel split across chunks is the usual cause.`,
|
|
1448
|
+
DOCS_LINKS.angularOverrides
|
|
1449
|
+
)
|
|
1450
|
+
);
|
|
1451
|
+
}
|
|
1452
|
+
const holes = [];
|
|
1453
|
+
DEFINITION_LISTS.forEach((list) => collectHoles(readProperty(found.definition, list), `${named}.${found.key}.${list}`, holes));
|
|
1454
|
+
if (holes.length > 0) {
|
|
1455
|
+
throw new Error(
|
|
1456
|
+
withDocs(
|
|
1457
|
+
`[vitest-auto-spy] ${holes.join(", ")} ${holes.length === 1 ? "is" : "are"} undefined.
|
|
1458
|
+
A component bakes its providers and its scope into the definition when its module executes, so a hole there means the chunk holding that symbol had not run at that moment \u2014 an uninitialised barrel chunk. Angular reports this much later as "Cannot read properties of undefined (reading 'provide')", from inside its own provider resolution.`,
|
|
1459
|
+
DOCS_LINKS.angularOverrides
|
|
1460
|
+
)
|
|
1461
|
+
);
|
|
1462
|
+
}
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1405
1465
|
function assertNgModuleScopes(...modules) {
|
|
1406
1466
|
const empty = modules.filter(hasEmptyRuntimeScope).map(moduleName);
|
|
1407
1467
|
if (empty.length === 0) {
|
|
@@ -2009,4 +2069,4 @@ function registerSignalMatchers() {
|
|
|
2009
2069
|
}
|
|
2010
2070
|
useVitestAdapter();
|
|
2011
2071
|
|
|
2012
|
-
export { assertNgModuleScopes, assertNoPendingRequests, countMockedProps, createDirectiveHost, createWithAutoSpies, disableAngularDiagnostics, disableTestBedDiagnostics, enableAngularDiagnostics, enableTestBedDiagnostics, flushEffects, formatSpecTiming, getTestBedTiming, injectSpy, instrumentTestBed, mockAccessorsProp, mockReadonlyProp, mockReadonlyPropGetter, mockResourceProp, mockSignalProp, mockValueProp, overrideAutoSpy, overrideComponentProvider, provideAutoSpy, provideAutoSpyForToken, registerDirectiveMatchers, registerResourceMatchers, registerSignalMatchers, renderShallow, reportSpecTiming, restoreMockedProps, runEffect, settleResource, setupAngularTestEnv, stable, trackInjections };
|
|
2072
|
+
export { assertComponentDefIntact, assertNgModuleScopes, assertNoPendingRequests, countMockedProps, createDirectiveHost, createWithAutoSpies, disableAngularDiagnostics, disableTestBedDiagnostics, enableAngularDiagnostics, enableTestBedDiagnostics, flushEffects, formatSpecTiming, getTestBedTiming, injectSpy, instrumentTestBed, mockAccessorsProp, mockReadonlyProp, mockReadonlyPropGetter, mockResourceProp, mockSignalProp, mockValueProp, overrideAutoSpy, overrideComponentProvider, provideAutoSpy, provideAutoSpyForToken, registerDirectiveMatchers, registerResourceMatchers, registerSignalMatchers, renderShallow, reportSpecTiming, restoreMockedProps, runEffect, settleResource, setupAngularTestEnv, stable, trackInjections };
|
package/dist/bun-angular.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { d as AccessorKeysOf, e as AddAccessorsSpies, f as AddCalledWithObservable, g as AddCalledWithPromise, h as AddCalledWithSpyMethods, A as AddObservableSpyMethods, i as AddPromiseSpyMethods, j as AddSpyMethodsByReturnTypes, k as AddVoidReturnHelpers, a as ClassSpyConfiguration, C as ClassType, b as CompleteValueConfig, D as DeepMockProxy, l as DeepPartial, E as ErrorValueConfig, F as Func, M as MethodReturns, m as Mutable, N as NextValueConfig, O as OnlyMethodKeysOf, n as OnlyObservablePropsOf, o as OnlyPropsOf, p as Overload, q as Overloads, P as PropStubValue, S as Spy, r as SpyDisposable, s as SpyOptions, t as StrictSpyConfiguration, U as UnstubbedCall, u as UnstubbedCallHandler, V as ValueConfig, c as ValueConfigPerCall, W as WithMockReturnValue } from './types-it6CnDkI.js';
|
|
2
|
-
export { ArgCaptor, AsInstances, AssertMockedOptions, ConstructorMock, ConstructorSpy, FlushUntilOptions, IntersectionObserverStubOptions, MediaElementState, MediaElementStub, MediaElementStubOptions, MockDeepOptions, ModuleNamespace, ModuleNamespaceOptions, MutationRecordInit, ObserverGlobal, ObserverInstance, ObserverStub, ObserverStubOptions, ResizeEntryRect, TestRunComparison, TestRunReport, TestRunSummary, asInstance, asInstances, asSpy, assertMocked, captureArg, clearAutoSpy, compareTestRuns, createFunctionSpy, createMock, createSpyClass, createSpyFromClass, diffByField, errorHandler, flushEventLoop, flushEventLoopUntil, formatTestRunComparison, intersectionEntry, mockConstructor, mockDeep, moduleNamespace, mutationRecord, narrow, resetAutoSpy, resizeEntry, settleDynamicImport, stubAbortController, stubConstructor, stubIntersectionObserver, stubMediaElement, stubMutationObserver, stubObserver, stubResizeObserver, summarizeTestRun, withOverrides } from './bun.js';
|
|
2
|
+
export { ArgCaptor, AsInstances, AssertMockedOptions, ConstructorMock, ConstructorSpy, FixtureFactory, FlushUntilOptions, IntersectionObserverStubOptions, MediaElementState, MediaElementStub, MediaElementStubOptions, MockDeepOptions, ModuleNamespace, ModuleNamespaceOptions, MutationRecordInit, ObserverGlobal, ObserverInstance, ObserverStub, ObserverStubOptions, ResizeEntryRect, TestRunComparison, TestRunReport, TestRunSummary, asInstance, asInstances, asSpy, assertMocked, captureArg, clearAutoSpy, compareTestRuns, createFixture, createFixtureFactory, createFunctionSpy, createMock, createSpyClass, createSpyFromClass, diffByField, errorHandler, flushEventLoop, flushEventLoopUntil, formatTestRunComparison, intersectionEntry, mockConstructor, mockDeep, moduleNamespace, mutationRecord, narrow, resetAutoSpy, resizeEntry, settleDynamicImport, stubAbortController, stubConstructor, stubIntersectionObserver, stubMediaElement, stubMutationObserver, stubObserver, stubResizeObserver, summarizeTestRun, withOverrides } from './bun.js';
|
|
3
3
|
export { A as AutoMockConfiguration, C as CallbackSubscribable, E as EmissionObserver, a as EmissionOptions, b as EmissionSource, S as SubscribableLike, c as autoMocked, d as createAutoMock, e as expectCompletion, f as expectEmission, g as expectEmissions, h as expectError, i as expectNoEmission, s as setEmissionTimeout } from './expect-emission-CuGjE0YR.js';
|
|
4
4
|
export { A as AccessorImplementations, R as RestoreProp, c as countMockedProps, m as mockAccessorsProp, a as mockReadonlyProp, b as mockReadonlyPropGetter, d as mockValueProp, r as restoreMockedProps } from './prop-mock-IgCwv_40.js';
|
|
5
5
|
export { d as describeDuplicateCopies, g as getPackageCopies } from './package-identity-B1pqa-Sh.js';
|
package/dist/bun-angular.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import './chunk-Y4YG7UOK.js';
|
|
2
2
|
import { createSpyForToken } from './chunk-LVLSMH6T.js';
|
|
3
3
|
import './chunk-NQXXUJPS.js';
|
|
4
|
-
export { asInstance, asInstances, asSpy, assertMocked, captureArg, compareTestRuns, createMock, createSpyClass, describeDuplicateCopies, diffByField, expectCompletion, expectEmission, expectEmissions, expectError, expectNoEmission, flushEventLoop, flushEventLoopUntil, formatTestRunComparison, getPackageCopies, intersectionEntry, mockConstructor, mockDeep, moduleNamespace, mutationRecord, narrow, resizeEntry, setEmissionTimeout, settleDynamicImport, stubAbortController, stubConstructor, stubIntersectionObserver, stubMediaElement, stubMutationObserver, stubObserver, stubResizeObserver, summarizeTestRun, withOverrides } from './chunk-
|
|
4
|
+
export { asInstance, asInstances, asSpy, assertMocked, captureArg, compareTestRuns, createFixture, createFixtureFactory, createMock, createSpyClass, describeDuplicateCopies, diffByField, expectCompletion, expectEmission, expectEmissions, expectError, expectNoEmission, flushEventLoop, flushEventLoopUntil, formatTestRunComparison, getPackageCopies, intersectionEntry, mockConstructor, mockDeep, moduleNamespace, mutationRecord, narrow, resizeEntry, setEmissionTimeout, settleDynamicImport, stubAbortController, stubConstructor, stubIntersectionObserver, stubMediaElement, stubMutationObserver, stubObserver, stubResizeObserver, summarizeTestRun, withOverrides } from './chunk-FKAXOKP5.js';
|
|
5
5
|
export { countMockedProps, mockAccessorsProp, mockReadonlyProp, mockReadonlyPropGetter, mockValueProp, restoreMockedProps } from './chunk-R4EWAG7Y.js';
|
|
6
6
|
import { createSpyFromClass } from './chunk-B7PDIHEG.js';
|
|
7
7
|
export { autoMocked, clearAutoSpy, createAutoMock, createSpyFromClass, resetAutoSpy } from './chunk-B7PDIHEG.js';
|
package/dist/bun.d.ts
CHANGED
|
@@ -76,6 +76,88 @@ declare function createSpyFromClass<T, Options extends SpyOptions = SpyOptions>(
|
|
|
76
76
|
*/
|
|
77
77
|
declare function createMock<T>(partial?: DeepPartial<T>): T;
|
|
78
78
|
|
|
79
|
+
/**
|
|
80
|
+
* `createFixture` / `createFixtureFactory` — one checked copy of a data model, handed out fresh.
|
|
81
|
+
*
|
|
82
|
+
* Specs do not only spy; they *build data*, and that is where a suite meets the type checker for
|
|
83
|
+
* the first time. A content model with seventeen required fields, each with its own nested
|
|
84
|
+
* interface, gets copied into every spec that needs it: eight files, a hundred-line literal apiece.
|
|
85
|
+
* The copies then rot independently. Measured on one migration shard, that single habit produced
|
|
86
|
+
* **28 `TS1117`** diagnostics (a duplicate key in a literal — the runtime keeps the *second*, so an
|
|
87
|
+
* automated "drop one" fix silently changes values) and half of the shard's `TS2741`.
|
|
88
|
+
*
|
|
89
|
+
* The two obvious escapes are worse than the problem:
|
|
90
|
+
*
|
|
91
|
+
* - `as T` and `Partial<T>` both delete the diagnostic that made the migration worth doing —
|
|
92
|
+
* `TS2353`, "this fixture still sets a field the model dropped six months ago". A fixture that
|
|
93
|
+
* cannot be wrong about the model is a fixture that never reports the model changed.
|
|
94
|
+
* - {@link createAutoMock} builds an object from a type, but only its **methods**. A data model has
|
|
95
|
+
* none, so the machine walks straight past the place the errors actually are.
|
|
96
|
+
*
|
|
97
|
+
* So the answer is not another way to skip the fields. It is somewhere to *put* them once:
|
|
98
|
+
* {@link createFixtureFactory} takes a complete, fully checked `T` — every required field, every
|
|
99
|
+
* removed field rejected, one place for the compiler to point at — and returns a function that
|
|
100
|
+
* stamps out copies with per-test changes applied. The check happens once; the fifty call sites
|
|
101
|
+
* that follow name only what they care about.
|
|
102
|
+
*
|
|
103
|
+
* **Each call returns a new object.** That is not a performance footnote — a fixture shared by
|
|
104
|
+
* reference across a file is the most common way one test's mutation decides another test's
|
|
105
|
+
* outcome, and under `isolate: false` the sharing reaches across files. The copy is deep through
|
|
106
|
+
* plain objects and arrays and stops there: a `Date`, a `Map`, a DOM node or a class instance is
|
|
107
|
+
* carried across by reference, because rebuilding one would strip its prototype — accessors
|
|
108
|
+
* included — and hand back something that is no longer the model it claims to be. When the defaults
|
|
109
|
+
* *are* a class instance with getters, snapshot it with {@link withOverrides} first and hand the
|
|
110
|
+
* result here.
|
|
111
|
+
*
|
|
112
|
+
* | | `createMock<T>()` | `createFixture<T>()` |
|
|
113
|
+
* | --- | --- | --- |
|
|
114
|
+
* | Input | the fields this test reads | a complete `T`, plus what this test changes |
|
|
115
|
+
* | Unnamed fields | `undefined` at runtime | the default's value |
|
|
116
|
+
* | Use it for | a shape read shallowly, once | a model many specs build, read deeply, and mutate |
|
|
117
|
+
*/
|
|
118
|
+
|
|
119
|
+
/** Stamps out one fixture per call. What {@link createFixtureFactory} returns. */
|
|
120
|
+
type FixtureFactory<T> = (overrides?: DeepPartial<T>) => T;
|
|
121
|
+
/**
|
|
122
|
+
* Build one `T` from a complete default and the fields this test changes.
|
|
123
|
+
*
|
|
124
|
+
* ```ts
|
|
125
|
+
* const article = createFixture<Article>(ARTICLE_DEFAULTS, { header: { title: 'Draft' } });
|
|
126
|
+
* ```
|
|
127
|
+
*
|
|
128
|
+
* `defaults` is a whole `T`, so a field the model removed is a compile error here rather than a
|
|
129
|
+
* value nothing reads; `overrides` is checked at every depth against the same type. Sibling fields
|
|
130
|
+
* of an overridden leaf are kept — `header.subtitle` above survives — while an overridden array
|
|
131
|
+
* replaces the default one entirely.
|
|
132
|
+
*
|
|
133
|
+
* Reach for {@link createFixtureFactory} as soon as a second spec needs the same defaults; reach
|
|
134
|
+
* for {@link createMock} when there are no meaningful defaults to begin with and the test reads two
|
|
135
|
+
* fields of a large shape.
|
|
136
|
+
*
|
|
137
|
+
* @param defaults A complete `T`. Checked in full: missing and removed fields both fail here.
|
|
138
|
+
* @param overrides The fields this fixture changes, checked against `T` at every depth.
|
|
139
|
+
*/
|
|
140
|
+
declare function createFixture<T>(defaults: T, overrides?: DeepPartial<T>): T;
|
|
141
|
+
/**
|
|
142
|
+
* Somewhere to put a fixture, so that the model is written out and checked exactly once.
|
|
143
|
+
*
|
|
144
|
+
* ```ts
|
|
145
|
+
* // article.fixture.ts — one file, one checked literal
|
|
146
|
+
* export const anArticle = createFixtureFactory<Article>({ id: '1', header: { title: '', subtitle: '' }, tags: [], … });
|
|
147
|
+
*
|
|
148
|
+
* // in a spec
|
|
149
|
+
* const draft = anArticle({ header: { title: 'Draft' } });
|
|
150
|
+
* const tagged = anArticle({ tags: ['news'] });
|
|
151
|
+
* ```
|
|
152
|
+
*
|
|
153
|
+
* The defaults are copied when the factory is built, so a later edit to the object that was passed
|
|
154
|
+
* in cannot reach a fixture already handed out — and every call returns a fresh copy, so one test
|
|
155
|
+
* mutating what it was given cannot decide what the next one sees.
|
|
156
|
+
*
|
|
157
|
+
* @param defaults A complete `T`, checked once, here.
|
|
158
|
+
*/
|
|
159
|
+
declare function createFixtureFactory<T>(defaults: T): FixtureFactory<T>;
|
|
160
|
+
|
|
79
161
|
/**
|
|
80
162
|
* Behaviour switches for {@link mockDeep}.
|
|
81
163
|
*
|
|
@@ -1105,4 +1187,4 @@ interface ResizeEntryRect {
|
|
|
1105
1187
|
*/
|
|
1106
1188
|
declare function resizeEntry(target: Element, rect?: ResizeEntryRect): ResizeObserverEntry;
|
|
1107
1189
|
|
|
1108
|
-
export { AddSpyMethodsByReturnTypes, type ArgCaptor, type AsInstances, type AssertMockedOptions, ClassSpyConfiguration, ClassType, type ConstructorMock, type ConstructorSpy, DeepMockProxy, DeepPartial, type FlushUntilOptions, Func, type IntersectionObserverStubOptions, type MediaElementState, type MediaElementStub, type MediaElementStubOptions, type MockDeepOptions, type ModuleNamespace, type ModuleNamespaceOptions, type MutationRecordInit, type ObserverGlobal, type ObserverInstance, type ObserverStub, type ObserverStubOptions, OnlyMethodKeysOf, type ResizeEntryRect, Spy, SpyOptions, type TestRunComparison, type TestRunReport, type TestRunSummary, UnstubbedCallHandler, asInstance, asInstances, asSpy, assertMocked, captureArg, clearAutoSpy, compareTestRuns, createFunctionSpy, createMock, createSpyClass, createSpyFromClass, diffByField, errorHandler, flushEventLoop, flushEventLoopUntil, formatTestRunComparison, intersectionEntry, mockConstructor, mockDeep, moduleNamespace, mutationRecord, narrow, resetAutoSpy, resizeEntry, settleDynamicImport, stubAbortController, stubConstructor, stubIntersectionObserver, stubMediaElement, stubMutationObserver, stubObserver, stubResizeObserver, summarizeTestRun, withOverrides };
|
|
1190
|
+
export { AddSpyMethodsByReturnTypes, type ArgCaptor, type AsInstances, type AssertMockedOptions, ClassSpyConfiguration, ClassType, type ConstructorMock, type ConstructorSpy, DeepMockProxy, DeepPartial, type FixtureFactory, type FlushUntilOptions, Func, type IntersectionObserverStubOptions, type MediaElementState, type MediaElementStub, type MediaElementStubOptions, type MockDeepOptions, type ModuleNamespace, type ModuleNamespaceOptions, type MutationRecordInit, type ObserverGlobal, type ObserverInstance, type ObserverStub, type ObserverStubOptions, OnlyMethodKeysOf, type ResizeEntryRect, Spy, SpyOptions, type TestRunComparison, type TestRunReport, type TestRunSummary, UnstubbedCallHandler, asInstance, asInstances, asSpy, assertMocked, captureArg, clearAutoSpy, compareTestRuns, createFixture, createFixtureFactory, createFunctionSpy, createMock, createSpyClass, createSpyFromClass, diffByField, errorHandler, flushEventLoop, flushEventLoopUntil, formatTestRunComparison, intersectionEntry, mockConstructor, mockDeep, moduleNamespace, mutationRecord, narrow, resetAutoSpy, resizeEntry, settleDynamicImport, stubAbortController, stubConstructor, stubIntersectionObserver, stubMediaElement, stubMutationObserver, stubObserver, stubResizeObserver, summarizeTestRun, withOverrides };
|
package/dist/bun.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import './chunk-Y4YG7UOK.js';
|
|
2
2
|
import './chunk-NQXXUJPS.js';
|
|
3
|
-
export { asInstance, asInstances, asSpy, assertMocked, captureArg, compareTestRuns, createMock, createSpyClass, describeDuplicateCopies, diffByField, expectCompletion, expectEmission, expectEmissions, expectError, expectNoEmission, flushEventLoop, flushEventLoopUntil, formatTestRunComparison, getPackageCopies, intersectionEntry, mockConstructor, mockDeep, moduleNamespace, mutationRecord, narrow, resizeEntry, setEmissionTimeout, settleDynamicImport, stubAbortController, stubConstructor, stubIntersectionObserver, stubMediaElement, stubMutationObserver, stubObserver, stubResizeObserver, summarizeTestRun, withOverrides } from './chunk-
|
|
3
|
+
export { asInstance, asInstances, asSpy, assertMocked, captureArg, compareTestRuns, createFixture, createFixtureFactory, createMock, createSpyClass, describeDuplicateCopies, diffByField, expectCompletion, expectEmission, expectEmissions, expectError, expectNoEmission, flushEventLoop, flushEventLoopUntil, formatTestRunComparison, getPackageCopies, intersectionEntry, mockConstructor, mockDeep, moduleNamespace, mutationRecord, narrow, resizeEntry, setEmissionTimeout, settleDynamicImport, stubAbortController, stubConstructor, stubIntersectionObserver, stubMediaElement, stubMutationObserver, stubObserver, stubResizeObserver, summarizeTestRun, withOverrides } from './chunk-FKAXOKP5.js';
|
|
4
4
|
export { countMockedProps, mockAccessorsProp, mockReadonlyProp, mockReadonlyPropGetter, mockValueProp, restoreMockedProps } from './chunk-R4EWAG7Y.js';
|
|
5
5
|
export { autoMocked, clearAutoSpy, createAutoMock, createSpyFromClass, resetAutoSpy } from './chunk-B7PDIHEG.js';
|
|
6
6
|
export { createFunctionSpy, errorHandler } from './chunk-3AWKPZCE.js';
|
|
@@ -9,6 +9,54 @@ function createMock(partial) {
|
|
|
9
9
|
return partial ?? {};
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
// src/lib/fixture.ts
|
|
13
|
+
var FORBIDDEN_KEY = "__proto__";
|
|
14
|
+
function isPlainRecord(value) {
|
|
15
|
+
if (typeof value !== "object" || value === null) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
const prototype = Object.getPrototypeOf(value);
|
|
19
|
+
return prototype === Object.prototype || prototype === null;
|
|
20
|
+
}
|
|
21
|
+
function copyValue(value) {
|
|
22
|
+
if (Array.isArray(value)) {
|
|
23
|
+
return value.map(copyValue);
|
|
24
|
+
}
|
|
25
|
+
return isPlainRecord(value) ? copyRecord(value) : value;
|
|
26
|
+
}
|
|
27
|
+
function copyRecord(record) {
|
|
28
|
+
const copy = {};
|
|
29
|
+
Object.keys(record).forEach((key) => {
|
|
30
|
+
if (key !== FORBIDDEN_KEY) {
|
|
31
|
+
copy[key] = copyValue(record[key]);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
return copy;
|
|
35
|
+
}
|
|
36
|
+
function mergeValue(base, override) {
|
|
37
|
+
if (isPlainRecord(base) && isPlainRecord(override)) {
|
|
38
|
+
return mergeRecord(base, override);
|
|
39
|
+
}
|
|
40
|
+
return copyValue(override);
|
|
41
|
+
}
|
|
42
|
+
function mergeRecord(base, override) {
|
|
43
|
+
const merged = copyRecord(base);
|
|
44
|
+
Object.keys(override).forEach((key) => {
|
|
45
|
+
if (key !== FORBIDDEN_KEY) {
|
|
46
|
+
merged[key] = key in base ? mergeValue(base[key], override[key]) : copyValue(override[key]);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
return merged;
|
|
50
|
+
}
|
|
51
|
+
function createFixture(defaults, overrides) {
|
|
52
|
+
const copy = copyValue(defaults);
|
|
53
|
+
return overrides === void 0 ? copy : mergeValue(copy, overrides);
|
|
54
|
+
}
|
|
55
|
+
function createFixtureFactory(defaults) {
|
|
56
|
+
const pinned = copyValue(defaults);
|
|
57
|
+
return (overrides) => createFixture(pinned, overrides);
|
|
58
|
+
}
|
|
59
|
+
|
|
12
60
|
// src/lib/mock-deep.ts
|
|
13
61
|
function collectKeys(value) {
|
|
14
62
|
const keys = /* @__PURE__ */ new Set();
|
|
@@ -753,4 +801,4 @@ function resizeEntry(target, rect = {}) {
|
|
|
753
801
|
return entry;
|
|
754
802
|
}
|
|
755
803
|
|
|
756
|
-
export { asInstance, asInstances, asSpy, assertMocked, captureArg, compareTestRuns, createMock, createSpyClass, diffByField, flushEventLoop, flushEventLoopUntil, formatTestRunComparison, intersectionEntry, mockConstructor, mockDeep, moduleNamespace, mutationRecord, narrow, resizeEntry, settleDynamicImport, stubAbortController, stubConstructor, stubIntersectionObserver, stubMediaElement, stubMutationObserver, stubObserver, stubResizeObserver, summarizeTestRun, withOverrides };
|
|
804
|
+
export { asInstance, asInstances, asSpy, assertMocked, captureArg, compareTestRuns, createFixture, createFixtureFactory, createMock, createSpyClass, diffByField, flushEventLoop, flushEventLoopUntil, formatTestRunComparison, intersectionEntry, mockConstructor, mockDeep, moduleNamespace, mutationRecord, narrow, resizeEntry, settleDynamicImport, stubAbortController, stubConstructor, stubIntersectionObserver, stubMediaElement, stubMutationObserver, stubObserver, stubResizeObserver, summarizeTestRun, withOverrides };
|