vitest-auto-spy 5.10.0 → 5.10.1

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
@@ -2369,11 +2369,17 @@ Five things to know:
2369
2369
  `provideRouter()` is what tests a navigation.
2370
2370
  - **`emitNavigation()` takes what you have** — nothing, a URL string, or an event you built. A
2371
2371
  `NavigationEnd` moves the URL with it; any other event does not.
2372
- - **The navigation in flight follows the events.** `setCurrentNavigation({ extras: { state } })`
2373
- puts one up and `setCurrentNavigation(null)` ends it; a `NavigationStart` pushed through
2374
- `emitNavigation()` starts one with that event's id, URL and trigger, and a `NavigationEnd`,
2375
- `NavigationCancel`, `NavigationError` or `NavigationSkipped` drops it back to `null` — which is
2376
- what the real router answers to a component reading it while handling a `NavigationEnd`.
2372
+ - **The navigation in flight follows the events, and a terminal event ends it _after_ delivering
2373
+ it.** `setCurrentNavigation({ extras: { state } })` puts one up and `setCurrentNavigation(null)`
2374
+ ends it; a `NavigationStart` pushed through `emitNavigation()` starts one with that event's id,
2375
+ URL and trigger, and a `NavigationEnd`, `NavigationCancel`, `NavigationError` or
2376
+ `NavigationSkipped` drops it back to `null` once its subscribers have run. Angular's doc comment
2377
+ — "the current navigation becomes to null after the NavigationEnd event is emitted" — means
2378
+ _after_ literally: the router emits the terminal event from a `tap` with the navigation still in
2379
+ flight and clears it in the `finalize` below, and `events` is a Subject, so a synchronous
2380
+ subscriber runs between the two. **A component that reads `currentNavigation()` while handling a
2381
+ `NavigationEnd` gets the navigation that just finished**, here and in production; it reads `null`
2382
+ only once `navigate()` has resolved. Probed against a real `provideRouter()` on Angular 22.
2377
2383
  - **Ordering does not matter here.** `Router` is `providedIn: 'root'` and `provideRouter()` does not
2378
2384
  re-provide the token, so the double wins in either order — and a `TestBed` without it hands out a
2379
2385
  real router, which is what `injectRouterDouble()` says by name.
@@ -246,22 +246,22 @@ function createUrlState(initial) {
246
246
  }
247
247
  };
248
248
  }
249
- function followEvent(event, inFlight, navigationOf) {
250
- if (event instanceof NavigationStart) {
251
- inFlight.set(
252
- navigationOf({
253
- id: event.id,
254
- initialUrl: serializer.parse(event.url),
255
- extractedUrl: serializer.parse(event.url),
256
- trigger: event.navigationTrigger ?? "imperative"
257
- })
258
- );
259
- return event.id;
260
- }
261
- if (event instanceof NavigationEnd || event instanceof NavigationCancel || event instanceof NavigationError || event instanceof NavigationSkipped) {
262
- inFlight.set(null);
249
+ function startEvent(event, inFlight, navigationOf) {
250
+ if (!(event instanceof NavigationStart)) {
251
+ return void 0;
263
252
  }
264
- return void 0;
253
+ inFlight.set(
254
+ navigationOf({
255
+ id: event.id,
256
+ initialUrl: serializer.parse(event.url),
257
+ extractedUrl: serializer.parse(event.url),
258
+ trigger: event.navigationTrigger ?? "imperative"
259
+ })
260
+ );
261
+ return event.id;
262
+ }
263
+ function endsNavigation(event) {
264
+ return event instanceof NavigationEnd || event instanceof NavigationCancel || event instanceof NavigationError || event instanceof NavigationSkipped;
265
265
  }
266
266
  function buildNavigation(state, id, named) {
267
267
  return {
@@ -334,8 +334,11 @@ function createRouterDouble(init = {}) {
334
334
  if (next instanceof NavigationEnd) {
335
335
  state.set(next.urlAfterRedirects);
336
336
  }
337
- navigationId = followEvent(next, inFlight, navigationOf) ?? navigationId;
337
+ navigationId = startEvent(next, inFlight, navigationOf) ?? navigationId;
338
338
  events.next(next);
339
+ if (endsNavigation(next)) {
340
+ inFlight.set(null);
341
+ }
339
342
  }
340
343
  };
341
344
  doubles2.set(router, double);
package/dist/cli.js CHANGED
@@ -2453,7 +2453,7 @@ function checkForeignPragma(graph) {
2453
2453
  }
2454
2454
 
2455
2455
  // src/cli/checks/export-map.generated.ts
2456
- var EXPORT_MAP_VERSION = "5.10.0";
2456
+ var EXPORT_MAP_VERSION = "5.10.1";
2457
2457
  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";
2458
2458
  var EXPORTED_BY = {
2459
2459
  AccessorImplementations: "0 1 2 3 4 12 17 18 19",
@@ -757,14 +757,22 @@ function factoryBody(context, value) {
757
757
  }
758
758
  var SPY_FACTORIES = /* @__PURE__ */ new Set([
759
759
  "autoMocked",
760
+ "createActivatedRoute",
760
761
  "createAutoMock",
762
+ "createDocumentDouble",
761
763
  "createMock",
764
+ "createRouterDouble",
762
765
  "createSpyClass",
763
766
  "createSpyFromClass",
767
+ "createWindowDouble",
764
768
  "mockConstructor",
765
769
  "mockDeep",
770
+ "provideActivatedRoute",
766
771
  "provideAutoSpy",
767
- "provideAutoSpyForToken"
772
+ "provideAutoSpyForToken",
773
+ "provideDocumentDouble",
774
+ "provideRouterDouble",
775
+ "provideWindowDouble"
768
776
  ]);
769
777
  function isFactoryCall(node) {
770
778
  return isCallExpression(node) && isIdentifier(node.callee) && SPY_FACTORIES.has(node.callee.name);
@@ -2104,6 +2112,46 @@ function testCall(node) {
2104
2112
  }
2105
2113
  return { skipped: SKIPPED_TESTS.has(chain.root) || chain.members.some((member) => SKIPPED_MEMBERS.has(member)) };
2106
2114
  }
2115
+ var SUBJECT_BUILDERS = /* @__PURE__ */ new Set(["get", "inject"]);
2116
+ function containsCall(node) {
2117
+ let current = node;
2118
+ while (isMemberExpression(current)) {
2119
+ if (isCallExpression(current.object)) {
2120
+ return true;
2121
+ }
2122
+ current = current.object;
2123
+ }
2124
+ return isCallExpression(current);
2125
+ }
2126
+ function namesSubject(subject, matcher) {
2127
+ if (isIdentifier(subject)) {
2128
+ return true;
2129
+ }
2130
+ if (isMemberExpression(subject)) {
2131
+ return !containsCall(subject);
2132
+ }
2133
+ if (!isCallExpression(subject)) {
2134
+ return false;
2135
+ }
2136
+ if (matcher === "toBeInstanceOf") {
2137
+ return false;
2138
+ }
2139
+ if (isIdentifier(subject.callee)) {
2140
+ return subject.arguments.length === 0;
2141
+ }
2142
+ const [token, ...rest] = subject.arguments;
2143
+ return isMemberExpression(subject.callee) && SUBJECT_BUILDERS.has(memberName(subject.callee) ?? "") && !containsCall(subject.callee.object) && rest.length === 0 && token !== void 0 && (isIdentifier(token) || isMemberExpression(token) && !containsCall(token));
2144
+ }
2145
+ function subjectReference(subject, context) {
2146
+ if (isIdentifier(subject) || isMemberExpression(subject)) {
2147
+ return context.sourceCode.getText(subject);
2148
+ }
2149
+ let current = subject;
2150
+ while (isCallExpression(current) || isMemberExpression(current)) {
2151
+ current = isCallExpression(current) ? current.callee : current.object;
2152
+ }
2153
+ return isIdentifier(current) ? current.name : void 0;
2154
+ }
2107
2155
  function existenceSubject(expression) {
2108
2156
  if (!isCallExpression(expression) || !isMemberExpression(expression.callee)) {
2109
2157
  return void 0;
@@ -2119,7 +2167,10 @@ function existenceSubject(expression) {
2119
2167
  return void 0;
2120
2168
  }
2121
2169
  const [subject] = target.arguments;
2122
- return subject && (negated ? MISSING.has(matcher) : EXISTS.has(matcher)) ? subject : void 0;
2170
+ if (!subject || !(negated ? MISSING.has(matcher) : EXISTS.has(matcher))) {
2171
+ return void 0;
2172
+ }
2173
+ return namesSubject(subject, matcher) ? subject : void 0;
2123
2174
  }
2124
2175
  function existenceOnly(callback) {
2125
2176
  const body = callback.body;
@@ -2139,6 +2190,14 @@ function removal2(context, statement) {
2139
2190
  return fixer.replaceTextRange([start, statement.range[1]], "");
2140
2191
  };
2141
2192
  }
2193
+ function recordName(names, node, context) {
2194
+ const text = context.sourceCode.getText(node);
2195
+ for (let scope = enclosingFunction(node); scope; scope = enclosingFunction(scope.parent)) {
2196
+ const seen = names.get(scope) ?? /* @__PURE__ */ new Set();
2197
+ seen.add(text);
2198
+ names.set(scope, seen);
2199
+ }
2200
+ }
2142
2201
  function countNested(blocks) {
2143
2202
  blocks.forEach((block, scope) => {
2144
2203
  let current = scope;
@@ -2146,6 +2205,7 @@ function countNested(blocks) {
2146
2205
  const above = blocks.get(current);
2147
2206
  if (above) {
2148
2207
  above.below += block.proving;
2208
+ above.running.push(...block.running);
2149
2209
  }
2150
2210
  if (current === void 0) {
2151
2211
  return;
@@ -2154,6 +2214,27 @@ function countNested(blocks) {
2154
2214
  }
2155
2215
  });
2156
2216
  }
2217
+ function reportBlock(context, block, names) {
2218
+ if (block.below === 0) {
2219
+ return;
2220
+ }
2221
+ const referenced = new Set(block.running.flatMap((callback) => [...names.get(callback) ?? []]));
2222
+ block.smoke.forEach(({ node, subject }) => {
2223
+ const reference = subjectReference(subject, context);
2224
+ if (reference !== void 0 && !referenced.has(reference)) {
2225
+ return;
2226
+ }
2227
+ const data = {
2228
+ siblings: block.below === 1 ? "another test under the same setup already runs against it" : `${block.below} other tests under the same setup already run against it`,
2229
+ subject: context.sourceCode.getText(subject)
2230
+ };
2231
+ const statement = node.parent;
2232
+ const report = { data, messageId: "noRedundantSmokeTest", node };
2233
+ context.report(
2234
+ isExpressionStatement(statement) ? { ...report, suggest: [{ desc: "Remove this test", fix: removal2(context, statement) }] } : report
2235
+ );
2236
+ });
2237
+ }
2157
2238
  var noRedundantSmokeTest = defineRule({
2158
2239
  anchor: "-a-promise-a-test-forgets-to-await",
2159
2240
  description: "Delete a test whose whole body asserts that the subject exists, where the block already has tests that use it",
@@ -2163,41 +2244,32 @@ var noRedundantSmokeTest = defineRule({
2163
2244
  },
2164
2245
  create: (context) => {
2165
2246
  const blocks = /* @__PURE__ */ new Map();
2247
+ const names = /* @__PURE__ */ new Map();
2166
2248
  return {
2249
+ Identifier: (node) => recordName(names, node, context),
2250
+ MemberExpression: (node) => recordName(names, node, context),
2167
2251
  CallExpression: (node) => {
2168
2252
  const test = testCall(node);
2169
2253
  if (!test) {
2170
2254
  return;
2171
2255
  }
2172
2256
  const scope = enclosingFunction(node);
2173
- const block = blocks.get(scope) ?? { below: 0, proving: 0, smoke: [] };
2257
+ const block = blocks.get(scope) ?? { below: 0, proving: 0, running: [], smoke: [] };
2174
2258
  const callback = node.arguments.find(isFunctionNode);
2175
2259
  const subject = callback && existenceOnly(callback);
2176
2260
  if (subject) {
2177
2261
  block.smoke.push({ node, subject });
2178
2262
  } else if (!test.skipped) {
2179
2263
  block.proving += 1;
2264
+ if (callback) {
2265
+ block.running.push(callback);
2266
+ }
2180
2267
  }
2181
2268
  blocks.set(scope, block);
2182
2269
  },
2183
2270
  "Program:exit": () => {
2184
2271
  countNested(blocks);
2185
- blocks.forEach((block) => {
2186
- if (block.below === 0) {
2187
- return;
2188
- }
2189
- block.smoke.forEach(({ node, subject }) => {
2190
- const data = {
2191
- siblings: block.below === 1 ? "another test under the same setup already runs against it" : `${block.below} other tests under the same setup already run against it`,
2192
- subject: context.sourceCode.getText(subject)
2193
- };
2194
- const statement = node.parent;
2195
- const report = { data, messageId: "noRedundantSmokeTest", node };
2196
- context.report(
2197
- isExpressionStatement(statement) ? { ...report, suggest: [{ desc: "Remove this test", fix: removal2(context, statement) }] } : report
2198
- );
2199
- });
2200
- });
2272
+ blocks.forEach((block) => reportBlock(context, block, names));
2201
2273
  }
2202
2274
  };
2203
2275
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vitest-auto-spy",
3
- "version": "5.10.0",
3
+ "version": "5.10.1",
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",