vitest-auto-spy 5.13.0 → 5.15.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 CHANGED
@@ -201,6 +201,23 @@ asSpy<AppLogger>(logger.channel('app')).info.mockReturnValue(undefined); // →
201
201
  `asInstance` did not take a deep mock before 3.5.0, which left it with nowhere to go: this tree
202
202
  sends you to `mockDeep` when the calls chain, and the result then fitted nothing that expected `T`.
203
203
 
204
+ **`selfReturning: true` chains a factory, not a `return this` builder**, and the difference decides
205
+ where the calls are recorded. A called node answers _itself_, not the object the method was read
206
+ off, so every hop moves one level deeper: `editor.chain().focus().insertContent('text')` records
207
+ `insertContent` on `chain.focus`, while the `chain` handle the spec is holding still has none — and
208
+ `expect(chain.insertContent).toHaveBeenCalled()` reports nothing although the chain ran. Either walk
209
+ the same path in the assertion, or, for an API where every command answers the **same** object
210
+ (tiptap's `ChainedCommands`, a query builder, a `mockReturnThis()` chain), build that object with
211
+ `createAutoMock` instead, where `selfReturning` names methods that answer one double:
212
+
213
+ ```ts
214
+ const chain = createAutoMock<ChainedCommands>(undefined, { selfReturning: ['focus', 'insertContent'] });
215
+ const editor = createAutoMock<Editor>(undefined, { returns: { chain: asInstance(chain) } });
216
+
217
+ editor.chain().focus().insertContent('text').run();
218
+ expect(chain.insertContent).toHaveBeenCalledWith('text'); // one double, so the assertion is the obvious one
219
+ ```
220
+
204
221
  **An `abstract class` is a class.** `abstract class LocalStorage extends AbstractStorage {}`,
205
222
  provided in production as `{ provide: LocalStorage, useClass: BrowserLocalStorage }`, is the
206
223
  standard Angular DI-token idiom, and `provideAutoSpy(LocalStorage)` / `createSpyFromClass(LocalStorage)`
@@ -231,6 +248,13 @@ createAutoMock<NavigationService>({ currentFocus: undefined, navRoot: undefined,
231
248
  This is the way to say "the member exists and is empty", and it is worth writing even when it looks
232
249
  redundant.
233
250
 
251
+ **A getter in `overrides` stays a getter.** The seed is kept as a descriptor rather than read once
252
+ while the double is assembled, so it runs at every read, with the double as `this` — and a seed
253
+ written to **throw** ("this global is missing on this platform") fails where the code under test
254
+ reads the member, not where the provider literal is evaluated. A `{ set }` seed is kept the same way
255
+ and takes the write. Both used to be flattened as the double was built, which turned a throwing
256
+ seed into a failure during `TestBed.configureTestingModule`, three frames from the branch under test.
257
+
234
258
  ### What a Proxy-backed double cannot do
235
259
 
236
260
  `createAutoMock` and `mockDeep` build a Proxy, not an object, and there is one place where the
@@ -3148,7 +3172,7 @@ blanket downgrade so those keep their severity; do not copy the two names into a
3148
3172
  | `prefer-create-spy-from-class` | `error` | — | an object literal of 2+ `vi.fn()`s → `createSpyFromClass` (a factory's own seed is exempt) |
3149
3173
  | `no-stub-class-double` | `warn` | — | a class whose fields are `vi.fn()`s → `createSpyFromClass` / `provideAutoSpy` and delete the stub; a decorated, `implements`-ing, `extends`-ing or unnamed class is exempt — option: `{ minRunnerFns }` |
3150
3174
  | `no-structural-double` | `warn` | — | an object of `vi.fn()`s bound to a name declared `{ load: Mock }` → `createAutoMock<T>()`; a bare `let fn: Mock` is a callback and never reported — option: `{ minRunnerFns }` |
3151
- | `prefer-inject-spy` | `error` | suggest | `vi.spyOn(TestBed.inject(X), 'm')`, inline or via a `const` → `injectSpy(X).m` |
3175
+ | `prefer-inject-spy` | `error` | suggest | `vi.spyOn(TestBed.inject(X), 'm')`, inline or via a `const` → `injectSpy(X).m`; `ApplicationRef`, `DestroyRef`, `EnvironmentInjector`, `HttpClient` and `Injector` keep their real instance — option: `{ ignoreTokens }` |
3152
3176
  | `no-shared-module-level-mock` | `error` | — | an **exported** value holding `vi.fn()`s → export a factory instead |
3153
3177
  | `no-mocked-for-spy` | `error` | `--fix` / suggest | `Mocked<T>` in any type position → `Spy<T>`, import and all — a suggestion where the assigned value is not from a factory of this library |
3154
3178
  | `prefer-as-spy` | `error` | `--fix` | `TestBed.inject(X) as Spy<X>` → `asSpy<X>(TestBed.inject(X))`, import and all |
@@ -3311,6 +3335,15 @@ The variable is resolved through the scope manager, so it has to be a `const`/`l
3311
3335
  from `TestBed.inject(...)` and never assigned again — a name bound by an import, a parameter, or a
3312
3336
  `let` that is reassigned is left alone.
3313
3337
 
3338
+ Five tokens are left alone whatever is spied on them: `ApplicationRef`, `DestroyRef`,
3339
+ `EnvironmentInjector`, `HttpClient`, `Injector`. Each is a case where this rule's advice is
3340
+ impossible or self-defeating — `{ provide: DestroyRef, useValue }` is silently ignored, because
3341
+ `R3Injector.get()` short-circuits on `__NG_ENV_ID__` before it reads its providers; a spied
3342
+ `ApplicationRef` has no `injector` to build a component with; `HttpClient` under
3343
+ `provideHttpClientTesting()` is supposed to be real. A project adds its own with
3344
+ `['error', { ignoreTokens: ['MapRendererService'] }]`, compared as source text, which extends the
3345
+ five rather than replacing them.
3346
+
3314
3347
  `no-inject-before-override` catches the trap this plugin's own advice sets. `TestBed.inject()` in a
3315
3348
  `beforeEach` — the line you write once `provideAutoSpy(X)` has taken away the literal you used to
3316
3349
  configure — instantiates the module, and every `TestBed.override*` afterwards throws, including one
package/README.md CHANGED
@@ -2027,6 +2027,22 @@ expect(svc.apiUrl).toBe('https://api.test'); // or assign: svc.apiUrl = '...'
2027
2027
  > access — an un-seeded property read returns a spy. Seed real property values via `overrides`
2028
2028
  > (or assignment) to get them back verbatim.
2029
2029
 
2030
+ **A getter in `overrides` stays a getter.** The seed keeps its descriptor, so an accessor is
2031
+ installed as one and runs at the read — once per read, with the double as `this` — and a `{ set }`
2032
+ seed takes the write. That is what a seed written to _throw_ needs: "this global is missing on this
2033
+ platform" fails where the code under test reads the member, not while the provider literal is being
2034
+ evaluated.
2035
+
2036
+ ```ts
2037
+ const platform = createAutoMock<PlatformSupport>({
2038
+ get transceiver(): never {
2039
+ throw new TypeError('RTCRtpTransceiver is not defined');
2040
+ },
2041
+ });
2042
+
2043
+ expect(() => platform.transceiver).toThrow(); // at the read, not at `TestBed.configureTestingModule`
2044
+ ```
2045
+
2030
2046
  For a double the code under test only **reads** — a DTO, a route snapshot, a config object — that
2031
2047
  caveat is the wrong trade, and [`createMock<T>()`](#utilities) is the other half of the pair: it
2032
2048
  returns a plain `T` built from the fields you seed, with no spies anywhere.
@@ -3427,7 +3443,7 @@ export can never be.
3427
3443
  | `prefer-create-spy-from-class` | `error` | — | an object literal of two or more `vi.fn()`s → `createSpyFromClass` / `createAutoMock`, unless it is a factory's own seed |
3428
3444
  | `no-stub-class-double` | `warn` | — | a class whose fields are `vi.fn()`s → `createSpyFromClass` / `provideAutoSpy`, the stub class deleted |
3429
3445
  | `no-structural-double` | `warn` | — | an object of `vi.fn()`s bound to a name declared `{ load: Mock }` → `createAutoMock<T>()` |
3430
- | `prefer-inject-spy` | `error` | suggest | `vi.spyOn(TestBed.inject(X), 'm')`, in one step or two → `injectSpy(X).m` |
3446
+ | `prefer-inject-spy` | `error` | suggest | `vi.spyOn(TestBed.inject(X), 'm')`, in one step or two → `injectSpy(X).m`; `ApplicationRef`, `DestroyRef`, `EnvironmentInjector`, `HttpClient` and `Injector` keep their real instance — option: `{ ignoreTokens }` |
3431
3447
  | `no-object-define-property` | `error` | suggest | `Object.defineProperty` in a spec → `mockReadonlyProp` / `mockValueProp` |
3432
3448
  | `no-expect-in-subscribe` | `error` | suggest | `expect()` inside a `subscribe()` callback → `expectEmission` / `firstValueFrom` |
3433
3449
  | `no-shared-module-level-mock` | `error` | — | an **exported** value holding `vi.fn()`s → export a factory that returns it |
package/dist/angular.js CHANGED
@@ -1521,7 +1521,12 @@ function exposeAsSpy(spy) {
1521
1521
  function createProxyPropStore(seed) {
1522
1522
  const store = { values: /* @__PURE__ */ new Map(), accessors: /* @__PURE__ */ new Map(), deleted: /* @__PURE__ */ new Set() };
1523
1523
  for (const key of Reflect.ownKeys(seed)) {
1524
- store.values.set(key, Reflect.get(seed, key));
1524
+ const descriptor = { ...Reflect.getOwnPropertyDescriptor(seed, key) };
1525
+ if ((descriptor.get ?? descriptor.set) !== void 0) {
1526
+ store.accessors.set(key, descriptor);
1527
+ continue;
1528
+ }
1529
+ store.values.set(key, descriptor.value);
1525
1530
  }
1526
1531
  return store;
1527
1532
  }
@@ -1,12 +1,12 @@
1
1
  import { stable, flushEffects } from './chunk-M5BULP7X.js';
2
2
  export { flushEffects, stable } from './chunk-M5BULP7X.js';
3
3
  import './chunk-M6VMOOZ2.js';
4
- export { asInstance, asInstances, asSpy, assertMocked, captureArg, createFixture, createFixtureFactory, createMock, createSpyClass, createSpyFromInstance, describeDuplicateCopies, expectCompletion, expectEmission, expectEmissions, expectError, expectNoEmission, flushEventLoop, flushEventLoopUntil, getPackageCopies, mockConstructor, mockDeep, moduleNamespace, narrow, restoreSpiedInstance, setEmissionTimeout, settleDynamicImport, stubConstructor, withOverrides } from './chunk-ADBB2HQG.js';
4
+ export { asInstance, asInstances, asSpy, assertMocked, captureArg, createFixture, createFixtureFactory, createMock, createSpyClass, createSpyFromInstance, describeDuplicateCopies, expectCompletion, expectEmission, expectEmissions, expectError, expectNoEmission, flushEventLoop, flushEventLoopUntil, getPackageCopies, mockConstructor, mockDeep, moduleNamespace, narrow, restoreSpiedInstance, setEmissionTimeout, settleDynamicImport, stubConstructor, withOverrides } from './chunk-63S25O6W.js';
5
5
  import { currentSpecFile, mockValueProp } from './chunk-N3T7KWWA.js';
6
6
  export { countMockedProps, mockAccessorsProp, mockReadonlyProp, mockReadonlyPropGetter, mockValueProp, reportPropsOutsideHooks, restoreMockedProps } from './chunk-N3T7KWWA.js';
7
- import { createSpyForToken } from './chunk-RPEYIZPL.js';
8
- import { createSpyFromClass } from './chunk-BWT7U4V4.js';
9
- export { autoMocked, clearAutoSpy, clearAutoSpyDefaults, createAutoMock, createSpyFromClass, registerAutoSpyDefaults, resetAutoSpy } from './chunk-BWT7U4V4.js';
7
+ import { createSpyForToken } from './chunk-I6AIKRUJ.js';
8
+ import { createSpyFromClass } from './chunk-C3EFC33U.js';
9
+ export { autoMocked, clearAutoSpy, clearAutoSpyDefaults, createAutoMock, createSpyFromClass, registerAutoSpyDefaults, resetAutoSpy } from './chunk-C3EFC33U.js';
10
10
  import './chunk-OS4PTU6D.js';
11
11
  import './chunk-DTZCPCMA.js';
12
12
  import './chunk-PC3FAZT3.js';
package/dist/bun.d.ts CHANGED
@@ -248,6 +248,13 @@ interface MockDeepOptions {
248
248
  * ```ts
249
249
  * asSpy<QueryBuilder>(query.where('id')).limit.mockReturnValue(query);
250
250
  * ```
251
+ *
252
+ * **A called node answers itself, not its receiver**, which is the difference between a factory
253
+ * and a `return this` builder. `editor.chain().focus().insertContent('x')` therefore records
254
+ * `insertContent` on `chain.focus`, while the `chain` handle the spec holds has no calls at all —
255
+ * the obvious assertion reports nothing although the chain ran. Assert down the path the chain
256
+ * walked, or build the object with `createAutoMock<T>(undefined, { selfReturning: ['focus', …] })`,
257
+ * where the named methods answer one double, which is what an API of this shape does.
251
258
  */
252
259
  selfReturning?: boolean;
253
260
  }
package/dist/bun.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import './chunk-M6VMOOZ2.js';
2
- export { asInstance, asInstances, asSpy, assertMocked, captureArg, createFixture, createFixtureFactory, createMock, createSpyClass, createSpyFromInstance, describeDuplicateCopies, expectCompletion, expectEmission, expectEmissions, expectError, expectNoEmission, flushEventLoop, flushEventLoopUntil, getPackageCopies, mockConstructor, mockDeep, moduleNamespace, narrow, restoreSpiedInstance, setEmissionTimeout, settleDynamicImport, stubConstructor, withOverrides } from './chunk-ADBB2HQG.js';
2
+ export { asInstance, asInstances, asSpy, assertMocked, captureArg, createFixture, createFixtureFactory, createMock, createSpyClass, createSpyFromInstance, describeDuplicateCopies, expectCompletion, expectEmission, expectEmissions, expectError, expectNoEmission, flushEventLoop, flushEventLoopUntil, getPackageCopies, mockConstructor, mockDeep, moduleNamespace, narrow, restoreSpiedInstance, setEmissionTimeout, settleDynamicImport, stubConstructor, withOverrides } from './chunk-63S25O6W.js';
3
3
  export { countMockedProps, mockAccessorsProp, mockReadonlyProp, mockReadonlyPropGetter, mockValueProp, reportPropsOutsideHooks, restoreMockedProps } from './chunk-N3T7KWWA.js';
4
- export { autoMocked, clearAutoSpy, clearAutoSpyDefaults, createAutoMock, createSpyFromClass, registerAutoSpyDefaults, resetAutoSpy } from './chunk-BWT7U4V4.js';
4
+ export { autoMocked, clearAutoSpy, clearAutoSpyDefaults, createAutoMock, createSpyFromClass, registerAutoSpyDefaults, resetAutoSpy } from './chunk-C3EFC33U.js';
5
5
  import './chunk-OS4PTU6D.js';
6
6
  import './chunk-DTZCPCMA.js';
7
7
  import './chunk-PC3FAZT3.js';
@@ -1,5 +1,5 @@
1
1
  import { mockValueProp, mockAccessorsProp } from './chunk-N3T7KWWA.js';
2
- import { resolveConfiguration, mergeMethodNames, getCallableMemberNames, applyConfiguredReturns, createSpyFromClass, resolveAccessors, createAccessorsSpies, createProxyPropStore, storeWriteTraps, describeStoredProp, readStoredAccessor, NOT_STORED, disposeAutoSpy, isProtocolKey, isDeletedProp } from './chunk-BWT7U4V4.js';
2
+ import { resolveConfiguration, mergeMethodNames, getCallableMemberNames, applyConfiguredReturns, createSpyFromClass, resolveAccessors, createAccessorsSpies, createProxyPropStore, storeWriteTraps, describeStoredProp, readStoredAccessor, NOT_STORED, disposeAutoSpy, isProtocolKey, isDeletedProp } from './chunk-C3EFC33U.js';
3
3
  import { resolveReadGuard, createTrackedPropSpy } from './chunk-OS4PTU6D.js';
4
4
  import { redefineFailure } from './chunk-DTZCPCMA.js';
5
5
  import { DISPOSE } from './chunk-PC3FAZT3.js';
@@ -72,7 +72,12 @@ function attachDispose(spy) {
72
72
  function createProxyPropStore(seed) {
73
73
  const store = { values: /* @__PURE__ */ new Map(), accessors: /* @__PURE__ */ new Map(), deleted: /* @__PURE__ */ new Set() };
74
74
  for (const key of Reflect.ownKeys(seed)) {
75
- store.values.set(key, Reflect.get(seed, key));
75
+ const descriptor = { ...Reflect.getOwnPropertyDescriptor(seed, key) };
76
+ if ((descriptor.get ?? descriptor.set) !== void 0) {
77
+ store.accessors.set(key, descriptor);
78
+ continue;
79
+ }
80
+ store.values.set(key, descriptor.value);
76
81
  }
77
82
  return store;
78
83
  }
@@ -1,4 +1,4 @@
1
- import { createSpyFromClass, createAutoMock } from './chunk-BWT7U4V4.js';
1
+ import { createSpyFromClass, createAutoMock } from './chunk-C3EFC33U.js';
2
2
  import { withDocs, DOCS_LINKS } from './chunk-Q477VHHF.js';
3
3
 
4
4
  // src/lib/track-injections.ts
package/dist/cli.js CHANGED
@@ -2455,7 +2455,7 @@ function checkForeignPragma(graph) {
2455
2455
  }
2456
2456
 
2457
2457
  // src/cli/checks/export-map.generated.ts
2458
- var EXPORT_MAP_VERSION = "5.13.0";
2458
+ var EXPORT_MAP_VERSION = "5.15.0";
2459
2459
  var ENTRY_SPECIFIERS = "vitest-auto-spy vitest-auto-spy/bun vitest-auto-spy/bun-angular vitest-auto-spy/node vitest-auto-spy/rstest vitest-auto-spy/rxjs vitest-auto-spy/console vitest-auto-spy/dom-stubs vitest-auto-spy/diagnostics vitest-auto-spy/jasmine vitest-auto-spy/jasmine-compat vitest-auto-spy/observer-spy vitest-auto-spy/angular vitest-auto-spy/angular-http vitest-auto-spy/angular-router vitest-auto-spy/signal-forms vitest-auto-spy/nestjs vitest-auto-spy/react vitest-auto-spy/vue vitest-auto-spy/svelte vitest-auto-spy/setup vitest-auto-spy/zone vitest-auto-spy/eslint-plugin";
2460
2460
  var EXPORTED_BY = {
2461
2461
  AccessorImplementations: "0 1 2 3 4 12 17 18 19",
@@ -4312,6 +4312,17 @@ var NO_PER_TEST = "\u2014";
4312
4312
  var INDENT = " ";
4313
4313
  var GAP = 4;
4314
4314
  var ELLIPSIS = "\u2026";
4315
+ var RED = "\x1B[31m";
4316
+ var DIM = "\x1B[2m";
4317
+ var OFF = "\x1B[0m";
4318
+ var MONOCHROME = { red: (text) => text, dim: (text) => text };
4319
+ var TERMINAL = {
4320
+ red: (text) => `${RED}${text}${OFF}`,
4321
+ dim: (text) => `${DIM}${text}${OFF}`
4322
+ };
4323
+ function colorWanted(env) {
4324
+ return env["NO_COLOR"] === void 0 && env["FORCE_COLOR"] !== "0" && env["TERM"] !== "dumb";
4325
+ }
4315
4326
  function fileHotspots(run, cwd, limit) {
4316
4327
  const found = [];
4317
4328
  for (const [file, measured] of measuredFiles(run, cwd)) {
@@ -4338,28 +4349,58 @@ function caseHotspots(run, cwd, limit) {
4338
4349
  function totalTestMs(run, cwd) {
4339
4350
  return [...measuredFiles(run, cwd).values()].reduce((total, file) => total + file.tests, 0);
4340
4351
  }
4341
- function truncateLeft(text, width) {
4342
- return text.length <= width ? text : `${ELLIPSIS}${text.slice(text.length - width + ELLIPSIS.length)}`;
4352
+ function truncateLeft(text, width, paint) {
4353
+ if (text.length <= width) {
4354
+ return text;
4355
+ }
4356
+ return `${paint.dim(ELLIPSIS)}${text.slice(text.length - width + ELLIPSIS.length)}`;
4357
+ }
4358
+ function truncatePath(path, width, paint) {
4359
+ if (path.length <= width) {
4360
+ return path;
4361
+ }
4362
+ const first = path.indexOf("/");
4363
+ if (first === -1) {
4364
+ return truncateLeft(path, width, paint);
4365
+ }
4366
+ const head = path.slice(0, first);
4367
+ const rest = path.slice(first + 1).split("/");
4368
+ const budget = width - head.length - 3;
4369
+ const tail = [];
4370
+ let used = 0;
4371
+ for (const segment of [...rest].reverse()) {
4372
+ const next = used === 0 ? segment.length : used + 1 + segment.length;
4373
+ if (next > budget) {
4374
+ break;
4375
+ }
4376
+ tail.unshift(segment);
4377
+ used = next;
4378
+ }
4379
+ if (tail.length === 0) {
4380
+ return truncateLeft(path, width, paint);
4381
+ }
4382
+ return `${head}/${paint.dim(ELLIPSIS)}/${tail.join("/")}`;
4343
4383
  }
4344
4384
  function widthOf(head, cells) {
4345
4385
  return cells.reduce((widest, cell) => Math.max(widest, cell.length), head.length);
4346
4386
  }
4347
- function table(label, numbers, width) {
4387
+ function table(label, numbers, width, truncate, decorate) {
4348
4388
  const spent = numbers.reduce((sum, column) => sum + widthOf(column.head, column.cells) + GAP, 0);
4349
4389
  const labelWidth = Math.max(Math.min(widthOf(label.head, label.cells), width - INDENT.length - spent), label.head.length);
4350
4390
  const laid = [
4351
- { width: labelWidth, left: true, texts: [label.head, ...label.cells.map((cell) => truncateLeft(cell, labelWidth))] },
4391
+ { width: labelWidth, left: true, texts: [label.head, ...label.cells.map((cell) => truncate(cell, labelWidth))] },
4352
4392
  ...numbers.map((column) => ({ width: widthOf(column.head, column.cells) + GAP, left: false, texts: [column.head, ...column.cells] }))
4353
4393
  ];
4354
4394
  return laid.reduce(
4355
- (lines, column) => column.texts.map(
4356
- (text, index) => `${lines[index] ?? INDENT}${column.left ? text.padEnd(column.width) : text.padStart(column.width)}`
4395
+ (lines, column, current) => column.texts.map(
4396
+ (text, index) => `${lines[index] ?? INDENT}${decorate(index, current, column.left ? text.padEnd(column.width) : text.padStart(column.width))}`
4357
4397
  ),
4358
4398
  []
4359
4399
  ).join("\n");
4360
4400
  }
4361
- function fileSection(files, total, width) {
4401
+ function fileSection(files, total, width, floorMs, paint) {
4362
4402
  const listed = files.reduce((sum, hotspot) => sum + hotspot.ms, 0);
4403
+ const overBudget = new Set(files.flatMap((hotspot, index) => hotspot.testCount > 0 && hotspot.perTest >= floorMs ? [index + 1] : []));
4363
4404
  const drawn = table(
4364
4405
  { head: "file", cells: files.map((hotspot) => hotspot.file) },
4365
4406
  [
@@ -4367,7 +4408,9 @@ function fileSection(files, total, width) {
4367
4408
  { head: "ms/test", cells: files.map((hotspot) => hotspot.testCount === 0 ? NO_PER_TEST : formatMs(hotspot.perTest)) },
4368
4409
  { head: "share", cells: files.map((hotspot) => formatShare(hotspot.ms / total)) }
4369
4410
  ],
4370
- width
4411
+ width,
4412
+ (cell, columnWidth) => truncatePath(cell, columnWidth, paint),
4413
+ (row, column, padded) => row === 0 ? paint.dim(padded) : column === 2 && overBudget.has(row) ? paint.red(padded) : padded
4371
4414
  );
4372
4415
  const counted = files.length === 1 ? "That one file is" : `Those ${files.length} files are`;
4373
4416
  const prose = [
@@ -4377,27 +4420,51 @@ function fileSection(files, total, width) {
4377
4420
  ].join("\n");
4378
4421
  return ["slowest files \u2014 what every body in the file cost, and what one of them cost", drawn, prose];
4379
4422
  }
4380
- function caseSection(cases, width) {
4423
+ function caseSection(cases, width, floorMs, paint) {
4381
4424
  if (cases.length === 0) {
4382
4425
  return [];
4383
4426
  }
4427
+ const timeWidth = widthOf(
4428
+ "time",
4429
+ cases.map((entry) => formatMs(entry.ms))
4430
+ ) + GAP;
4431
+ const nameWidth = Math.max(width - 2 * INDENT.length - timeWidth, "test".length);
4432
+ const groups = /* @__PURE__ */ new Map();
4433
+ for (const entry of cases) {
4434
+ const bucket = groups.get(entry.file);
4435
+ bucket === void 0 ? groups.set(entry.file, [entry]) : bucket.push(entry);
4436
+ }
4437
+ const lines = [];
4438
+ for (const [file, entries] of groups) {
4439
+ lines.push(`${INDENT}${truncatePath(file, width - INDENT.length, paint)}`);
4440
+ for (const entry of entries) {
4441
+ const time = formatMs(entry.ms).padStart(timeWidth);
4442
+ lines.push(
4443
+ `${INDENT}${INDENT}${truncateLeft(entry.name, nameWidth, paint).padEnd(nameWidth)}${entry.ms >= floorMs ? paint.red(time) : time}`
4444
+ );
4445
+ }
4446
+ }
4384
4447
  return [
4385
4448
  `slowest test bodies \u2014 a body under ${formatMs(CASE_FLOOR_MS)} is not in the report at all, so a fast one is absent rather than cheap`,
4386
- table(
4387
- { head: "test", cells: cases.map((entry) => `${entry.file} \u203A ${entry.name}`) },
4388
- [{ head: "time", cells: cases.map((entry) => formatMs(entry.ms)) }],
4389
- width
4390
- )
4449
+ lines.join("\n")
4391
4450
  ];
4392
4451
  }
4393
4452
  function formatHotspots(run, cwd, options = {}) {
4394
- const { limit, floorMs, width } = { ...HOTSPOT_DEFAULTS, ...options };
4453
+ const { limit, floorMs, width, colors } = { ...HOTSPOT_DEFAULTS, ...options };
4454
+ const paint = withColors(colors ?? colorWanted(process.env));
4395
4455
  const files = fileHotspots(run, cwd, limit);
4396
4456
  const [slowest] = files;
4397
4457
  if (slowest === void 0 || slowest.ms < floorMs) {
4398
4458
  return "";
4399
4459
  }
4400
- return [...fileSection(files, totalTestMs(run, cwd), width), ...caseSection(caseHotspots(run, cwd, limit), width)].join("\n\n");
4460
+ const sections = [
4461
+ ...fileSection(files, totalTestMs(run, cwd), width, floorMs, paint),
4462
+ ...caseSection(caseHotspots(run, cwd, limit), width, floorMs, paint)
4463
+ ];
4464
+ return sections.join("\n\n");
4465
+ }
4466
+ function withColors(on) {
4467
+ return on ? TERMINAL : MONOCHROME;
4401
4468
  }
4402
4469
  function hotspotFloorNote(run, cwd, floorMs = HOTSPOT_DEFAULTS.floorMs) {
4403
4470
  const [slowest] = fileHotspots(run, cwd, 1);
@@ -952,6 +952,23 @@ function injectedFromVariable(context, target) {
952
952
  const initializer = initializerOf(context.sourceCode.getScope(target), target);
953
953
  return initializer && isTestBedInject(initializer) ? initializer : void 0;
954
954
  }
955
+ var KEPT_REAL_TOKENS = ["ApplicationRef", "DestroyRef", "EnvironmentInjector", "HttpClient", "Injector"];
956
+ var INJECTED_SPY_SCHEMA = [
957
+ {
958
+ type: "object",
959
+ properties: { ignoreTokens: { type: "array", items: { type: "string" }, uniqueItems: true } },
960
+ additionalProperties: false
961
+ }
962
+ ];
963
+ function keepsTheRealInstance(context, injectCall) {
964
+ const [token] = injectCall.arguments;
965
+ if (!token) {
966
+ return false;
967
+ }
968
+ const text = context.sourceCode.getText(token);
969
+ const configured = Reflect.get(Object(context.options[0]), "ignoreTokens");
970
+ return KEPT_REAL_TOKENS.includes(text) || Array.isArray(configured) && configured.includes(text);
971
+ }
955
972
  function memberName2(node) {
956
973
  const value = node?.type === "Literal" ? Reflect.get(node, "value") : void 0;
957
974
  return typeof value === "string" && /^[$A-Z_a-z][\w$]*$/.test(value) ? value : void 0;
@@ -3007,8 +3024,9 @@ var preferInjectSpy = defineRule({
3007
3024
  anchor: "-reading-a-spy-back-from-di",
3008
3025
  description: "Read an already-spied dependency with injectSpy() instead of re-spying a TestBed.inject() result",
3009
3026
  hasSuggestions: true,
3027
+ schema: INJECTED_SPY_SCHEMA,
3010
3028
  messages: {
3011
- preferInjectSpy: "Spying the instance DI just handed you replaces one method and leaves the rest real. Provide it with `provideAutoSpy(X)` and read it back with `injectSpy(X)`."
3029
+ preferInjectSpy: "Spying the instance DI just handed you replaces one method and leaves the rest real. Provide it with `provideAutoSpy(X)` and read it back with `injectSpy(X)`. When the real instance is what the spec came for \u2014 the spy holds one method of an object whose other half has to keep working, or the token cannot be substituted at all, as `DestroyRef` cannot (it carries `__NG_ENV_ID__`, so `R3Injector.get()` answers before it reads its own providers and `{ provide: DestroyRef, useValue }` is silently ignored) \u2014 that token belongs in `{ ignoreTokens: [...] }`, which is a statement about the token and survives the next edit, rather than behind a per-line disable."
3012
3030
  },
3013
3031
  create: (context) => ({
3014
3032
  'CallExpression[callee.object.name="vi"][callee.property.name="spyOn"]': (node) => {
@@ -3017,7 +3035,7 @@ var preferInjectSpy = defineRule({
3017
3035
  return;
3018
3036
  }
3019
3037
  const injectCall = isTestBedInject(target) ? target : injectedFromVariable(context, target);
3020
- if (!injectCall) {
3038
+ if (!injectCall || keepsTheRealInstance(context, injectCall)) {
3021
3039
  return;
3022
3040
  }
3023
3041
  const suggestion = injectSpySuggestion(context, node, injectCall);
package/dist/index.js CHANGED
@@ -1502,7 +1502,12 @@ function createAccessorsSpies(autoSpy, gettersToSpyOn, settersToSpyOn, reads) {
1502
1502
  function createProxyPropStore(seed) {
1503
1503
  const store = { values: /* @__PURE__ */ new Map(), accessors: /* @__PURE__ */ new Map(), deleted: /* @__PURE__ */ new Set() };
1504
1504
  for (const key of Reflect.ownKeys(seed)) {
1505
- store.values.set(key, Reflect.get(seed, key));
1505
+ const descriptor = { ...Reflect.getOwnPropertyDescriptor(seed, key) };
1506
+ if ((descriptor.get ?? descriptor.set) !== void 0) {
1507
+ store.accessors.set(key, descriptor);
1508
+ continue;
1509
+ }
1510
+ store.values.set(key, descriptor.value);
1506
1511
  }
1507
1512
  return store;
1508
1513
  }
package/dist/jasmine.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { useVitestAdapter } from './chunk-PA4ZNLNZ.js';
2
2
  import { enableJasmineCompat } from './chunk-MAKQSO2R.js';
3
3
  export { enableJasmineCompat } from './chunk-MAKQSO2R.js';
4
- import { createSpyFromClass } from './chunk-BWT7U4V4.js';
4
+ import { createSpyFromClass } from './chunk-C3EFC33U.js';
5
5
  import './chunk-OS4PTU6D.js';
6
6
  import './chunk-ZU55UY2M.js';
7
7
  import './chunk-33NJHIS2.js';
package/dist/nestjs.js CHANGED
@@ -1,6 +1,6 @@
1
- import { createSpyForToken } from './chunk-RPEYIZPL.js';
2
- export { trackInjections } from './chunk-RPEYIZPL.js';
3
- import { createSpyFromClass } from './chunk-BWT7U4V4.js';
1
+ import { createSpyForToken } from './chunk-I6AIKRUJ.js';
2
+ export { trackInjections } from './chunk-I6AIKRUJ.js';
3
+ import { createSpyFromClass } from './chunk-C3EFC33U.js';
4
4
  import './chunk-OS4PTU6D.js';
5
5
  import { vitestMockAdapter } from './chunk-ZU55UY2M.js';
6
6
  import './chunk-33NJHIS2.js';
package/dist/node.cjs CHANGED
@@ -1222,7 +1222,12 @@ var DISPOSE = installDisposeSymbol(Symbol);
1222
1222
  function createProxyPropStore(seed) {
1223
1223
  const store = { values: /* @__PURE__ */ new Map(), accessors: /* @__PURE__ */ new Map(), deleted: /* @__PURE__ */ new Set() };
1224
1224
  for (const key of Reflect.ownKeys(seed)) {
1225
- store.values.set(key, Reflect.get(seed, key));
1225
+ const descriptor = { ...Reflect.getOwnPropertyDescriptor(seed, key) };
1226
+ if ((descriptor.get ?? descriptor.set) !== void 0) {
1227
+ store.accessors.set(key, descriptor);
1228
+ continue;
1229
+ }
1230
+ store.values.set(key, descriptor.value);
1226
1231
  }
1227
1232
  return store;
1228
1233
  }
package/dist/node.js CHANGED
@@ -1,6 +1,6 @@
1
- export { asInstance, asInstances, asSpy, assertMocked, captureArg, createFixture, createFixtureFactory, createMock, createSpyClass, createSpyFromInstance, describeDuplicateCopies, expectCompletion, expectEmission, expectEmissions, expectError, expectNoEmission, flushEventLoop, flushEventLoopUntil, getPackageCopies, mockConstructor, mockDeep, moduleNamespace, narrow, restoreSpiedInstance, setEmissionTimeout, settleDynamicImport, stubConstructor, withOverrides } from './chunk-ADBB2HQG.js';
1
+ export { asInstance, asInstances, asSpy, assertMocked, captureArg, createFixture, createFixtureFactory, createMock, createSpyClass, createSpyFromInstance, describeDuplicateCopies, expectCompletion, expectEmission, expectEmissions, expectError, expectNoEmission, flushEventLoop, flushEventLoopUntil, getPackageCopies, mockConstructor, mockDeep, moduleNamespace, narrow, restoreSpiedInstance, setEmissionTimeout, settleDynamicImport, stubConstructor, withOverrides } from './chunk-63S25O6W.js';
2
2
  export { countMockedProps, mockAccessorsProp, mockReadonlyProp, mockReadonlyPropGetter, mockValueProp, reportPropsOutsideHooks, restoreMockedProps } from './chunk-N3T7KWWA.js';
3
- export { autoMocked, clearAutoSpy, clearAutoSpyDefaults, createAutoMock, createSpyFromClass, registerAutoSpyDefaults, resetAutoSpy } from './chunk-BWT7U4V4.js';
3
+ export { autoMocked, clearAutoSpy, clearAutoSpyDefaults, createAutoMock, createSpyFromClass, registerAutoSpyDefaults, resetAutoSpy } from './chunk-C3EFC33U.js';
4
4
  import './chunk-OS4PTU6D.js';
5
5
  import { guardAccessorSpies, createRedefineMockAdapter } from './chunk-DTZCPCMA.js';
6
6
  import './chunk-PC3FAZT3.js';
package/dist/react.js CHANGED
@@ -1,6 +1,6 @@
1
- export { asInstance, asInstances, asSpy, assertMocked, captureArg, createFixture, createFixtureFactory, createMock, createSpyClass, createSpyFromInstance, describeDuplicateCopies, expectCompletion, expectEmission, expectEmissions, expectError, expectNoEmission, flushEventLoop, flushEventLoopUntil, getPackageCopies, mockConstructor, mockDeep, moduleNamespace, narrow, restoreSpiedInstance, setEmissionTimeout, settleDynamicImport, stubConstructor, withOverrides } from './chunk-ADBB2HQG.js';
1
+ export { asInstance, asInstances, asSpy, assertMocked, captureArg, createFixture, createFixtureFactory, createMock, createSpyClass, createSpyFromInstance, describeDuplicateCopies, expectCompletion, expectEmission, expectEmissions, expectError, expectNoEmission, flushEventLoop, flushEventLoopUntil, getPackageCopies, mockConstructor, mockDeep, moduleNamespace, narrow, restoreSpiedInstance, setEmissionTimeout, settleDynamicImport, stubConstructor, withOverrides } from './chunk-63S25O6W.js';
2
2
  export { countMockedProps, mockAccessorsProp, mockReadonlyProp, mockReadonlyPropGetter, mockValueProp, reportPropsOutsideHooks, restoreMockedProps } from './chunk-N3T7KWWA.js';
3
- export { autoMocked, clearAutoSpy, clearAutoSpyDefaults, createAutoMock, createSpyFromClass, registerAutoSpyDefaults, resetAutoSpy } from './chunk-BWT7U4V4.js';
3
+ export { autoMocked, clearAutoSpy, clearAutoSpyDefaults, createAutoMock, createSpyFromClass, registerAutoSpyDefaults, resetAutoSpy } from './chunk-C3EFC33U.js';
4
4
  import './chunk-OS4PTU6D.js';
5
5
  import { vitestMockAdapter } from './chunk-ZU55UY2M.js';
6
6
  import './chunk-33NJHIS2.js';
package/dist/rstest.js CHANGED
@@ -1,6 +1,6 @@
1
- export { asInstance, asInstances, asSpy, assertMocked, captureArg, createFixture, createFixtureFactory, createMock, createSpyClass, createSpyFromInstance, describeDuplicateCopies, expectCompletion, expectEmission, expectEmissions, expectError, expectNoEmission, flushEventLoop, flushEventLoopUntil, getPackageCopies, mockConstructor, mockDeep, moduleNamespace, narrow, restoreSpiedInstance, setEmissionTimeout, settleDynamicImport, stubConstructor, withOverrides } from './chunk-ADBB2HQG.js';
1
+ export { asInstance, asInstances, asSpy, assertMocked, captureArg, createFixture, createFixtureFactory, createMock, createSpyClass, createSpyFromInstance, describeDuplicateCopies, expectCompletion, expectEmission, expectEmissions, expectError, expectNoEmission, flushEventLoop, flushEventLoopUntil, getPackageCopies, mockConstructor, mockDeep, moduleNamespace, narrow, restoreSpiedInstance, setEmissionTimeout, settleDynamicImport, stubConstructor, withOverrides } from './chunk-63S25O6W.js';
2
2
  export { countMockedProps, mockAccessorsProp, mockReadonlyProp, mockReadonlyPropGetter, mockValueProp, reportPropsOutsideHooks, restoreMockedProps } from './chunk-N3T7KWWA.js';
3
- export { autoMocked, clearAutoSpy, clearAutoSpyDefaults, createAutoMock, createSpyFromClass, registerAutoSpyDefaults, resetAutoSpy } from './chunk-BWT7U4V4.js';
3
+ export { autoMocked, clearAutoSpy, clearAutoSpyDefaults, createAutoMock, createSpyFromClass, registerAutoSpyDefaults, resetAutoSpy } from './chunk-C3EFC33U.js';
4
4
  import './chunk-OS4PTU6D.js';
5
5
  import { clearAllFastSpies, resetAllFastSpies, getSpyEngine, createFastSpy } from './chunk-33NJHIS2.js';
6
6
  import { guardAccessorSpies, spyOnAccessorByRedefine } from './chunk-DTZCPCMA.js';
package/dist/svelte.js CHANGED
@@ -1,6 +1,6 @@
1
- export { asInstance, asInstances, asSpy, assertMocked, captureArg, createFixture, createFixtureFactory, createMock, createSpyClass, createSpyFromInstance, describeDuplicateCopies, expectCompletion, expectEmission, expectEmissions, expectError, expectNoEmission, flushEventLoop, flushEventLoopUntil, getPackageCopies, mockConstructor, mockDeep, moduleNamespace, narrow, restoreSpiedInstance, setEmissionTimeout, settleDynamicImport, stubConstructor, withOverrides } from './chunk-ADBB2HQG.js';
1
+ export { asInstance, asInstances, asSpy, assertMocked, captureArg, createFixture, createFixtureFactory, createMock, createSpyClass, createSpyFromInstance, describeDuplicateCopies, expectCompletion, expectEmission, expectEmissions, expectError, expectNoEmission, flushEventLoop, flushEventLoopUntil, getPackageCopies, mockConstructor, mockDeep, moduleNamespace, narrow, restoreSpiedInstance, setEmissionTimeout, settleDynamicImport, stubConstructor, withOverrides } from './chunk-63S25O6W.js';
2
2
  export { countMockedProps, mockAccessorsProp, mockReadonlyProp, mockReadonlyPropGetter, mockValueProp, reportPropsOutsideHooks, restoreMockedProps } from './chunk-N3T7KWWA.js';
3
- export { autoMocked, clearAutoSpy, clearAutoSpyDefaults, createAutoMock, createSpyFromClass, registerAutoSpyDefaults, resetAutoSpy } from './chunk-BWT7U4V4.js';
3
+ export { autoMocked, clearAutoSpy, clearAutoSpyDefaults, createAutoMock, createSpyFromClass, registerAutoSpyDefaults, resetAutoSpy } from './chunk-C3EFC33U.js';
4
4
  import './chunk-OS4PTU6D.js';
5
5
  import { vitestMockAdapter } from './chunk-ZU55UY2M.js';
6
6
  import './chunk-33NJHIS2.js';
package/dist/vue.js CHANGED
@@ -1,7 +1,7 @@
1
- export { asInstance, asInstances, asSpy, assertMocked, captureArg, createFixture, createFixtureFactory, createMock, createSpyClass, createSpyFromInstance, describeDuplicateCopies, expectCompletion, expectEmission, expectEmissions, expectError, expectNoEmission, flushEventLoop, flushEventLoopUntil, getPackageCopies, mockConstructor, mockDeep, moduleNamespace, narrow, restoreSpiedInstance, setEmissionTimeout, settleDynamicImport, stubConstructor, withOverrides } from './chunk-ADBB2HQG.js';
1
+ export { asInstance, asInstances, asSpy, assertMocked, captureArg, createFixture, createFixtureFactory, createMock, createSpyClass, createSpyFromInstance, describeDuplicateCopies, expectCompletion, expectEmission, expectEmissions, expectError, expectNoEmission, flushEventLoop, flushEventLoopUntil, getPackageCopies, mockConstructor, mockDeep, moduleNamespace, narrow, restoreSpiedInstance, setEmissionTimeout, settleDynamicImport, stubConstructor, withOverrides } from './chunk-63S25O6W.js';
2
2
  export { countMockedProps, mockAccessorsProp, mockReadonlyProp, mockReadonlyPropGetter, mockValueProp, reportPropsOutsideHooks, restoreMockedProps } from './chunk-N3T7KWWA.js';
3
- import { createSpyFromClass } from './chunk-BWT7U4V4.js';
4
- export { autoMocked, clearAutoSpy, clearAutoSpyDefaults, createAutoMock, createSpyFromClass, registerAutoSpyDefaults, resetAutoSpy } from './chunk-BWT7U4V4.js';
3
+ import { createSpyFromClass } from './chunk-C3EFC33U.js';
4
+ export { autoMocked, clearAutoSpy, clearAutoSpyDefaults, createAutoMock, createSpyFromClass, registerAutoSpyDefaults, resetAutoSpy } from './chunk-C3EFC33U.js';
5
5
  import './chunk-OS4PTU6D.js';
6
6
  import { vitestMockAdapter } from './chunk-ZU55UY2M.js';
7
7
  import './chunk-33NJHIS2.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vitest-auto-spy",
3
- "version": "5.13.0",
3
+ "version": "5.15.0",
4
4
  "description": "Auto-generate fully-typed test spies from a class — across Vitest, Bun, node:test and Rstest, with Angular/NestJS/React/Vue/Svelte recipes. Drop-in replacement for jest-auto-spies and jasmine-auto-spies.",
5
5
  "keywords": [
6
6
  "auto-mock",