@frontmcp/testing 1.8.6 → 1.8.7

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.
Files changed (39) hide show
  1. package/auth/mock-oauth-server.d.ts.map +1 -1
  2. package/auth/token-factory.d.ts +2 -2
  3. package/auth/token-factory.d.ts.map +1 -1
  4. package/client/mcp-test-client.d.ts +6 -1
  5. package/client/mcp-test-client.d.ts.map +1 -1
  6. package/esm/fixtures/index.mjs +487 -224
  7. package/esm/index.mjs +644 -347
  8. package/esm/package.json +6 -6
  9. package/esm/perf/index.mjs +407 -272
  10. package/expect.d.ts +12 -11
  11. package/expect.d.ts.map +1 -1
  12. package/fixtures/fixture-types.d.ts +22 -4
  13. package/fixtures/fixture-types.d.ts.map +1 -1
  14. package/fixtures/index.js +486 -216
  15. package/fixtures/test-fixture.d.ts +6 -5
  16. package/fixtures/test-fixture.d.ts.map +1 -1
  17. package/http-mock/http-mock.d.ts +5 -3
  18. package/http-mock/http-mock.d.ts.map +1 -1
  19. package/http-mock/http-mock.types.d.ts +17 -6
  20. package/http-mock/http-mock.types.d.ts.map +1 -1
  21. package/http-mock/index.d.ts +1 -1
  22. package/http-mock/index.d.ts.map +1 -1
  23. package/index.d.ts +1 -1
  24. package/index.d.ts.map +1 -1
  25. package/index.js +652 -348
  26. package/package.json +6 -6
  27. package/perf/baseline-store.d.ts +1 -1
  28. package/perf/baseline-store.d.ts.map +1 -1
  29. package/perf/index.js +410 -285
  30. package/perf/perf-test.d.ts.map +1 -1
  31. package/perf/report-generator.d.ts +1 -1
  32. package/perf/report-generator.d.ts.map +1 -1
  33. package/server/port-registry.d.ts +7 -1
  34. package/server/port-registry.d.ts.map +1 -1
  35. package/server/test-server.d.ts +10 -1
  36. package/server/test-server.d.ts.map +1 -1
  37. package/transport/error-utils.d.ts +15 -0
  38. package/transport/error-utils.d.ts.map +1 -0
  39. package/transport/streamable-http.transport.d.ts.map +1 -1
@@ -5,16 +5,6 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
5
5
  throw Error('Dynamic require of "' + x + '" is not supported');
6
6
  });
7
7
 
8
- // libs/testing/src/fixtures/test-fixture.ts
9
- import {
10
- afterAll as _afterAll,
11
- afterEach as _afterEach,
12
- beforeAll as _beforeAll,
13
- beforeEach as _beforeEach,
14
- describe as _describe,
15
- it as _it
16
- } from "@jest/globals";
17
-
18
8
  // libs/testing/src/auth/token-factory.ts
19
9
  import { exportJWK, generateKeyPair, SignJWT } from "jose";
20
10
  var TestTokenFactory = class {
@@ -73,14 +63,15 @@ var TestTokenFactory = class {
73
63
  */
74
64
  async createTestToken(options) {
75
65
  await this.ensureKeys();
76
- const now = Math.floor(Date.now() / 1e3);
66
+ const nowMs = Date.now();
67
+ const iat = Math.floor(nowMs / 1e3);
77
68
  const exp = options.exp ?? 3600;
78
69
  const payload = {
79
70
  iss: options.iss ?? this.issuer,
80
71
  sub: options.sub,
81
72
  aud: options.aud ?? this.audience,
82
- iat: now,
83
- exp: now + exp,
73
+ iat,
74
+ exp: Math.ceil(nowMs / 1e3) + exp,
84
75
  scope: options.scopes?.join(" "),
85
76
  ...options.claims
86
77
  };
@@ -105,10 +96,10 @@ var TestTokenFactory = class {
105
96
  /**
106
97
  * Create a regular user token
107
98
  */
108
- async createUserToken(sub = "user-001", scopes = ["read", "write"]) {
99
+ async createUserToken(sub = "user-001", scopes2 = ["read", "write"]) {
109
100
  return this.createTestToken({
110
101
  sub,
111
- scopes,
102
+ scopes: scopes2,
112
103
  claims: {
113
104
  email: "user@test.local",
114
105
  name: "Test User",
@@ -119,10 +110,11 @@ var TestTokenFactory = class {
119
110
  /**
120
111
  * Create an anonymous user token
121
112
  */
122
- async createAnonymousToken() {
113
+ async createAnonymousToken(expiresIn) {
123
114
  return this.createTestToken({
124
115
  sub: `anon:${Date.now()}`,
125
116
  scopes: ["anonymous"],
117
+ exp: expiresIn,
126
118
  claims: {
127
119
  name: "Anonymous",
128
120
  role: "anonymous"
@@ -471,6 +463,32 @@ function sleep(ms) {
471
463
  return new Promise((resolve) => setTimeout(resolve, ms));
472
464
  }
473
465
 
466
+ // libs/testing/src/transport/error-utils.ts
467
+ function errorName(err) {
468
+ if (typeof err !== "object" || err === null) return void 0;
469
+ const name = err.name;
470
+ return typeof name === "string" ? name : void 0;
471
+ }
472
+ function isAbortError(err) {
473
+ return errorName(err) === "AbortError";
474
+ }
475
+ function errorMessage(err, fallback = "Unknown error") {
476
+ if (typeof err === "string" && err) return err;
477
+ if (typeof err === "object" && err !== null) {
478
+ const message = err.message;
479
+ if (typeof message === "string" && message) return message;
480
+ }
481
+ return fallback;
482
+ }
483
+ var interceptedErrors = /* @__PURE__ */ new WeakSet();
484
+ function markInterceptedError(err) {
485
+ interceptedErrors.add(err);
486
+ return err;
487
+ }
488
+ function isInterceptedError(err) {
489
+ return typeof err === "object" && err !== null && interceptedErrors.has(err);
490
+ }
491
+
474
492
  // libs/testing/src/transport/streamable-http.transport.ts
475
493
  var DEFAULT_TIMEOUT = 3e4;
476
494
  var NOTIFICATION_STREAM_REOPEN_DELAY_MS = 250;
@@ -579,7 +597,7 @@ var StreamableHttpTransport = class {
579
597
  return mockResponse2;
580
598
  }
581
599
  case "error":
582
- throw interceptResult.error;
600
+ throw markInterceptedError(interceptResult.error);
583
601
  case "continue":
584
602
  message = interceptResult.request;
585
603
  break;
@@ -640,7 +658,7 @@ var StreamableHttpTransport = class {
640
658
  return jsonResponse;
641
659
  } catch (error) {
642
660
  clearTimeout(timeoutId);
643
- if (error instanceof Error && error.name === "AbortError") {
661
+ if (isAbortError(error)) {
644
662
  return {
645
663
  jsonrpc: "2.0",
646
664
  id: message.id ?? null,
@@ -679,7 +697,7 @@ var StreamableHttpTransport = class {
679
697
  }
680
698
  } catch (error) {
681
699
  clearTimeout(timeoutId);
682
- if (error instanceof Error && error.name !== "AbortError") {
700
+ if (!isAbortError(error)) {
683
701
  throw error;
684
702
  }
685
703
  }
@@ -720,7 +738,7 @@ var StreamableHttpTransport = class {
720
738
  error: {
721
739
  code: -32700,
722
740
  message: "Parse error",
723
- data: error instanceof Error ? error.message : "Unknown error"
741
+ data: errorMessage(error)
724
742
  }
725
743
  };
726
744
  }
@@ -1090,7 +1108,7 @@ var StreamableHttpTransport = class {
1090
1108
  if (this.config.clientInfo) {
1091
1109
  headers["User-Agent"] = `${this.config.clientInfo.name}/${this.config.clientInfo.version}`;
1092
1110
  }
1093
- if (this.authToken && !this.publicMode) {
1111
+ if (this.authToken) {
1094
1112
  headers["Authorization"] = `Bearer ${this.authToken}`;
1095
1113
  }
1096
1114
  if (this.sessionId) {
@@ -1414,6 +1432,15 @@ var DEFAULT_CLIENT_INFO = {
1414
1432
  version: "0.4.0"
1415
1433
  };
1416
1434
  var MAX_LIST_PAGES = 1e3;
1435
+ function isAuthFailure(error) {
1436
+ return typeof error?.message === "string" && /^HTTP (401|403)\b/.test(error.message);
1437
+ }
1438
+ function describeInitializeError(error) {
1439
+ const message = error?.message ?? "Unknown error";
1440
+ const data = error?.data;
1441
+ const detail = typeof data === "string" ? data.trim() : "";
1442
+ return detail ? `${message} - ${detail.slice(0, 500)}` : message;
1443
+ }
1417
1444
  function extractEntryPaths(data) {
1418
1445
  let payload = data;
1419
1446
  if (typeof payload === "string") {
@@ -1500,19 +1527,22 @@ var McpTestClient = class {
1500
1527
  async connect() {
1501
1528
  this.log("debug", `Connecting to ${this.config.baseUrl}...`);
1502
1529
  this.triedDiscoveredEntryPath = false;
1530
+ this.initResult = null;
1503
1531
  this.transport = this.createTransport();
1504
1532
  await this.transport.connect();
1505
1533
  const maxRetries = 3;
1506
1534
  const retryDelayMs = 500;
1507
1535
  let lastError;
1508
1536
  let reportedEntryPaths;
1537
+ let attemptsMade = 0;
1509
1538
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
1539
+ attemptsMade = attempt;
1510
1540
  const initResponse = await this.initialize();
1511
1541
  if (initResponse.success && initResponse.data) {
1512
1542
  this.initResult = initResponse.data;
1513
1543
  break;
1514
1544
  }
1515
- lastError = initResponse.error?.message ?? "Unknown error";
1545
+ lastError = describeInitializeError(initResponse.error);
1516
1546
  reportedEntryPaths = extractEntryPaths(initResponse.error?.data) ?? reportedEntryPaths;
1517
1547
  if (reportedEntryPaths?.length === 1 && !this.triedDiscoveredEntryPath) {
1518
1548
  this.triedDiscoveredEntryPath = true;
@@ -1524,6 +1554,7 @@ var McpTestClient = class {
1524
1554
  await this.transport.connect();
1525
1555
  continue;
1526
1556
  }
1557
+ if (isAuthFailure(initResponse.error)) break;
1527
1558
  if (attempt < maxRetries) {
1528
1559
  this.log("debug", `MCP init attempt ${attempt} failed (${lastError}), retrying in ${retryDelayMs}ms...`);
1529
1560
  await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
@@ -1532,7 +1563,7 @@ var McpTestClient = class {
1532
1563
  if (!this.initResult) {
1533
1564
  const served = reportedEntryPaths?.length ? ` The server reports it serves MCP at ${reportedEntryPaths.join(", ")}; set \`entryPath\` in test.use() (or \`http.entryPath\` on the server) to match.` : "";
1534
1565
  throw new Error(
1535
- `Failed to initialize MCP connection to ${this.config.baseUrl}${this.mcpPathLabel()} after ${maxRetries} attempts: ${lastError}.${served}`
1566
+ `Failed to initialize MCP connection to ${this.config.baseUrl}${this.mcpPathLabel()} after ${attemptsMade} attempt${attemptsMade === 1 ? "" : "s"}: ${lastError}.${served}`
1536
1567
  );
1537
1568
  }
1538
1569
  this._sessionId = this.transport.getSessionId();
@@ -1602,18 +1633,47 @@ var McpTestClient = class {
1602
1633
  return this._authState;
1603
1634
  }
1604
1635
  /**
1605
- * Authenticate with a token
1636
+ * Authenticate with a token.
1637
+ *
1638
+ * When connected, this opens a NEW session with the token (the previous session was created
1639
+ * for the previous identity) and rejects if the server refuses it — for example because the
1640
+ * token is expired or badly signed. On rejection the client keeps its previous identity and
1641
+ * session. The token is kept in the client config, so `reconnect()` keeps using it.
1606
1642
  */
1607
1643
  async authenticate(token) {
1644
+ if (typeof token !== "string" || token.trim() === "") {
1645
+ throw new Error("authenticate() requires a non-empty token");
1646
+ }
1647
+ const previousAuthState = this._authState;
1648
+ const previousConfigAuth = this.config.auth;
1649
+ this.config.auth = { ...previousConfigAuth, token };
1608
1650
  this._authState = {
1609
1651
  isAnonymous: false,
1610
1652
  token,
1611
1653
  scopes: this.parseScopesFromToken(token),
1612
1654
  user: this.parseUserFromToken(token)
1613
1655
  };
1614
- if (this.transport) {
1615
- this.transport.setAuthToken(token);
1656
+ const previousTransport = this.transport;
1657
+ if (!previousTransport) {
1658
+ this.log("debug", "Authentication updated (not connected yet)");
1659
+ return;
1660
+ }
1661
+ const previousInit = this.initResult;
1662
+ const previousSessionId = this._sessionId;
1663
+ const previousSessionInfo = this._sessionInfo;
1664
+ try {
1665
+ await this.connect();
1666
+ } catch (err) {
1667
+ await this.transport?.close().catch(() => void 0);
1668
+ this.transport = previousTransport;
1669
+ this.initResult = previousInit;
1670
+ this._sessionId = previousSessionId;
1671
+ this._sessionInfo = previousSessionInfo;
1672
+ this.config.auth = previousConfigAuth;
1673
+ this._authState = previousAuthState;
1674
+ throw err;
1616
1675
  }
1676
+ await previousTransport.close().catch(() => void 0);
1617
1677
  this.log("debug", "Authentication updated");
1618
1678
  }
1619
1679
  // ═══════════════════════════════════════════════════════════════════
@@ -2237,10 +2297,11 @@ var McpTestClient = class {
2237
2297
  requestId: id
2238
2298
  };
2239
2299
  } catch (err) {
2300
+ if (isInterceptedError(err)) throw err;
2240
2301
  const durationMs = Date.now() - start;
2241
2302
  const error = {
2242
2303
  code: -32603,
2243
- message: err instanceof Error ? err.message : "Unknown error"
2304
+ message: errorMessage(err)
2244
2305
  };
2245
2306
  return {
2246
2307
  success: false,
@@ -2388,12 +2449,12 @@ var McpTestClient = class {
2388
2449
  const payload = this.decodeJwtPayload(token);
2389
2450
  if (!payload) return [];
2390
2451
  const scope = payload["scope"];
2391
- const scopes = payload["scopes"];
2452
+ const scopes2 = payload["scopes"];
2392
2453
  if (typeof scope === "string") {
2393
2454
  return scope.split(" ");
2394
2455
  }
2395
- if (Array.isArray(scopes)) {
2396
- return scopes;
2456
+ if (Array.isArray(scopes2)) {
2457
+ return scopes2;
2397
2458
  }
2398
2459
  return [];
2399
2460
  } catch {
@@ -2468,7 +2529,7 @@ var ProgressCollector = class {
2468
2529
  };
2469
2530
 
2470
2531
  // libs/testing/src/server/test-server.ts
2471
- import { spawn } from "child_process";
2532
+ import { execFile, spawn } from "child_process";
2472
2533
  import { sha256Hex } from "@frontmcp/utils";
2473
2534
 
2474
2535
  // libs/testing/src/errors/index.ts
@@ -2539,6 +2600,8 @@ var E2E_PORT_RANGES = {
2539
2600
  "demo-e2e-distributed": { start: 50440, size: 20 },
2540
2601
  // Protocol revision E2E tests (50460-50479)
2541
2602
  "demo-e2e-protocol-20260728": { start: 50460, size: 20 },
2603
+ // Testing library E2E tests (50480-50499)
2604
+ "demo-e2e-testing": { start: 50480, size: 20 },
2542
2605
  // Mock servers and utilities (50900-50999)
2543
2606
  "mock-oauth": { start: 50900, size: 10 },
2544
2607
  "mock-api": { start: 50910, size: 10 },
@@ -2547,6 +2610,66 @@ var E2E_PORT_RANGES = {
2547
2610
  default: { start: 51e3, size: 100 }
2548
2611
  };
2549
2612
  var reservedPorts = /* @__PURE__ */ new Map();
2613
+ var lockedPorts = /* @__PURE__ */ new Set();
2614
+ var LOCK_MAX_AGE_MS = 10 * 60 * 1e3;
2615
+ function lockDir() {
2616
+ try {
2617
+ const os = __require("node:os");
2618
+ const path = __require("node:path");
2619
+ return path.join(os.tmpdir(), "frontmcp-testing-ports");
2620
+ } catch {
2621
+ return void 0;
2622
+ }
2623
+ }
2624
+ function pidIsAlive(pid) {
2625
+ try {
2626
+ process.kill(pid, 0);
2627
+ return true;
2628
+ } catch (error) {
2629
+ return error.code === "EPERM";
2630
+ }
2631
+ }
2632
+ function acquirePortLock(port) {
2633
+ if (lockedPorts.has(port)) return false;
2634
+ let fs;
2635
+ let file;
2636
+ try {
2637
+ fs = __require("node:fs");
2638
+ const dir = lockDir();
2639
+ if (!dir) return true;
2640
+ fs.mkdirSync(dir, { recursive: true });
2641
+ file = __require("node:path").join(dir, `${port}.lock`);
2642
+ } catch {
2643
+ return true;
2644
+ }
2645
+ const payload = JSON.stringify({ pid: process.pid, at: Date.now() });
2646
+ for (let attempt = 0; attempt < 2; attempt++) {
2647
+ try {
2648
+ fs.writeFileSync(file, payload, { flag: "wx" });
2649
+ lockedPorts.add(port);
2650
+ return true;
2651
+ } catch (error) {
2652
+ if (error.code !== "EEXIST") return true;
2653
+ try {
2654
+ const held = JSON.parse(fs.readFileSync(file, "utf8"));
2655
+ const fresh = typeof held.at === "number" && Date.now() - held.at < LOCK_MAX_AGE_MS;
2656
+ if (fresh && typeof held.pid === "number" && pidIsAlive(held.pid)) return false;
2657
+ fs.unlinkSync(file);
2658
+ } catch {
2659
+ }
2660
+ }
2661
+ }
2662
+ return false;
2663
+ }
2664
+ function releasePortLock(port) {
2665
+ if (!lockedPorts.delete(port)) return;
2666
+ try {
2667
+ const fs = __require("node:fs");
2668
+ const dir = lockDir();
2669
+ if (dir) fs.unlinkSync(__require("node:path").join(dir, `${port}.lock`));
2670
+ } catch {
2671
+ }
2672
+ }
2550
2673
  var projectPortIndex = /* @__PURE__ */ new Map();
2551
2674
  function getPortRange(project) {
2552
2675
  const key = project;
@@ -2557,15 +2680,22 @@ function getPortRange(project) {
2557
2680
  }
2558
2681
  async function reservePort(project, preferredPort) {
2559
2682
  const range = getPortRange(project);
2560
- if (preferredPort !== void 0) {
2683
+ const handle = (port) => ({
2684
+ port,
2685
+ // Frees the socket only, so the server about to be spawned can bind the port; the lock file stays
2686
+ releaseSocket: async () => {
2687
+ await releasePort(port);
2688
+ },
2689
+ release: async () => {
2690
+ await releasePort(port);
2691
+ releasePortLock(port);
2692
+ }
2693
+ });
2694
+ const wantsSpecificPort = preferredPort !== void 0 && Number.isInteger(preferredPort) && preferredPort > 0;
2695
+ if (wantsSpecificPort) {
2561
2696
  const reservation = await tryReservePort(preferredPort, project);
2562
2697
  if (reservation) {
2563
- return {
2564
- port: preferredPort,
2565
- release: async () => {
2566
- await releasePort(preferredPort);
2567
- }
2568
- };
2698
+ return handle(preferredPort);
2569
2699
  }
2570
2700
  console.warn(`[PortRegistry] Preferred port ${preferredPort} not available for ${project}, allocating from range`);
2571
2701
  }
@@ -2574,30 +2704,19 @@ async function reservePort(project, preferredPort) {
2574
2704
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
2575
2705
  const port = range.start + index % range.size;
2576
2706
  index = (index + 1) % range.size;
2577
- if (reservedPorts.has(port)) {
2707
+ if (reservedPorts.has(port) || lockedPorts.has(port)) {
2578
2708
  continue;
2579
2709
  }
2580
2710
  const reservation = await tryReservePort(port, project);
2581
2711
  if (reservation) {
2582
2712
  projectPortIndex.set(project, index);
2583
- return {
2584
- port,
2585
- release: async () => {
2586
- await releasePort(port);
2587
- }
2588
- };
2713
+ return handle(port);
2589
2714
  }
2590
2715
  }
2591
- const dynamicPort = await findAvailablePortInRange(51e3, 52e3);
2592
- if (dynamicPort) {
2593
- const reservation = await tryReservePort(dynamicPort, project);
2594
- if (reservation) {
2595
- return {
2596
- port: dynamicPort,
2597
- release: async () => {
2598
- await releasePort(dynamicPort);
2599
- }
2600
- };
2716
+ for (let port = 51e3; port < 52e3; port++) {
2717
+ if (reservedPorts.has(port) || lockedPorts.has(port)) continue;
2718
+ if (await tryReservePort(port, project)) {
2719
+ return handle(port);
2601
2720
  }
2602
2721
  }
2603
2722
  throw new Error(
@@ -2605,9 +2724,13 @@ async function reservePort(project, preferredPort) {
2605
2724
  );
2606
2725
  }
2607
2726
  async function tryReservePort(port, project) {
2727
+ if (!acquirePortLock(port)) {
2728
+ return false;
2729
+ }
2608
2730
  return new Promise((resolve) => {
2609
2731
  const server = createServer();
2610
2732
  server.once("error", () => {
2733
+ releasePortLock(port);
2611
2734
  resolve(false);
2612
2735
  });
2613
2736
  server.listen(port, () => {
@@ -2633,31 +2756,6 @@ async function releasePort(port) {
2633
2756
  });
2634
2757
  });
2635
2758
  }
2636
- async function findAvailablePortInRange(start, end) {
2637
- for (let port = start; port < end; port++) {
2638
- if (reservedPorts.has(port)) {
2639
- continue;
2640
- }
2641
- const available = await isPortAvailable(port);
2642
- if (available) {
2643
- return port;
2644
- }
2645
- }
2646
- return null;
2647
- }
2648
- async function isPortAvailable(port) {
2649
- return new Promise((resolve) => {
2650
- const server = createServer();
2651
- server.once("error", () => {
2652
- resolve(false);
2653
- });
2654
- server.listen(port, () => {
2655
- server.close(() => {
2656
- resolve(true);
2657
- });
2658
- });
2659
- });
2660
- }
2661
2759
 
2662
2760
  // libs/testing/src/server/test-server.ts
2663
2761
  var DEBUG_SERVER = process.env["DEBUG_SERVER"] === "1" || process.env["DEBUG"] === "1";
@@ -2666,8 +2764,11 @@ var TestServer = class _TestServer {
2666
2764
  options;
2667
2765
  _info;
2668
2766
  logs = [];
2767
+ /** Frees the port reservation (socket and cross-process lock); held until stop() */
2669
2768
  portRelease = null;
2670
- constructor(options, port, portRelease) {
2769
+ /** Frees only the reservation socket, just before the server is spawned */
2770
+ portReleaseSocket = null;
2771
+ constructor(options, port, portRelease, portReleaseSocket) {
2671
2772
  this.options = {
2672
2773
  port,
2673
2774
  project: options.project,
@@ -2679,6 +2780,7 @@ var TestServer = class _TestServer {
2679
2780
  debug: options.debug ?? DEBUG_SERVER
2680
2781
  };
2681
2782
  this.portRelease = portRelease ?? null;
2783
+ this.portReleaseSocket = portReleaseSocket ?? portRelease ?? null;
2682
2784
  this._info = {
2683
2785
  baseUrl: `http://localhost:${port}`,
2684
2786
  port
@@ -2691,8 +2793,8 @@ var TestServer = class _TestServer {
2691
2793
  const project = options.project ?? "default";
2692
2794
  const maxAttempts = 3;
2693
2795
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
2694
- const { port, release } = await reservePort(project, options.port);
2695
- const server = new _TestServer(options, port, release);
2796
+ const { port, release, releaseSocket } = await reservePort(project, options.port);
2797
+ const server = new _TestServer(options, port, release, releaseSocket);
2696
2798
  try {
2697
2799
  await server.startProcess();
2698
2800
  return server;
@@ -2732,7 +2834,7 @@ var TestServer = class _TestServer {
2732
2834
  }
2733
2835
  const maxAttempts = 3;
2734
2836
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
2735
- const { port, release } = await reservePort(project, options.port);
2837
+ const { port, release, releaseSocket } = await reservePort(project, options.port);
2736
2838
  const serverOptions = {
2737
2839
  ...options,
2738
2840
  port,
@@ -2740,7 +2842,7 @@ var TestServer = class _TestServer {
2740
2842
  command: `npx nx serve ${project} --port ${port}`,
2741
2843
  cwd: options.cwd ?? process.cwd()
2742
2844
  };
2743
- const server = new _TestServer(serverOptions, port, release);
2845
+ const server = new _TestServer(serverOptions, port, release, releaseSocket);
2744
2846
  try {
2745
2847
  await server.startProcess();
2746
2848
  return server;
@@ -2798,10 +2900,17 @@ var TestServer = class _TestServer {
2798
2900
  * Stop the test server
2799
2901
  */
2800
2902
  async stop() {
2801
- if (this.portRelease) {
2802
- await this.portRelease();
2803
- this.portRelease = null;
2903
+ try {
2904
+ await this.stopProcess();
2905
+ } finally {
2906
+ if (this.portRelease) {
2907
+ await this.portRelease();
2908
+ this.portRelease = null;
2909
+ }
2910
+ this.portReleaseSocket = null;
2804
2911
  }
2912
+ }
2913
+ async stopProcess() {
2805
2914
  if (this.process) {
2806
2915
  this.log("Stopping server...");
2807
2916
  if (this.process.exitCode !== null || this.process.signalCode !== null) {
@@ -2874,7 +2983,7 @@ var TestServer = class _TestServer {
2874
2983
  * Restart the server
2875
2984
  */
2876
2985
  async restart() {
2877
- await this.stop();
2986
+ await this.stopProcess();
2878
2987
  await this.startProcess();
2879
2988
  }
2880
2989
  /**
@@ -2904,9 +3013,9 @@ var TestServer = class _TestServer {
2904
3013
  PORT: String(this.options.port)
2905
3014
  };
2906
3015
  const runtimeEnv = withWorkspaceProtocolFallback(env, this.options.cwd);
2907
- if (this.portRelease) {
2908
- await this.portRelease();
2909
- this.portRelease = null;
3016
+ if (this.portReleaseSocket) {
3017
+ await this.portReleaseSocket();
3018
+ this.portReleaseSocket = null;
2910
3019
  await sleep2(300);
2911
3020
  }
2912
3021
  this.process = spawn(this.options.command, [], {
@@ -2916,9 +3025,8 @@ var TestServer = class _TestServer {
2916
3025
  detached: true,
2917
3026
  stdio: ["pipe", "pipe", "pipe"]
2918
3027
  });
2919
- if (this.process.pid !== void 0) {
2920
- this._info.pid = this.process.pid;
2921
- }
3028
+ this._info.pid = this.process.pid;
3029
+ this._info.shellPid = this.process.pid;
2922
3030
  let processExited = false;
2923
3031
  let exitCode = null;
2924
3032
  let exitError = null;
@@ -3021,6 +3129,8 @@ ${allLogs}`);
3021
3129
  });
3022
3130
  if (response.ok || response.status === 404) {
3023
3131
  this.log(`Server is ready after ${healthCheckAttempts} health check attempts`);
3132
+ const listeningPid = await findListeningPid(this.options.port);
3133
+ if (listeningPid !== void 0) this._info.pid = listeningPid;
3024
3134
  return;
3025
3135
  }
3026
3136
  lastHealthCheckError = `HTTP ${response.status}: ${response.statusText}`;
@@ -3065,6 +3175,19 @@ TIP: Set DEBUG_SERVER=1 or DEBUG=1 environment variable for verbose output`
3065
3175
  function sleep2(ms) {
3066
3176
  return new Promise((resolve) => setTimeout(resolve, ms));
3067
3177
  }
3178
+ function findListeningPid(port) {
3179
+ if (process.platform === "win32") return Promise.resolve(void 0);
3180
+ return new Promise((resolve) => {
3181
+ execFile("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], { timeout: 2e3 }, (error, stdout) => {
3182
+ if (error) {
3183
+ resolve(void 0);
3184
+ return;
3185
+ }
3186
+ const pid = parseInt(String(stdout).split("\n")[0] ?? "", 10);
3187
+ resolve(Number.isInteger(pid) && pid > 0 ? pid : void 0);
3188
+ });
3189
+ });
3190
+ }
3068
3191
  function withWorkspaceProtocolFallback(env, cwd) {
3069
3192
  if (findInstalledProtocolPackageDir(cwd)) {
3070
3193
  return env;
@@ -3181,81 +3304,152 @@ function gatewayTokenBinding(serverUrl, entryPath) {
3181
3304
  }
3182
3305
 
3183
3306
  // libs/testing/src/fixtures/test-fixture.ts
3184
- var describe = _describe;
3185
- var beforeAll = _beforeAll;
3186
- var beforeEach = _beforeEach;
3187
- var afterEach = _afterEach;
3188
- var afterAll = _afterAll;
3189
- var it = _it;
3190
- var currentConfig = {};
3191
- var serverInstance = null;
3192
- var tokenFactory = null;
3193
- var serverStartedByUs = false;
3194
- var skipScopes = [{ skipped: false }];
3195
- function currentSkipScope() {
3196
- return skipScopes[skipScopes.length - 1];
3307
+ var jestApi;
3308
+ function jestGlobals() {
3309
+ if (jestApi) return jestApi;
3310
+ try {
3311
+ const api = __require("@jest/globals");
3312
+ jestApi = {
3313
+ describe: api["describe"],
3314
+ beforeAll: api["beforeAll"],
3315
+ beforeEach: api["beforeEach"],
3316
+ afterEach: api["afterEach"],
3317
+ afterAll: api["afterAll"],
3318
+ it: api["it"]
3319
+ };
3320
+ return jestApi;
3321
+ } catch (error) {
3322
+ throw new Error(
3323
+ `@frontmcp/testing fixtures ("test", "test.use", ...) can only be used inside a Jest test file: ${error instanceof Error ? error.message : String(error)}`,
3324
+ { cause: error }
3325
+ );
3326
+ }
3197
3327
  }
3198
- async function initializeSharedResources() {
3199
- if (!serverInstance) {
3200
- if (currentConfig.baseUrl && !currentConfig.server) {
3201
- serverInstance = TestServer.connect(currentConfig.baseUrl);
3202
- serverStartedByUs = false;
3203
- } else if (currentConfig.server) {
3204
- const serverCommand = resolveServerCommand(currentConfig.server);
3205
- const isDebug = currentConfig.logLevel === "debug" || process.env["DEBUG"] === "1" || process.env["DEBUG_SERVER"] === "1";
3328
+ var scopes = [{ skipped: false, config: {}, configured: false, cleanupRegistered: false }];
3329
+ function currentScope() {
3330
+ return scopes[scopes.length - 1];
3331
+ }
3332
+ var serverEntries = /* @__PURE__ */ new Map();
3333
+ function resolveConfig(chain) {
3334
+ const merged = {};
3335
+ for (const scope of chain) {
3336
+ const { env, ...rest } = scope.config;
3337
+ Object.assign(merged, rest);
3338
+ if (env) merged.env = { ...merged.env, ...env };
3339
+ }
3340
+ return merged;
3341
+ }
3342
+ function ownerScopeOf(chain) {
3343
+ for (let i = chain.length - 1; i > 0; i--) {
3344
+ if (chain[i].configured) return chain[i];
3345
+ }
3346
+ return chain[0];
3347
+ }
3348
+ function serverKey(config) {
3349
+ const env = Object.fromEntries(Object.entries(config.env ?? {}).sort(([a], [b]) => a.localeCompare(b)));
3350
+ return JSON.stringify({
3351
+ server: config.server,
3352
+ baseUrl: config.baseUrl,
3353
+ project: config.project,
3354
+ port: config.port,
3355
+ entryPath: config.entryPath,
3356
+ startupTimeout: config.startupTimeout,
3357
+ logLevel: config.logLevel,
3358
+ auth: config.auth,
3359
+ env
3360
+ });
3361
+ }
3362
+ function serverEnv(config) {
3363
+ const extra = {};
3364
+ if (config.auth?.mode) extra["FRONTMCP_TEST_AUTH_MODE"] = config.auth.mode;
3365
+ if (config.auth?.type) extra["FRONTMCP_TEST_AUTH_TYPE"] = config.auth.type;
3366
+ if (Object.keys(extra).length === 0 && !config.env) return void 0;
3367
+ return { ...extra, ...config.env };
3368
+ }
3369
+ async function startServerEntry(config, owner) {
3370
+ let server;
3371
+ let startedByUs = false;
3372
+ if (config.baseUrl && !config.server) {
3373
+ server = TestServer.connect(config.baseUrl);
3374
+ } else if (config.server) {
3375
+ const serverCommand = resolveServerCommand(config.server);
3376
+ const isDebug = config.logLevel === "debug" || process.env["DEBUG"] === "1" || process.env["DEBUG_SERVER"] === "1";
3377
+ if (isDebug) {
3378
+ console.log(`[TestFixture] Starting server: ${serverCommand}`);
3379
+ }
3380
+ try {
3381
+ server = await TestServer.start({
3382
+ project: config.project,
3383
+ port: config.port,
3384
+ command: serverCommand,
3385
+ env: serverEnv(config),
3386
+ startupTimeout: config.startupTimeout ?? 3e4,
3387
+ debug: isDebug
3388
+ });
3389
+ startedByUs = true;
3206
3390
  if (isDebug) {
3207
- console.log(`[TestFixture] Starting server: ${serverCommand}`);
3391
+ console.log(`[TestFixture] Server started at ${server.info.baseUrl}`);
3208
3392
  }
3209
- try {
3210
- serverInstance = await TestServer.start({
3211
- project: currentConfig.project,
3212
- port: currentConfig.port,
3213
- command: serverCommand,
3214
- env: currentConfig.env,
3215
- startupTimeout: currentConfig.startupTimeout ?? 3e4,
3216
- debug: isDebug
3217
- });
3218
- serverStartedByUs = true;
3219
- if (isDebug) {
3220
- console.log(`[TestFixture] Server started at ${serverInstance.info.baseUrl}`);
3221
- }
3222
- } catch (error) {
3223
- const errMsg = error instanceof Error ? error.message : String(error);
3224
- throw new Error(
3225
- `Failed to start test server.
3393
+ } catch (error) {
3394
+ const errMsg = error instanceof Error ? error.message : String(error);
3395
+ throw new Error(
3396
+ `Failed to start test server.
3226
3397
 
3227
- Server entry: ${currentConfig.server}
3228
- Project: ${currentConfig.project ?? "default"}
3398
+ Server entry: ${config.server}
3399
+ Project: ${config.project ?? "default"}
3229
3400
  Command: ${serverCommand}
3230
3401
 
3231
- Error: ${errMsg}`
3232
- );
3233
- }
3234
- } else {
3235
- throw new Error(
3236
- 'test.use() requires either "server" (entry file path) or "baseUrl" (for external server) option'
3402
+ Error: ${errMsg}`,
3403
+ { cause: error }
3237
3404
  );
3238
3405
  }
3406
+ } else {
3407
+ throw new Error('test.use() requires either "server" (entry file path) or "baseUrl" (for external server) option');
3239
3408
  }
3240
- if (!tokenFactory) {
3241
- const sharedSecret = currentConfig.env?.["JWT_SECRET"];
3242
- const binding = serverInstance ? gatewayTokenBinding(resolveClientBaseUrl(serverInstance), currentConfig.entryPath) : void 0;
3243
- tokenFactory = new TestTokenFactory(sharedSecret ? { hmacSecret: sharedSecret, ...binding } : {});
3244
- }
3409
+ const sharedSecret = config.env?.["JWT_SECRET"];
3410
+ const binding = sharedSecret ? gatewayTokenBinding(config.baseUrl ?? server.info.baseUrl, config.entryPath) : void 0;
3411
+ const tokenFactory = new TestTokenFactory(sharedSecret ? { hmacSecret: sharedSecret, ...binding } : {});
3412
+ return { server, startedByUs, tokenFactory, owner };
3245
3413
  }
3246
- async function createTestFixtures() {
3247
- await initializeSharedResources();
3248
- if (!serverInstance) {
3249
- throw new Error("Server instance not initialized");
3250
- }
3251
- if (!tokenFactory) {
3252
- throw new Error("Token factory not initialized");
3414
+ function acquireServer(chain) {
3415
+ const config = resolveConfig(chain);
3416
+ const key = serverKey(config);
3417
+ let entry = serverEntries.get(key);
3418
+ if (!entry) {
3419
+ entry = startServerEntry(config, ownerScopeOf(chain));
3420
+ serverEntries.set(key, entry);
3421
+ entry.catch(() => serverEntries.delete(key));
3422
+ }
3423
+ return entry;
3424
+ }
3425
+ async function stopServers(owner) {
3426
+ for (const [key, pending] of Array.from(serverEntries)) {
3427
+ let entry;
3428
+ try {
3429
+ entry = await pending;
3430
+ } catch {
3431
+ serverEntries.delete(key);
3432
+ continue;
3433
+ }
3434
+ if (owner && entry.owner !== owner) continue;
3435
+ serverEntries.delete(key);
3436
+ if (entry.startedByUs) {
3437
+ await entry.server.stop();
3438
+ }
3253
3439
  }
3440
+ }
3441
+ var fixtureContexts = /* @__PURE__ */ new WeakMap();
3442
+ async function buildFixtures(chain) {
3443
+ const config = resolveConfig(chain);
3444
+ const entry = await acquireServer(chain);
3445
+ const { server, tokenFactory } = entry;
3446
+ const publicMode = config.publicMode ?? config.auth?.mode === "public";
3447
+ const clientConfig = { ...config, publicMode };
3254
3448
  const clientInstance = McpTestClient.create({
3255
- baseUrl: resolveClientBaseUrl(serverInstance),
3256
- entryPath: currentConfig.entryPath,
3257
- transport: currentConfig.transport ?? "streamable-http",
3258
- publicMode: currentConfig.publicMode
3449
+ baseUrl: resolveClientBaseUrl(server, clientConfig),
3450
+ entryPath: clientConfig.entryPath,
3451
+ transport: clientConfig.transport ?? "streamable-http",
3452
+ publicMode
3259
3453
  }).build();
3260
3454
  try {
3261
3455
  await clientInstance.connect();
@@ -3269,17 +3463,24 @@ async function createTestFixtures() {
3269
3463
  throw err;
3270
3464
  }
3271
3465
  }
3272
- const auth = createAuthFixture(tokenFactory);
3273
- const server = createServerFixture(serverInstance);
3274
- return {
3466
+ const context = { extraClients: [] };
3467
+ const fixtures = {
3275
3468
  mcp: clientInstance,
3276
- auth,
3277
- server
3469
+ auth: createAuthFixture(tokenFactory),
3470
+ server: createServerFixture(server, clientConfig, clientInstance, context)
3278
3471
  };
3472
+ fixtureContexts.set(fixtures, context);
3473
+ return fixtures;
3474
+ }
3475
+ async function createTestFixtures() {
3476
+ return buildFixtures(scopes.slice());
3477
+ }
3478
+ async function initializeSharedResources() {
3479
+ await acquireServer(scopes.slice());
3279
3480
  }
3280
3481
  async function cleanupTestFixtures(fixtures, testFailed = false) {
3281
- if (testFailed && serverInstance) {
3282
- const logs = serverInstance.getLogs();
3482
+ if (testFailed) {
3483
+ const logs = fixtures.server.getLogs();
3283
3484
  if (logs.length > 0) {
3284
3485
  console.error("\n[TestFixture] === Server Logs (test failed) ===");
3285
3486
  const recentLogs = logs.slice(-50);
@@ -3290,20 +3491,21 @@ async function cleanupTestFixtures(fixtures, testFailed = false) {
3290
3491
  console.error("[TestFixture] === End Server Logs ===\n");
3291
3492
  }
3292
3493
  }
3494
+ const extras = fixtureContexts.get(fixtures)?.extraClients ?? [];
3495
+ for (const client of extras) {
3496
+ if (client.isConnected()) {
3497
+ await client.disconnect().catch(() => void 0);
3498
+ }
3499
+ }
3293
3500
  if (fixtures.mcp.isConnected()) {
3294
3501
  await fixtures.mcp.disconnect();
3295
3502
  }
3296
3503
  }
3297
3504
  async function cleanupSharedResources() {
3298
- if (serverInstance && serverStartedByUs) {
3299
- await serverInstance.stop();
3300
- }
3301
- serverInstance = null;
3302
- tokenFactory = null;
3303
- serverStartedByUs = false;
3505
+ await stopServers();
3304
3506
  }
3305
- function resolveClientBaseUrl(server) {
3306
- return currentConfig.baseUrl ?? server.info.baseUrl;
3507
+ function resolveClientBaseUrl(server, config) {
3508
+ return config.baseUrl ?? server.info.baseUrl;
3307
3509
  }
3308
3510
  function createAuthFixture(factory) {
3309
3511
  const users = {
@@ -3349,28 +3551,39 @@ function createAuthFixture(factory) {
3349
3551
  getAudience: () => factory.getAudience()
3350
3552
  };
3351
3553
  }
3352
- function createServerFixture(server) {
3554
+ function createServerFixture(server, config, mcp, context) {
3353
3555
  return {
3354
- info: server.info,
3556
+ // Live view: `pid` is filled in once the server is listening and follows restarts
3557
+ get info() {
3558
+ return server.info;
3559
+ },
3355
3560
  createClient: async (opts) => {
3356
- return McpTestClient.create({
3357
- baseUrl: resolveClientBaseUrl(server),
3358
- entryPath: opts?.entryPath ?? currentConfig.entryPath,
3561
+ const client = await McpTestClient.create({
3562
+ baseUrl: resolveClientBaseUrl(server, config),
3563
+ entryPath: opts?.entryPath ?? config.entryPath,
3359
3564
  transport: opts?.transport ?? "streamable-http",
3360
3565
  auth: opts?.token ? { token: opts.token } : void 0,
3361
3566
  clientInfo: opts?.clientInfo,
3362
- publicMode: currentConfig.publicMode
3567
+ publicMode: config.publicMode
3363
3568
  }).buildAndConnect();
3569
+ context.extraClients.push(client);
3570
+ return client;
3364
3571
  },
3365
3572
  createClientBuilder: () => {
3366
3573
  const builder = new McpTestClientBuilder({
3367
- baseUrl: resolveClientBaseUrl(server),
3368
- entryPath: currentConfig.entryPath,
3369
- publicMode: currentConfig.publicMode
3574
+ baseUrl: resolveClientBaseUrl(server, config),
3575
+ entryPath: config.entryPath,
3576
+ publicMode: config.publicMode
3370
3577
  });
3371
3578
  return builder;
3372
3579
  },
3373
- restart: () => server.restart(),
3580
+ restart: async () => {
3581
+ const reconnect = [mcp, ...context.extraClients].filter((client) => client.isConnected());
3582
+ await server.restart();
3583
+ for (const client of reconnect) {
3584
+ await client.reconnect();
3585
+ }
3586
+ },
3374
3587
  getLogs: () => server.getLogs(),
3375
3588
  clearLogs: () => server.clearLogs()
3376
3589
  };
@@ -3381,12 +3594,12 @@ function resolveServerCommand(server) {
3381
3594
  }
3382
3595
  return `npx tsx ${server}`;
3383
3596
  }
3384
- function runWithFixtures(fn) {
3385
- return async () => {
3386
- const fixtures = await createTestFixtures();
3597
+ function runWithFixtures(fn, chain) {
3598
+ return async (...extra) => {
3599
+ const fixtures = await buildFixtures(chain);
3387
3600
  let testFailed = false;
3388
3601
  try {
3389
- await fn(fixtures);
3602
+ await fn(fixtures, ...extra);
3390
3603
  } catch (error) {
3391
3604
  testFailed = true;
3392
3605
  throw error;
@@ -3395,24 +3608,37 @@ function runWithFixtures(fn) {
3395
3608
  }
3396
3609
  };
3397
3610
  }
3611
+ function skipTitle(name, scope) {
3612
+ return scope.reason ? `${name} (skipped: ${scope.reason})` : name;
3613
+ }
3398
3614
  function testWithFixtures(name, fn) {
3399
- const scope = currentSkipScope();
3615
+ const chain = scopes.slice();
3616
+ const scope = currentScope();
3400
3617
  if (scope.skipped) {
3401
- it.skip(scope.reason ? `${name} (skipped: ${scope.reason})` : name, runWithFixtures(fn));
3618
+ jestGlobals().it.skip(skipTitle(name, scope), runWithFixtures(fn, chain));
3402
3619
  return;
3403
3620
  }
3404
- it(name, runWithFixtures(fn));
3621
+ jestGlobals().it(name, runWithFixtures(fn, chain));
3405
3622
  }
3406
3623
  function use(config) {
3407
- currentConfig = { ...currentConfig, ...config };
3408
- afterAll(async () => {
3409
- await cleanupSharedResources();
3410
- });
3624
+ const scope = currentScope();
3625
+ scope.config = {
3626
+ ...scope.config,
3627
+ ...config,
3628
+ env: config.env ? { ...scope.config.env, ...config.env } : scope.config.env
3629
+ };
3630
+ scope.configured = true;
3631
+ if (!scope.cleanupRegistered) {
3632
+ scope.cleanupRegistered = true;
3633
+ jestGlobals().afterAll(async () => {
3634
+ await stopServers(scope === scopes[0] ? void 0 : scope);
3635
+ });
3636
+ }
3411
3637
  }
3412
3638
  function skip(nameOrCondition, fnOrReason) {
3413
3639
  if (typeof nameOrCondition === "boolean") {
3414
3640
  if (nameOrCondition) {
3415
- const scope = currentSkipScope();
3641
+ const scope = currentScope();
3416
3642
  scope.skipped = true;
3417
3643
  scope.reason = typeof fnOrReason === "string" ? fnOrReason : void 0;
3418
3644
  }
@@ -3423,53 +3649,90 @@ function skip(nameOrCondition, fnOrReason) {
3423
3649
  `test.skip expects either (name: string, fn) or (condition: boolean, reason?: string); received (${typeof nameOrCondition}, ${typeof fnOrReason}).`
3424
3650
  );
3425
3651
  }
3426
- it.skip(nameOrCondition, runWithFixtures(fnOrReason));
3652
+ jestGlobals().it.skip(nameOrCondition, runWithFixtures(fnOrReason, scopes.slice()));
3427
3653
  }
3428
3654
  function only(name, fn) {
3429
- const scope = currentSkipScope();
3655
+ const scope = currentScope();
3430
3656
  if (scope.skipped) {
3431
- it.skip(scope.reason ? `${name} (skipped: ${scope.reason})` : name, runWithFixtures(fn));
3657
+ jestGlobals().it.skip(skipTitle(name, scope), runWithFixtures(fn, scopes.slice()));
3432
3658
  return;
3433
3659
  }
3434
- it.only(name, runWithFixtures(fn));
3660
+ jestGlobals().it.only(name, runWithFixtures(fn, scopes.slice()));
3435
3661
  }
3436
3662
  function todo(name) {
3437
- it.todo(name);
3663
+ jestGlobals().it.todo(name);
3438
3664
  }
3439
- function withSkipScope(register) {
3665
+ function each(table, ...tagged) {
3666
+ const chain = scopes.slice();
3667
+ const register = jestGlobals().it.each(table, ...tagged);
3440
3668
  return (name, fn) => {
3441
- register(name, () => {
3442
- const parent = currentSkipScope();
3443
- skipScopes.push({ skipped: parent.skipped, reason: parent.reason });
3444
- try {
3445
- fn();
3446
- } finally {
3447
- skipScopes.pop();
3448
- }
3669
+ const scope = currentScope();
3670
+ if (scope.skipped) {
3671
+ const skipRegister = jestGlobals().it.skip.each(table, ...tagged);
3672
+ skipRegister(skipTitle(name, scope), runWithFixtures(fn, chain));
3673
+ return;
3674
+ }
3675
+ register(name, runWithFixtures(fn, chain));
3676
+ };
3677
+ }
3678
+ function withScope(fn) {
3679
+ return (...args) => {
3680
+ const parent = currentScope();
3681
+ scopes.push({
3682
+ skipped: parent.skipped,
3683
+ reason: parent.reason,
3684
+ config: {},
3685
+ configured: false,
3686
+ cleanupRegistered: false
3449
3687
  });
3688
+ try {
3689
+ fn(...args);
3690
+ } finally {
3691
+ scopes.pop();
3692
+ }
3450
3693
  };
3451
3694
  }
3452
- function eachWithSkipScope(each) {
3453
- return ((...eachArgs) => {
3454
- const register = each(...eachArgs);
3455
- return withSkipScope(register);
3456
- });
3695
+ function scopedRegister(pick) {
3696
+ return (name, fn, timeout) => {
3697
+ pick()(name, withScope(fn), timeout);
3698
+ };
3457
3699
  }
3458
- var describeWithSkipScope = Object.assign(withSkipScope(describe), describe, {
3459
- only: Object.assign(withSkipScope(describe.only), { each: eachWithSkipScope(describe.only.each) }),
3460
- skip: Object.assign(withSkipScope(describe.skip), { each: eachWithSkipScope(describe.skip.each) }),
3461
- each: eachWithSkipScope(describe.each)
3462
- });
3700
+ function scopedEach(pick) {
3701
+ return (...eachArgs) => (name, fn, timeout) => {
3702
+ const register = pick()(...eachArgs);
3703
+ register(name, withScope(fn), timeout);
3704
+ };
3705
+ }
3706
+ var describeWithScope = Object.assign(
3707
+ scopedRegister(() => jestGlobals().describe),
3708
+ {
3709
+ // `describe.skip` still evaluates its callback during collection, so it needs a scope too
3710
+ only: Object.assign(
3711
+ scopedRegister(() => jestGlobals().describe.only),
3712
+ {
3713
+ each: scopedEach(() => jestGlobals().describe.only.each)
3714
+ }
3715
+ ),
3716
+ skip: Object.assign(
3717
+ scopedRegister(() => jestGlobals().describe.skip),
3718
+ {
3719
+ each: scopedEach(() => jestGlobals().describe.skip.each)
3720
+ }
3721
+ ),
3722
+ each: scopedEach(() => jestGlobals().describe.each)
3723
+ }
3724
+ );
3463
3725
  var test = testWithFixtures;
3464
3726
  test.use = use;
3465
- test.describe = describeWithSkipScope;
3466
- test.beforeAll = beforeAll;
3467
- test.beforeEach = beforeEach;
3468
- test.afterEach = afterEach;
3469
- test.afterAll = afterAll;
3727
+ test.describe = describeWithScope;
3728
+ test.beforeAll = ((...args) => jestGlobals().beforeAll(...args));
3729
+ test.beforeEach = ((...args) => jestGlobals().beforeEach(...args));
3730
+ test.afterEach = ((...args) => jestGlobals().afterEach(...args));
3731
+ test.afterAll = ((...args) => jestGlobals().afterAll(...args));
3470
3732
  test.skip = skip;
3471
3733
  test.only = only;
3472
3734
  test.todo = todo;
3735
+ test.each = each;
3473
3736
  export {
3474
3737
  cleanupSharedResources,
3475
3738
  cleanupTestFixtures,