@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
package/fixtures/index.js CHANGED
@@ -28,9 +28,6 @@ __export(fixtures_exports, {
28
28
  });
29
29
  module.exports = __toCommonJS(fixtures_exports);
30
30
 
31
- // libs/testing/src/fixtures/test-fixture.ts
32
- var import_globals = require("@jest/globals");
33
-
34
31
  // libs/testing/src/auth/token-factory.ts
35
32
  var import_jose = require("jose");
36
33
  var TestTokenFactory = class {
@@ -89,14 +86,15 @@ var TestTokenFactory = class {
89
86
  */
90
87
  async createTestToken(options) {
91
88
  await this.ensureKeys();
92
- const now = Math.floor(Date.now() / 1e3);
89
+ const nowMs = Date.now();
90
+ const iat = Math.floor(nowMs / 1e3);
93
91
  const exp = options.exp ?? 3600;
94
92
  const payload = {
95
93
  iss: options.iss ?? this.issuer,
96
94
  sub: options.sub,
97
95
  aud: options.aud ?? this.audience,
98
- iat: now,
99
- exp: now + exp,
96
+ iat,
97
+ exp: Math.ceil(nowMs / 1e3) + exp,
100
98
  scope: options.scopes?.join(" "),
101
99
  ...options.claims
102
100
  };
@@ -121,10 +119,10 @@ var TestTokenFactory = class {
121
119
  /**
122
120
  * Create a regular user token
123
121
  */
124
- async createUserToken(sub = "user-001", scopes = ["read", "write"]) {
122
+ async createUserToken(sub = "user-001", scopes2 = ["read", "write"]) {
125
123
  return this.createTestToken({
126
124
  sub,
127
- scopes,
125
+ scopes: scopes2,
128
126
  claims: {
129
127
  email: "user@test.local",
130
128
  name: "Test User",
@@ -135,10 +133,11 @@ var TestTokenFactory = class {
135
133
  /**
136
134
  * Create an anonymous user token
137
135
  */
138
- async createAnonymousToken() {
136
+ async createAnonymousToken(expiresIn) {
139
137
  return this.createTestToken({
140
138
  sub: `anon:${Date.now()}`,
141
139
  scopes: ["anonymous"],
140
+ exp: expiresIn,
142
141
  claims: {
143
142
  name: "Anonymous",
144
143
  role: "anonymous"
@@ -487,6 +486,32 @@ function sleep(ms) {
487
486
  return new Promise((resolve) => setTimeout(resolve, ms));
488
487
  }
489
488
 
489
+ // libs/testing/src/transport/error-utils.ts
490
+ function errorName(err) {
491
+ if (typeof err !== "object" || err === null) return void 0;
492
+ const name = err.name;
493
+ return typeof name === "string" ? name : void 0;
494
+ }
495
+ function isAbortError(err) {
496
+ return errorName(err) === "AbortError";
497
+ }
498
+ function errorMessage(err, fallback = "Unknown error") {
499
+ if (typeof err === "string" && err) return err;
500
+ if (typeof err === "object" && err !== null) {
501
+ const message = err.message;
502
+ if (typeof message === "string" && message) return message;
503
+ }
504
+ return fallback;
505
+ }
506
+ var interceptedErrors = /* @__PURE__ */ new WeakSet();
507
+ function markInterceptedError(err) {
508
+ interceptedErrors.add(err);
509
+ return err;
510
+ }
511
+ function isInterceptedError(err) {
512
+ return typeof err === "object" && err !== null && interceptedErrors.has(err);
513
+ }
514
+
490
515
  // libs/testing/src/transport/streamable-http.transport.ts
491
516
  var DEFAULT_TIMEOUT = 3e4;
492
517
  var NOTIFICATION_STREAM_REOPEN_DELAY_MS = 250;
@@ -595,7 +620,7 @@ var StreamableHttpTransport = class {
595
620
  return mockResponse2;
596
621
  }
597
622
  case "error":
598
- throw interceptResult.error;
623
+ throw markInterceptedError(interceptResult.error);
599
624
  case "continue":
600
625
  message = interceptResult.request;
601
626
  break;
@@ -656,7 +681,7 @@ var StreamableHttpTransport = class {
656
681
  return jsonResponse;
657
682
  } catch (error) {
658
683
  clearTimeout(timeoutId);
659
- if (error instanceof Error && error.name === "AbortError") {
684
+ if (isAbortError(error)) {
660
685
  return {
661
686
  jsonrpc: "2.0",
662
687
  id: message.id ?? null,
@@ -695,7 +720,7 @@ var StreamableHttpTransport = class {
695
720
  }
696
721
  } catch (error) {
697
722
  clearTimeout(timeoutId);
698
- if (error instanceof Error && error.name !== "AbortError") {
723
+ if (!isAbortError(error)) {
699
724
  throw error;
700
725
  }
701
726
  }
@@ -736,7 +761,7 @@ var StreamableHttpTransport = class {
736
761
  error: {
737
762
  code: -32700,
738
763
  message: "Parse error",
739
- data: error instanceof Error ? error.message : "Unknown error"
764
+ data: errorMessage(error)
740
765
  }
741
766
  };
742
767
  }
@@ -1106,7 +1131,7 @@ var StreamableHttpTransport = class {
1106
1131
  if (this.config.clientInfo) {
1107
1132
  headers["User-Agent"] = `${this.config.clientInfo.name}/${this.config.clientInfo.version}`;
1108
1133
  }
1109
- if (this.authToken && !this.publicMode) {
1134
+ if (this.authToken) {
1110
1135
  headers["Authorization"] = `Bearer ${this.authToken}`;
1111
1136
  }
1112
1137
  if (this.sessionId) {
@@ -1430,6 +1455,15 @@ var DEFAULT_CLIENT_INFO = {
1430
1455
  version: "0.4.0"
1431
1456
  };
1432
1457
  var MAX_LIST_PAGES = 1e3;
1458
+ function isAuthFailure(error) {
1459
+ return typeof error?.message === "string" && /^HTTP (401|403)\b/.test(error.message);
1460
+ }
1461
+ function describeInitializeError(error) {
1462
+ const message = error?.message ?? "Unknown error";
1463
+ const data = error?.data;
1464
+ const detail = typeof data === "string" ? data.trim() : "";
1465
+ return detail ? `${message} - ${detail.slice(0, 500)}` : message;
1466
+ }
1433
1467
  function extractEntryPaths(data) {
1434
1468
  let payload = data;
1435
1469
  if (typeof payload === "string") {
@@ -1516,19 +1550,22 @@ var McpTestClient = class {
1516
1550
  async connect() {
1517
1551
  this.log("debug", `Connecting to ${this.config.baseUrl}...`);
1518
1552
  this.triedDiscoveredEntryPath = false;
1553
+ this.initResult = null;
1519
1554
  this.transport = this.createTransport();
1520
1555
  await this.transport.connect();
1521
1556
  const maxRetries = 3;
1522
1557
  const retryDelayMs = 500;
1523
1558
  let lastError;
1524
1559
  let reportedEntryPaths;
1560
+ let attemptsMade = 0;
1525
1561
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
1562
+ attemptsMade = attempt;
1526
1563
  const initResponse = await this.initialize();
1527
1564
  if (initResponse.success && initResponse.data) {
1528
1565
  this.initResult = initResponse.data;
1529
1566
  break;
1530
1567
  }
1531
- lastError = initResponse.error?.message ?? "Unknown error";
1568
+ lastError = describeInitializeError(initResponse.error);
1532
1569
  reportedEntryPaths = extractEntryPaths(initResponse.error?.data) ?? reportedEntryPaths;
1533
1570
  if (reportedEntryPaths?.length === 1 && !this.triedDiscoveredEntryPath) {
1534
1571
  this.triedDiscoveredEntryPath = true;
@@ -1540,6 +1577,7 @@ var McpTestClient = class {
1540
1577
  await this.transport.connect();
1541
1578
  continue;
1542
1579
  }
1580
+ if (isAuthFailure(initResponse.error)) break;
1543
1581
  if (attempt < maxRetries) {
1544
1582
  this.log("debug", `MCP init attempt ${attempt} failed (${lastError}), retrying in ${retryDelayMs}ms...`);
1545
1583
  await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
@@ -1548,7 +1586,7 @@ var McpTestClient = class {
1548
1586
  if (!this.initResult) {
1549
1587
  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.` : "";
1550
1588
  throw new Error(
1551
- `Failed to initialize MCP connection to ${this.config.baseUrl}${this.mcpPathLabel()} after ${maxRetries} attempts: ${lastError}.${served}`
1589
+ `Failed to initialize MCP connection to ${this.config.baseUrl}${this.mcpPathLabel()} after ${attemptsMade} attempt${attemptsMade === 1 ? "" : "s"}: ${lastError}.${served}`
1552
1590
  );
1553
1591
  }
1554
1592
  this._sessionId = this.transport.getSessionId();
@@ -1618,18 +1656,47 @@ var McpTestClient = class {
1618
1656
  return this._authState;
1619
1657
  }
1620
1658
  /**
1621
- * Authenticate with a token
1659
+ * Authenticate with a token.
1660
+ *
1661
+ * When connected, this opens a NEW session with the token (the previous session was created
1662
+ * for the previous identity) and rejects if the server refuses it — for example because the
1663
+ * token is expired or badly signed. On rejection the client keeps its previous identity and
1664
+ * session. The token is kept in the client config, so `reconnect()` keeps using it.
1622
1665
  */
1623
1666
  async authenticate(token) {
1667
+ if (typeof token !== "string" || token.trim() === "") {
1668
+ throw new Error("authenticate() requires a non-empty token");
1669
+ }
1670
+ const previousAuthState = this._authState;
1671
+ const previousConfigAuth = this.config.auth;
1672
+ this.config.auth = { ...previousConfigAuth, token };
1624
1673
  this._authState = {
1625
1674
  isAnonymous: false,
1626
1675
  token,
1627
1676
  scopes: this.parseScopesFromToken(token),
1628
1677
  user: this.parseUserFromToken(token)
1629
1678
  };
1630
- if (this.transport) {
1631
- this.transport.setAuthToken(token);
1679
+ const previousTransport = this.transport;
1680
+ if (!previousTransport) {
1681
+ this.log("debug", "Authentication updated (not connected yet)");
1682
+ return;
1683
+ }
1684
+ const previousInit = this.initResult;
1685
+ const previousSessionId = this._sessionId;
1686
+ const previousSessionInfo = this._sessionInfo;
1687
+ try {
1688
+ await this.connect();
1689
+ } catch (err) {
1690
+ await this.transport?.close().catch(() => void 0);
1691
+ this.transport = previousTransport;
1692
+ this.initResult = previousInit;
1693
+ this._sessionId = previousSessionId;
1694
+ this._sessionInfo = previousSessionInfo;
1695
+ this.config.auth = previousConfigAuth;
1696
+ this._authState = previousAuthState;
1697
+ throw err;
1632
1698
  }
1699
+ await previousTransport.close().catch(() => void 0);
1633
1700
  this.log("debug", "Authentication updated");
1634
1701
  }
1635
1702
  // ═══════════════════════════════════════════════════════════════════
@@ -2253,10 +2320,11 @@ var McpTestClient = class {
2253
2320
  requestId: id
2254
2321
  };
2255
2322
  } catch (err) {
2323
+ if (isInterceptedError(err)) throw err;
2256
2324
  const durationMs = Date.now() - start;
2257
2325
  const error = {
2258
2326
  code: -32603,
2259
- message: err instanceof Error ? err.message : "Unknown error"
2327
+ message: errorMessage(err)
2260
2328
  };
2261
2329
  return {
2262
2330
  success: false,
@@ -2404,12 +2472,12 @@ var McpTestClient = class {
2404
2472
  const payload = this.decodeJwtPayload(token);
2405
2473
  if (!payload) return [];
2406
2474
  const scope = payload["scope"];
2407
- const scopes = payload["scopes"];
2475
+ const scopes2 = payload["scopes"];
2408
2476
  if (typeof scope === "string") {
2409
2477
  return scope.split(" ");
2410
2478
  }
2411
- if (Array.isArray(scopes)) {
2412
- return scopes;
2479
+ if (Array.isArray(scopes2)) {
2480
+ return scopes2;
2413
2481
  }
2414
2482
  return [];
2415
2483
  } catch {
@@ -2555,6 +2623,8 @@ var E2E_PORT_RANGES = {
2555
2623
  "demo-e2e-distributed": { start: 50440, size: 20 },
2556
2624
  // Protocol revision E2E tests (50460-50479)
2557
2625
  "demo-e2e-protocol-20260728": { start: 50460, size: 20 },
2626
+ // Testing library E2E tests (50480-50499)
2627
+ "demo-e2e-testing": { start: 50480, size: 20 },
2558
2628
  // Mock servers and utilities (50900-50999)
2559
2629
  "mock-oauth": { start: 50900, size: 10 },
2560
2630
  "mock-api": { start: 50910, size: 10 },
@@ -2563,6 +2633,66 @@ var E2E_PORT_RANGES = {
2563
2633
  default: { start: 51e3, size: 100 }
2564
2634
  };
2565
2635
  var reservedPorts = /* @__PURE__ */ new Map();
2636
+ var lockedPorts = /* @__PURE__ */ new Set();
2637
+ var LOCK_MAX_AGE_MS = 10 * 60 * 1e3;
2638
+ function lockDir() {
2639
+ try {
2640
+ const os = require("node:os");
2641
+ const path = require("node:path");
2642
+ return path.join(os.tmpdir(), "frontmcp-testing-ports");
2643
+ } catch {
2644
+ return void 0;
2645
+ }
2646
+ }
2647
+ function pidIsAlive(pid) {
2648
+ try {
2649
+ process.kill(pid, 0);
2650
+ return true;
2651
+ } catch (error) {
2652
+ return error.code === "EPERM";
2653
+ }
2654
+ }
2655
+ function acquirePortLock(port) {
2656
+ if (lockedPorts.has(port)) return false;
2657
+ let fs;
2658
+ let file;
2659
+ try {
2660
+ fs = require("node:fs");
2661
+ const dir = lockDir();
2662
+ if (!dir) return true;
2663
+ fs.mkdirSync(dir, { recursive: true });
2664
+ file = require("node:path").join(dir, `${port}.lock`);
2665
+ } catch {
2666
+ return true;
2667
+ }
2668
+ const payload = JSON.stringify({ pid: process.pid, at: Date.now() });
2669
+ for (let attempt = 0; attempt < 2; attempt++) {
2670
+ try {
2671
+ fs.writeFileSync(file, payload, { flag: "wx" });
2672
+ lockedPorts.add(port);
2673
+ return true;
2674
+ } catch (error) {
2675
+ if (error.code !== "EEXIST") return true;
2676
+ try {
2677
+ const held = JSON.parse(fs.readFileSync(file, "utf8"));
2678
+ const fresh = typeof held.at === "number" && Date.now() - held.at < LOCK_MAX_AGE_MS;
2679
+ if (fresh && typeof held.pid === "number" && pidIsAlive(held.pid)) return false;
2680
+ fs.unlinkSync(file);
2681
+ } catch {
2682
+ }
2683
+ }
2684
+ }
2685
+ return false;
2686
+ }
2687
+ function releasePortLock(port) {
2688
+ if (!lockedPorts.delete(port)) return;
2689
+ try {
2690
+ const fs = require("node:fs");
2691
+ const dir = lockDir();
2692
+ if (dir) fs.unlinkSync(require("node:path").join(dir, `${port}.lock`));
2693
+ } catch {
2694
+ }
2695
+ }
2566
2696
  var projectPortIndex = /* @__PURE__ */ new Map();
2567
2697
  function getPortRange(project) {
2568
2698
  const key = project;
@@ -2573,15 +2703,22 @@ function getPortRange(project) {
2573
2703
  }
2574
2704
  async function reservePort(project, preferredPort) {
2575
2705
  const range = getPortRange(project);
2576
- if (preferredPort !== void 0) {
2706
+ const handle = (port) => ({
2707
+ port,
2708
+ // Frees the socket only, so the server about to be spawned can bind the port; the lock file stays
2709
+ releaseSocket: async () => {
2710
+ await releasePort(port);
2711
+ },
2712
+ release: async () => {
2713
+ await releasePort(port);
2714
+ releasePortLock(port);
2715
+ }
2716
+ });
2717
+ const wantsSpecificPort = preferredPort !== void 0 && Number.isInteger(preferredPort) && preferredPort > 0;
2718
+ if (wantsSpecificPort) {
2577
2719
  const reservation = await tryReservePort(preferredPort, project);
2578
2720
  if (reservation) {
2579
- return {
2580
- port: preferredPort,
2581
- release: async () => {
2582
- await releasePort(preferredPort);
2583
- }
2584
- };
2721
+ return handle(preferredPort);
2585
2722
  }
2586
2723
  console.warn(`[PortRegistry] Preferred port ${preferredPort} not available for ${project}, allocating from range`);
2587
2724
  }
@@ -2590,30 +2727,19 @@ async function reservePort(project, preferredPort) {
2590
2727
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
2591
2728
  const port = range.start + index % range.size;
2592
2729
  index = (index + 1) % range.size;
2593
- if (reservedPorts.has(port)) {
2730
+ if (reservedPorts.has(port) || lockedPorts.has(port)) {
2594
2731
  continue;
2595
2732
  }
2596
2733
  const reservation = await tryReservePort(port, project);
2597
2734
  if (reservation) {
2598
2735
  projectPortIndex.set(project, index);
2599
- return {
2600
- port,
2601
- release: async () => {
2602
- await releasePort(port);
2603
- }
2604
- };
2736
+ return handle(port);
2605
2737
  }
2606
2738
  }
2607
- const dynamicPort = await findAvailablePortInRange(51e3, 52e3);
2608
- if (dynamicPort) {
2609
- const reservation = await tryReservePort(dynamicPort, project);
2610
- if (reservation) {
2611
- return {
2612
- port: dynamicPort,
2613
- release: async () => {
2614
- await releasePort(dynamicPort);
2615
- }
2616
- };
2739
+ for (let port = 51e3; port < 52e3; port++) {
2740
+ if (reservedPorts.has(port) || lockedPorts.has(port)) continue;
2741
+ if (await tryReservePort(port, project)) {
2742
+ return handle(port);
2617
2743
  }
2618
2744
  }
2619
2745
  throw new Error(
@@ -2621,9 +2747,13 @@ async function reservePort(project, preferredPort) {
2621
2747
  );
2622
2748
  }
2623
2749
  async function tryReservePort(port, project) {
2750
+ if (!acquirePortLock(port)) {
2751
+ return false;
2752
+ }
2624
2753
  return new Promise((resolve) => {
2625
2754
  const server = (0, import_net.createServer)();
2626
2755
  server.once("error", () => {
2756
+ releasePortLock(port);
2627
2757
  resolve(false);
2628
2758
  });
2629
2759
  server.listen(port, () => {
@@ -2649,31 +2779,6 @@ async function releasePort(port) {
2649
2779
  });
2650
2780
  });
2651
2781
  }
2652
- async function findAvailablePortInRange(start, end) {
2653
- for (let port = start; port < end; port++) {
2654
- if (reservedPorts.has(port)) {
2655
- continue;
2656
- }
2657
- const available = await isPortAvailable(port);
2658
- if (available) {
2659
- return port;
2660
- }
2661
- }
2662
- return null;
2663
- }
2664
- async function isPortAvailable(port) {
2665
- return new Promise((resolve) => {
2666
- const server = (0, import_net.createServer)();
2667
- server.once("error", () => {
2668
- resolve(false);
2669
- });
2670
- server.listen(port, () => {
2671
- server.close(() => {
2672
- resolve(true);
2673
- });
2674
- });
2675
- });
2676
- }
2677
2782
 
2678
2783
  // libs/testing/src/server/test-server.ts
2679
2784
  var DEBUG_SERVER = process.env["DEBUG_SERVER"] === "1" || process.env["DEBUG"] === "1";
@@ -2682,8 +2787,11 @@ var TestServer = class _TestServer {
2682
2787
  options;
2683
2788
  _info;
2684
2789
  logs = [];
2790
+ /** Frees the port reservation (socket and cross-process lock); held until stop() */
2685
2791
  portRelease = null;
2686
- constructor(options, port, portRelease) {
2792
+ /** Frees only the reservation socket, just before the server is spawned */
2793
+ portReleaseSocket = null;
2794
+ constructor(options, port, portRelease, portReleaseSocket) {
2687
2795
  this.options = {
2688
2796
  port,
2689
2797
  project: options.project,
@@ -2695,6 +2803,7 @@ var TestServer = class _TestServer {
2695
2803
  debug: options.debug ?? DEBUG_SERVER
2696
2804
  };
2697
2805
  this.portRelease = portRelease ?? null;
2806
+ this.portReleaseSocket = portReleaseSocket ?? portRelease ?? null;
2698
2807
  this._info = {
2699
2808
  baseUrl: `http://localhost:${port}`,
2700
2809
  port
@@ -2707,8 +2816,8 @@ var TestServer = class _TestServer {
2707
2816
  const project = options.project ?? "default";
2708
2817
  const maxAttempts = 3;
2709
2818
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
2710
- const { port, release } = await reservePort(project, options.port);
2711
- const server = new _TestServer(options, port, release);
2819
+ const { port, release, releaseSocket } = await reservePort(project, options.port);
2820
+ const server = new _TestServer(options, port, release, releaseSocket);
2712
2821
  try {
2713
2822
  await server.startProcess();
2714
2823
  return server;
@@ -2748,7 +2857,7 @@ var TestServer = class _TestServer {
2748
2857
  }
2749
2858
  const maxAttempts = 3;
2750
2859
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
2751
- const { port, release } = await reservePort(project, options.port);
2860
+ const { port, release, releaseSocket } = await reservePort(project, options.port);
2752
2861
  const serverOptions = {
2753
2862
  ...options,
2754
2863
  port,
@@ -2756,7 +2865,7 @@ var TestServer = class _TestServer {
2756
2865
  command: `npx nx serve ${project} --port ${port}`,
2757
2866
  cwd: options.cwd ?? process.cwd()
2758
2867
  };
2759
- const server = new _TestServer(serverOptions, port, release);
2868
+ const server = new _TestServer(serverOptions, port, release, releaseSocket);
2760
2869
  try {
2761
2870
  await server.startProcess();
2762
2871
  return server;
@@ -2814,10 +2923,17 @@ var TestServer = class _TestServer {
2814
2923
  * Stop the test server
2815
2924
  */
2816
2925
  async stop() {
2817
- if (this.portRelease) {
2818
- await this.portRelease();
2819
- this.portRelease = null;
2926
+ try {
2927
+ await this.stopProcess();
2928
+ } finally {
2929
+ if (this.portRelease) {
2930
+ await this.portRelease();
2931
+ this.portRelease = null;
2932
+ }
2933
+ this.portReleaseSocket = null;
2820
2934
  }
2935
+ }
2936
+ async stopProcess() {
2821
2937
  if (this.process) {
2822
2938
  this.log("Stopping server...");
2823
2939
  if (this.process.exitCode !== null || this.process.signalCode !== null) {
@@ -2890,7 +3006,7 @@ var TestServer = class _TestServer {
2890
3006
  * Restart the server
2891
3007
  */
2892
3008
  async restart() {
2893
- await this.stop();
3009
+ await this.stopProcess();
2894
3010
  await this.startProcess();
2895
3011
  }
2896
3012
  /**
@@ -2920,9 +3036,9 @@ var TestServer = class _TestServer {
2920
3036
  PORT: String(this.options.port)
2921
3037
  };
2922
3038
  const runtimeEnv = withWorkspaceProtocolFallback(env, this.options.cwd);
2923
- if (this.portRelease) {
2924
- await this.portRelease();
2925
- this.portRelease = null;
3039
+ if (this.portReleaseSocket) {
3040
+ await this.portReleaseSocket();
3041
+ this.portReleaseSocket = null;
2926
3042
  await sleep2(300);
2927
3043
  }
2928
3044
  this.process = (0, import_child_process.spawn)(this.options.command, [], {
@@ -2932,9 +3048,8 @@ var TestServer = class _TestServer {
2932
3048
  detached: true,
2933
3049
  stdio: ["pipe", "pipe", "pipe"]
2934
3050
  });
2935
- if (this.process.pid !== void 0) {
2936
- this._info.pid = this.process.pid;
2937
- }
3051
+ this._info.pid = this.process.pid;
3052
+ this._info.shellPid = this.process.pid;
2938
3053
  let processExited = false;
2939
3054
  let exitCode = null;
2940
3055
  let exitError = null;
@@ -3037,6 +3152,8 @@ ${allLogs}`);
3037
3152
  });
3038
3153
  if (response.ok || response.status === 404) {
3039
3154
  this.log(`Server is ready after ${healthCheckAttempts} health check attempts`);
3155
+ const listeningPid = await findListeningPid(this.options.port);
3156
+ if (listeningPid !== void 0) this._info.pid = listeningPid;
3040
3157
  return;
3041
3158
  }
3042
3159
  lastHealthCheckError = `HTTP ${response.status}: ${response.statusText}`;
@@ -3081,6 +3198,19 @@ TIP: Set DEBUG_SERVER=1 or DEBUG=1 environment variable for verbose output`
3081
3198
  function sleep2(ms) {
3082
3199
  return new Promise((resolve) => setTimeout(resolve, ms));
3083
3200
  }
3201
+ function findListeningPid(port) {
3202
+ if (process.platform === "win32") return Promise.resolve(void 0);
3203
+ return new Promise((resolve) => {
3204
+ (0, import_child_process.execFile)("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], { timeout: 2e3 }, (error, stdout) => {
3205
+ if (error) {
3206
+ resolve(void 0);
3207
+ return;
3208
+ }
3209
+ const pid = parseInt(String(stdout).split("\n")[0] ?? "", 10);
3210
+ resolve(Number.isInteger(pid) && pid > 0 ? pid : void 0);
3211
+ });
3212
+ });
3213
+ }
3084
3214
  function withWorkspaceProtocolFallback(env, cwd) {
3085
3215
  if (findInstalledProtocolPackageDir(cwd)) {
3086
3216
  return env;
@@ -3197,81 +3327,152 @@ function gatewayTokenBinding(serverUrl, entryPath) {
3197
3327
  }
3198
3328
 
3199
3329
  // libs/testing/src/fixtures/test-fixture.ts
3200
- var describe = import_globals.describe;
3201
- var beforeAll = import_globals.beforeAll;
3202
- var beforeEach = import_globals.beforeEach;
3203
- var afterEach = import_globals.afterEach;
3204
- var afterAll = import_globals.afterAll;
3205
- var it = import_globals.it;
3206
- var currentConfig = {};
3207
- var serverInstance = null;
3208
- var tokenFactory = null;
3209
- var serverStartedByUs = false;
3210
- var skipScopes = [{ skipped: false }];
3211
- function currentSkipScope() {
3212
- return skipScopes[skipScopes.length - 1];
3330
+ var jestApi;
3331
+ function jestGlobals() {
3332
+ if (jestApi) return jestApi;
3333
+ try {
3334
+ const api = require("@jest/globals");
3335
+ jestApi = {
3336
+ describe: api["describe"],
3337
+ beforeAll: api["beforeAll"],
3338
+ beforeEach: api["beforeEach"],
3339
+ afterEach: api["afterEach"],
3340
+ afterAll: api["afterAll"],
3341
+ it: api["it"]
3342
+ };
3343
+ return jestApi;
3344
+ } catch (error) {
3345
+ throw new Error(
3346
+ `@frontmcp/testing fixtures ("test", "test.use", ...) can only be used inside a Jest test file: ${error instanceof Error ? error.message : String(error)}`,
3347
+ { cause: error }
3348
+ );
3349
+ }
3213
3350
  }
3214
- async function initializeSharedResources() {
3215
- if (!serverInstance) {
3216
- if (currentConfig.baseUrl && !currentConfig.server) {
3217
- serverInstance = TestServer.connect(currentConfig.baseUrl);
3218
- serverStartedByUs = false;
3219
- } else if (currentConfig.server) {
3220
- const serverCommand = resolveServerCommand(currentConfig.server);
3221
- const isDebug = currentConfig.logLevel === "debug" || process.env["DEBUG"] === "1" || process.env["DEBUG_SERVER"] === "1";
3351
+ var scopes = [{ skipped: false, config: {}, configured: false, cleanupRegistered: false }];
3352
+ function currentScope() {
3353
+ return scopes[scopes.length - 1];
3354
+ }
3355
+ var serverEntries = /* @__PURE__ */ new Map();
3356
+ function resolveConfig(chain) {
3357
+ const merged = {};
3358
+ for (const scope of chain) {
3359
+ const { env, ...rest } = scope.config;
3360
+ Object.assign(merged, rest);
3361
+ if (env) merged.env = { ...merged.env, ...env };
3362
+ }
3363
+ return merged;
3364
+ }
3365
+ function ownerScopeOf(chain) {
3366
+ for (let i = chain.length - 1; i > 0; i--) {
3367
+ if (chain[i].configured) return chain[i];
3368
+ }
3369
+ return chain[0];
3370
+ }
3371
+ function serverKey(config) {
3372
+ const env = Object.fromEntries(Object.entries(config.env ?? {}).sort(([a], [b]) => a.localeCompare(b)));
3373
+ return JSON.stringify({
3374
+ server: config.server,
3375
+ baseUrl: config.baseUrl,
3376
+ project: config.project,
3377
+ port: config.port,
3378
+ entryPath: config.entryPath,
3379
+ startupTimeout: config.startupTimeout,
3380
+ logLevel: config.logLevel,
3381
+ auth: config.auth,
3382
+ env
3383
+ });
3384
+ }
3385
+ function serverEnv(config) {
3386
+ const extra = {};
3387
+ if (config.auth?.mode) extra["FRONTMCP_TEST_AUTH_MODE"] = config.auth.mode;
3388
+ if (config.auth?.type) extra["FRONTMCP_TEST_AUTH_TYPE"] = config.auth.type;
3389
+ if (Object.keys(extra).length === 0 && !config.env) return void 0;
3390
+ return { ...extra, ...config.env };
3391
+ }
3392
+ async function startServerEntry(config, owner) {
3393
+ let server;
3394
+ let startedByUs = false;
3395
+ if (config.baseUrl && !config.server) {
3396
+ server = TestServer.connect(config.baseUrl);
3397
+ } else if (config.server) {
3398
+ const serverCommand = resolveServerCommand(config.server);
3399
+ const isDebug = config.logLevel === "debug" || process.env["DEBUG"] === "1" || process.env["DEBUG_SERVER"] === "1";
3400
+ if (isDebug) {
3401
+ console.log(`[TestFixture] Starting server: ${serverCommand}`);
3402
+ }
3403
+ try {
3404
+ server = await TestServer.start({
3405
+ project: config.project,
3406
+ port: config.port,
3407
+ command: serverCommand,
3408
+ env: serverEnv(config),
3409
+ startupTimeout: config.startupTimeout ?? 3e4,
3410
+ debug: isDebug
3411
+ });
3412
+ startedByUs = true;
3222
3413
  if (isDebug) {
3223
- console.log(`[TestFixture] Starting server: ${serverCommand}`);
3414
+ console.log(`[TestFixture] Server started at ${server.info.baseUrl}`);
3224
3415
  }
3225
- try {
3226
- serverInstance = await TestServer.start({
3227
- project: currentConfig.project,
3228
- port: currentConfig.port,
3229
- command: serverCommand,
3230
- env: currentConfig.env,
3231
- startupTimeout: currentConfig.startupTimeout ?? 3e4,
3232
- debug: isDebug
3233
- });
3234
- serverStartedByUs = true;
3235
- if (isDebug) {
3236
- console.log(`[TestFixture] Server started at ${serverInstance.info.baseUrl}`);
3237
- }
3238
- } catch (error) {
3239
- const errMsg = error instanceof Error ? error.message : String(error);
3240
- throw new Error(
3241
- `Failed to start test server.
3416
+ } catch (error) {
3417
+ const errMsg = error instanceof Error ? error.message : String(error);
3418
+ throw new Error(
3419
+ `Failed to start test server.
3242
3420
 
3243
- Server entry: ${currentConfig.server}
3244
- Project: ${currentConfig.project ?? "default"}
3421
+ Server entry: ${config.server}
3422
+ Project: ${config.project ?? "default"}
3245
3423
  Command: ${serverCommand}
3246
3424
 
3247
- Error: ${errMsg}`
3248
- );
3249
- }
3250
- } else {
3251
- throw new Error(
3252
- 'test.use() requires either "server" (entry file path) or "baseUrl" (for external server) option'
3425
+ Error: ${errMsg}`,
3426
+ { cause: error }
3253
3427
  );
3254
3428
  }
3429
+ } else {
3430
+ throw new Error('test.use() requires either "server" (entry file path) or "baseUrl" (for external server) option');
3255
3431
  }
3256
- if (!tokenFactory) {
3257
- const sharedSecret = currentConfig.env?.["JWT_SECRET"];
3258
- const binding = serverInstance ? gatewayTokenBinding(resolveClientBaseUrl(serverInstance), currentConfig.entryPath) : void 0;
3259
- tokenFactory = new TestTokenFactory(sharedSecret ? { hmacSecret: sharedSecret, ...binding } : {});
3260
- }
3432
+ const sharedSecret = config.env?.["JWT_SECRET"];
3433
+ const binding = sharedSecret ? gatewayTokenBinding(config.baseUrl ?? server.info.baseUrl, config.entryPath) : void 0;
3434
+ const tokenFactory = new TestTokenFactory(sharedSecret ? { hmacSecret: sharedSecret, ...binding } : {});
3435
+ return { server, startedByUs, tokenFactory, owner };
3261
3436
  }
3262
- async function createTestFixtures() {
3263
- await initializeSharedResources();
3264
- if (!serverInstance) {
3265
- throw new Error("Server instance not initialized");
3266
- }
3267
- if (!tokenFactory) {
3268
- throw new Error("Token factory not initialized");
3437
+ function acquireServer(chain) {
3438
+ const config = resolveConfig(chain);
3439
+ const key = serverKey(config);
3440
+ let entry = serverEntries.get(key);
3441
+ if (!entry) {
3442
+ entry = startServerEntry(config, ownerScopeOf(chain));
3443
+ serverEntries.set(key, entry);
3444
+ entry.catch(() => serverEntries.delete(key));
3445
+ }
3446
+ return entry;
3447
+ }
3448
+ async function stopServers(owner) {
3449
+ for (const [key, pending] of Array.from(serverEntries)) {
3450
+ let entry;
3451
+ try {
3452
+ entry = await pending;
3453
+ } catch {
3454
+ serverEntries.delete(key);
3455
+ continue;
3456
+ }
3457
+ if (owner && entry.owner !== owner) continue;
3458
+ serverEntries.delete(key);
3459
+ if (entry.startedByUs) {
3460
+ await entry.server.stop();
3461
+ }
3269
3462
  }
3463
+ }
3464
+ var fixtureContexts = /* @__PURE__ */ new WeakMap();
3465
+ async function buildFixtures(chain) {
3466
+ const config = resolveConfig(chain);
3467
+ const entry = await acquireServer(chain);
3468
+ const { server, tokenFactory } = entry;
3469
+ const publicMode = config.publicMode ?? config.auth?.mode === "public";
3470
+ const clientConfig = { ...config, publicMode };
3270
3471
  const clientInstance = McpTestClient.create({
3271
- baseUrl: resolveClientBaseUrl(serverInstance),
3272
- entryPath: currentConfig.entryPath,
3273
- transport: currentConfig.transport ?? "streamable-http",
3274
- publicMode: currentConfig.publicMode
3472
+ baseUrl: resolveClientBaseUrl(server, clientConfig),
3473
+ entryPath: clientConfig.entryPath,
3474
+ transport: clientConfig.transport ?? "streamable-http",
3475
+ publicMode
3275
3476
  }).build();
3276
3477
  try {
3277
3478
  await clientInstance.connect();
@@ -3285,17 +3486,24 @@ async function createTestFixtures() {
3285
3486
  throw err;
3286
3487
  }
3287
3488
  }
3288
- const auth = createAuthFixture(tokenFactory);
3289
- const server = createServerFixture(serverInstance);
3290
- return {
3489
+ const context = { extraClients: [] };
3490
+ const fixtures = {
3291
3491
  mcp: clientInstance,
3292
- auth,
3293
- server
3492
+ auth: createAuthFixture(tokenFactory),
3493
+ server: createServerFixture(server, clientConfig, clientInstance, context)
3294
3494
  };
3495
+ fixtureContexts.set(fixtures, context);
3496
+ return fixtures;
3497
+ }
3498
+ async function createTestFixtures() {
3499
+ return buildFixtures(scopes.slice());
3500
+ }
3501
+ async function initializeSharedResources() {
3502
+ await acquireServer(scopes.slice());
3295
3503
  }
3296
3504
  async function cleanupTestFixtures(fixtures, testFailed = false) {
3297
- if (testFailed && serverInstance) {
3298
- const logs = serverInstance.getLogs();
3505
+ if (testFailed) {
3506
+ const logs = fixtures.server.getLogs();
3299
3507
  if (logs.length > 0) {
3300
3508
  console.error("\n[TestFixture] === Server Logs (test failed) ===");
3301
3509
  const recentLogs = logs.slice(-50);
@@ -3306,20 +3514,21 @@ async function cleanupTestFixtures(fixtures, testFailed = false) {
3306
3514
  console.error("[TestFixture] === End Server Logs ===\n");
3307
3515
  }
3308
3516
  }
3517
+ const extras = fixtureContexts.get(fixtures)?.extraClients ?? [];
3518
+ for (const client of extras) {
3519
+ if (client.isConnected()) {
3520
+ await client.disconnect().catch(() => void 0);
3521
+ }
3522
+ }
3309
3523
  if (fixtures.mcp.isConnected()) {
3310
3524
  await fixtures.mcp.disconnect();
3311
3525
  }
3312
3526
  }
3313
3527
  async function cleanupSharedResources() {
3314
- if (serverInstance && serverStartedByUs) {
3315
- await serverInstance.stop();
3316
- }
3317
- serverInstance = null;
3318
- tokenFactory = null;
3319
- serverStartedByUs = false;
3528
+ await stopServers();
3320
3529
  }
3321
- function resolveClientBaseUrl(server) {
3322
- return currentConfig.baseUrl ?? server.info.baseUrl;
3530
+ function resolveClientBaseUrl(server, config) {
3531
+ return config.baseUrl ?? server.info.baseUrl;
3323
3532
  }
3324
3533
  function createAuthFixture(factory) {
3325
3534
  const users = {
@@ -3365,28 +3574,39 @@ function createAuthFixture(factory) {
3365
3574
  getAudience: () => factory.getAudience()
3366
3575
  };
3367
3576
  }
3368
- function createServerFixture(server) {
3577
+ function createServerFixture(server, config, mcp, context) {
3369
3578
  return {
3370
- info: server.info,
3579
+ // Live view: `pid` is filled in once the server is listening and follows restarts
3580
+ get info() {
3581
+ return server.info;
3582
+ },
3371
3583
  createClient: async (opts) => {
3372
- return McpTestClient.create({
3373
- baseUrl: resolveClientBaseUrl(server),
3374
- entryPath: opts?.entryPath ?? currentConfig.entryPath,
3584
+ const client = await McpTestClient.create({
3585
+ baseUrl: resolveClientBaseUrl(server, config),
3586
+ entryPath: opts?.entryPath ?? config.entryPath,
3375
3587
  transport: opts?.transport ?? "streamable-http",
3376
3588
  auth: opts?.token ? { token: opts.token } : void 0,
3377
3589
  clientInfo: opts?.clientInfo,
3378
- publicMode: currentConfig.publicMode
3590
+ publicMode: config.publicMode
3379
3591
  }).buildAndConnect();
3592
+ context.extraClients.push(client);
3593
+ return client;
3380
3594
  },
3381
3595
  createClientBuilder: () => {
3382
3596
  const builder = new McpTestClientBuilder({
3383
- baseUrl: resolveClientBaseUrl(server),
3384
- entryPath: currentConfig.entryPath,
3385
- publicMode: currentConfig.publicMode
3597
+ baseUrl: resolveClientBaseUrl(server, config),
3598
+ entryPath: config.entryPath,
3599
+ publicMode: config.publicMode
3386
3600
  });
3387
3601
  return builder;
3388
3602
  },
3389
- restart: () => server.restart(),
3603
+ restart: async () => {
3604
+ const reconnect = [mcp, ...context.extraClients].filter((client) => client.isConnected());
3605
+ await server.restart();
3606
+ for (const client of reconnect) {
3607
+ await client.reconnect();
3608
+ }
3609
+ },
3390
3610
  getLogs: () => server.getLogs(),
3391
3611
  clearLogs: () => server.clearLogs()
3392
3612
  };
@@ -3397,12 +3617,12 @@ function resolveServerCommand(server) {
3397
3617
  }
3398
3618
  return `npx tsx ${server}`;
3399
3619
  }
3400
- function runWithFixtures(fn) {
3401
- return async () => {
3402
- const fixtures = await createTestFixtures();
3620
+ function runWithFixtures(fn, chain) {
3621
+ return async (...extra) => {
3622
+ const fixtures = await buildFixtures(chain);
3403
3623
  let testFailed = false;
3404
3624
  try {
3405
- await fn(fixtures);
3625
+ await fn(fixtures, ...extra);
3406
3626
  } catch (error) {
3407
3627
  testFailed = true;
3408
3628
  throw error;
@@ -3411,24 +3631,37 @@ function runWithFixtures(fn) {
3411
3631
  }
3412
3632
  };
3413
3633
  }
3634
+ function skipTitle(name, scope) {
3635
+ return scope.reason ? `${name} (skipped: ${scope.reason})` : name;
3636
+ }
3414
3637
  function testWithFixtures(name, fn) {
3415
- const scope = currentSkipScope();
3638
+ const chain = scopes.slice();
3639
+ const scope = currentScope();
3416
3640
  if (scope.skipped) {
3417
- it.skip(scope.reason ? `${name} (skipped: ${scope.reason})` : name, runWithFixtures(fn));
3641
+ jestGlobals().it.skip(skipTitle(name, scope), runWithFixtures(fn, chain));
3418
3642
  return;
3419
3643
  }
3420
- it(name, runWithFixtures(fn));
3644
+ jestGlobals().it(name, runWithFixtures(fn, chain));
3421
3645
  }
3422
3646
  function use(config) {
3423
- currentConfig = { ...currentConfig, ...config };
3424
- afterAll(async () => {
3425
- await cleanupSharedResources();
3426
- });
3647
+ const scope = currentScope();
3648
+ scope.config = {
3649
+ ...scope.config,
3650
+ ...config,
3651
+ env: config.env ? { ...scope.config.env, ...config.env } : scope.config.env
3652
+ };
3653
+ scope.configured = true;
3654
+ if (!scope.cleanupRegistered) {
3655
+ scope.cleanupRegistered = true;
3656
+ jestGlobals().afterAll(async () => {
3657
+ await stopServers(scope === scopes[0] ? void 0 : scope);
3658
+ });
3659
+ }
3427
3660
  }
3428
3661
  function skip(nameOrCondition, fnOrReason) {
3429
3662
  if (typeof nameOrCondition === "boolean") {
3430
3663
  if (nameOrCondition) {
3431
- const scope = currentSkipScope();
3664
+ const scope = currentScope();
3432
3665
  scope.skipped = true;
3433
3666
  scope.reason = typeof fnOrReason === "string" ? fnOrReason : void 0;
3434
3667
  }
@@ -3439,53 +3672,90 @@ function skip(nameOrCondition, fnOrReason) {
3439
3672
  `test.skip expects either (name: string, fn) or (condition: boolean, reason?: string); received (${typeof nameOrCondition}, ${typeof fnOrReason}).`
3440
3673
  );
3441
3674
  }
3442
- it.skip(nameOrCondition, runWithFixtures(fnOrReason));
3675
+ jestGlobals().it.skip(nameOrCondition, runWithFixtures(fnOrReason, scopes.slice()));
3443
3676
  }
3444
3677
  function only(name, fn) {
3445
- const scope = currentSkipScope();
3678
+ const scope = currentScope();
3446
3679
  if (scope.skipped) {
3447
- it.skip(scope.reason ? `${name} (skipped: ${scope.reason})` : name, runWithFixtures(fn));
3680
+ jestGlobals().it.skip(skipTitle(name, scope), runWithFixtures(fn, scopes.slice()));
3448
3681
  return;
3449
3682
  }
3450
- it.only(name, runWithFixtures(fn));
3683
+ jestGlobals().it.only(name, runWithFixtures(fn, scopes.slice()));
3451
3684
  }
3452
3685
  function todo(name) {
3453
- it.todo(name);
3686
+ jestGlobals().it.todo(name);
3454
3687
  }
3455
- function withSkipScope(register) {
3688
+ function each(table, ...tagged) {
3689
+ const chain = scopes.slice();
3690
+ const register = jestGlobals().it.each(table, ...tagged);
3456
3691
  return (name, fn) => {
3457
- register(name, () => {
3458
- const parent = currentSkipScope();
3459
- skipScopes.push({ skipped: parent.skipped, reason: parent.reason });
3460
- try {
3461
- fn();
3462
- } finally {
3463
- skipScopes.pop();
3464
- }
3692
+ const scope = currentScope();
3693
+ if (scope.skipped) {
3694
+ const skipRegister = jestGlobals().it.skip.each(table, ...tagged);
3695
+ skipRegister(skipTitle(name, scope), runWithFixtures(fn, chain));
3696
+ return;
3697
+ }
3698
+ register(name, runWithFixtures(fn, chain));
3699
+ };
3700
+ }
3701
+ function withScope(fn) {
3702
+ return (...args) => {
3703
+ const parent = currentScope();
3704
+ scopes.push({
3705
+ skipped: parent.skipped,
3706
+ reason: parent.reason,
3707
+ config: {},
3708
+ configured: false,
3709
+ cleanupRegistered: false
3465
3710
  });
3711
+ try {
3712
+ fn(...args);
3713
+ } finally {
3714
+ scopes.pop();
3715
+ }
3466
3716
  };
3467
3717
  }
3468
- function eachWithSkipScope(each) {
3469
- return ((...eachArgs) => {
3470
- const register = each(...eachArgs);
3471
- return withSkipScope(register);
3472
- });
3718
+ function scopedRegister(pick) {
3719
+ return (name, fn, timeout) => {
3720
+ pick()(name, withScope(fn), timeout);
3721
+ };
3473
3722
  }
3474
- var describeWithSkipScope = Object.assign(withSkipScope(describe), describe, {
3475
- only: Object.assign(withSkipScope(describe.only), { each: eachWithSkipScope(describe.only.each) }),
3476
- skip: Object.assign(withSkipScope(describe.skip), { each: eachWithSkipScope(describe.skip.each) }),
3477
- each: eachWithSkipScope(describe.each)
3478
- });
3723
+ function scopedEach(pick) {
3724
+ return (...eachArgs) => (name, fn, timeout) => {
3725
+ const register = pick()(...eachArgs);
3726
+ register(name, withScope(fn), timeout);
3727
+ };
3728
+ }
3729
+ var describeWithScope = Object.assign(
3730
+ scopedRegister(() => jestGlobals().describe),
3731
+ {
3732
+ // `describe.skip` still evaluates its callback during collection, so it needs a scope too
3733
+ only: Object.assign(
3734
+ scopedRegister(() => jestGlobals().describe.only),
3735
+ {
3736
+ each: scopedEach(() => jestGlobals().describe.only.each)
3737
+ }
3738
+ ),
3739
+ skip: Object.assign(
3740
+ scopedRegister(() => jestGlobals().describe.skip),
3741
+ {
3742
+ each: scopedEach(() => jestGlobals().describe.skip.each)
3743
+ }
3744
+ ),
3745
+ each: scopedEach(() => jestGlobals().describe.each)
3746
+ }
3747
+ );
3479
3748
  var test = testWithFixtures;
3480
3749
  test.use = use;
3481
- test.describe = describeWithSkipScope;
3482
- test.beforeAll = beforeAll;
3483
- test.beforeEach = beforeEach;
3484
- test.afterEach = afterEach;
3485
- test.afterAll = afterAll;
3750
+ test.describe = describeWithScope;
3751
+ test.beforeAll = ((...args) => jestGlobals().beforeAll(...args));
3752
+ test.beforeEach = ((...args) => jestGlobals().beforeEach(...args));
3753
+ test.afterEach = ((...args) => jestGlobals().afterEach(...args));
3754
+ test.afterAll = ((...args) => jestGlobals().afterAll(...args));
3486
3755
  test.skip = skip;
3487
3756
  test.only = only;
3488
3757
  test.todo = todo;
3758
+ test.each = each;
3489
3759
  // Annotate the CommonJS export names for ESM import in node:
3490
3760
  0 && (module.exports = {
3491
3761
  cleanupSharedResources,