@rvoh/psychic-spec-helpers 3.1.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -32,8 +32,8 @@ export default function providePuppeteerViteMatchers() {
32
32
  async toMatchTextContent(page, text, opts) {
33
33
  return await toMatchTextContent(page, text, opts);
34
34
  },
35
- async toNotMatchTextContent(page, text) {
36
- return await toNotMatchTextContent(page, text);
35
+ async toNotMatchTextContent(page, text, opts) {
36
+ return await toNotMatchTextContent(page, text, opts);
37
37
  },
38
38
  async toHaveSelector(page, cssSelector, opts) {
39
39
  return await toHaveSelector(page, cssSelector, opts);
@@ -1,19 +1,35 @@
1
- export default async function getAllTextContentFromPage(page) {
1
+ export default async function getAllTextContentFromPage(page, selector = 'body') {
2
2
  // Evaluate and extract all text content on the page
3
- const allText = await page.evaluate(() => {
3
+ const allText = await page.evaluate(selector => {
4
4
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
5
5
  // @ts-ignore
6
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
7
- const elements = document.body.querySelectorAll('*');
6
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument
7
+ const roots = Array.from(document.querySelectorAll(selector));
8
8
  const textContentArray = [];
9
- elements.forEach(element => {
10
- // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
11
- if (element.textContent.trim() !== '') {
12
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
13
- textContentArray.push(element.innerText.trim());
14
- }
9
+ roots.forEach(root => {
10
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument
11
+ const elements = [root, ...Array.from(root.querySelectorAll('*'))];
12
+ elements.forEach(element => {
13
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
14
+ const tagName = String(element.tagName || '').toLowerCase();
15
+ if (['script', 'style', 'noscript'].includes(tagName))
16
+ return;
17
+ let elementText;
18
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
19
+ if (typeof element.innerText === 'string') {
20
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
21
+ elementText = element.innerText;
22
+ }
23
+ else {
24
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
25
+ elementText = element.textContent;
26
+ }
27
+ const normalizedText = elementText?.trim();
28
+ if (normalizedText)
29
+ textContentArray.push(normalizedText);
30
+ });
15
31
  });
16
32
  return textContentArray.join(' ');
17
- });
33
+ }, selector);
18
34
  return allText;
19
35
  }
@@ -1,23 +1,27 @@
1
- import applyDefaultWaitForOpts from '../helpers/applyDefaultWaitForOpts.js';
2
- export default async function toMatchTextContent(page, text, opts = {}) {
3
- try {
4
- await page.waitForSelector(`${opts.selector || 'body'}::-p-text(${text.replace(/"/g, '\\"')})`, applyDefaultWaitForOpts(opts));
1
+ import evaluateWithRetryAndTimeout from '../internal/evaluateWithRetryAndTimeout.js';
2
+ import getAllTextContentFromPage from '../internal/getAllTextContentFromPage.js';
3
+ import requirePuppeteerPage from '../internal/requirePuppeteerPage.js';
4
+ export default async function toMatchTextContent(argumentPassedToExpect, expected, opts = {}) {
5
+ return await evaluateWithRetryAndTimeout(argumentPassedToExpect, async () => {
6
+ requirePuppeteerPage(argumentPassedToExpect);
7
+ const actual = await getAllTextContentFromPage(argumentPassedToExpect, opts.selector);
8
+ if (expected instanceof RegExp)
9
+ expected.lastIndex = 0;
5
10
  return {
6
- pass: true,
7
- message: () => {
8
- throw new Error('Cannot negate toMatchTextContent, use toNotMatchTextContent instead');
9
- },
11
+ pass: typeof expected === 'string' ? actual.includes(expected) : expected.test(actual),
12
+ actual,
10
13
  };
11
- }
12
- catch {
13
- return {
14
- pass: false,
15
- message: () => `
14
+ }, {
15
+ successText: () => {
16
+ throw new Error('Cannot negate toMatchTextContent, use toNotMatchTextContent instead');
17
+ },
18
+ failureText: actual => `
16
19
  expected ${opts.selector || 'body'} with text:
17
- ${text}
20
+ ${expected.toString()}
18
21
 
19
- but no text was found within that selector
22
+ but no matching text was found within that selector:
23
+ ${actual}
20
24
  `,
21
- };
22
- }
25
+ timeout: opts.timeout,
26
+ });
23
27
  }
@@ -4,16 +4,18 @@ import requirePuppeteerPage from '../internal/requirePuppeteerPage.js';
4
4
  export default async function toNotMatchTextContent(argumentPassedToExpect, expected, opts = {}) {
5
5
  return await evaluateWithRetryAndTimeout(argumentPassedToExpect, async () => {
6
6
  requirePuppeteerPage(argumentPassedToExpect);
7
- const actual = await getAllTextContentFromPage(argumentPassedToExpect);
7
+ const actual = await getAllTextContentFromPage(argumentPassedToExpect, opts.selector);
8
+ if (expected instanceof RegExp)
9
+ expected.lastIndex = 0;
8
10
  return {
9
- pass: !actual.includes(expected),
11
+ pass: typeof expected === 'string' ? !actual.includes(expected) : !expected.test(actual),
10
12
  actual,
11
13
  };
12
14
  }, {
13
15
  successText: () => {
14
16
  throw new Error('Cannot negate toNotMatchTextContent, use toMatchTextContent instead');
15
17
  },
16
- failureText: r => `Expected ${r} to not match text ${expected}, but it did`,
18
+ failureText: r => `Expected ${r} to not match text ${expected.toString()}, but it did`,
17
19
  timeout: opts.timeout,
18
20
  });
19
21
  }
@@ -1,14 +1,28 @@
1
- // eslint-disable-next-line
2
- const _server = undefined;
1
+ // The booted spec server is cached for the lifetime of the worker process so it
2
+ // is created and booted exactly once, then reused by every spec — instead of
3
+ // re-booting a brand-new PsychicServer for each spec, which re-runs application
4
+ // initialization and churns database/websocket connections (a significant
5
+ // source of slow suites and flaky, connection-exhaustion-driven failures).
6
+ //
7
+ // The cache lives on `globalThis` rather than in a module-scoped variable
8
+ // because isolating test runners (e.g. Vitest with the default `isolate: true`)
9
+ // reset the module registry between spec files. A module-scoped cache would be
10
+ // discarded at every file boundary, forcing a fresh boot per file; `globalThis`
11
+ // persists for the whole worker process, so the server boots once per worker.
12
+ const CACHED_SPEC_SERVER_KEY = Symbol.for('@rvoh/psychic-spec-helpers:cachedSpecServer');
3
13
  // eslint-disable-next-line
4
14
  export default async function createPsychicServer(PsychicServer) {
5
15
  // eslint-disable-next-line
6
- if (_server)
7
- return _server;
16
+ const store = globalThis;
17
+ // eslint-disable-next-line
18
+ if (store[CACHED_SPEC_SERVER_KEY])
19
+ return store[CACHED_SPEC_SERVER_KEY];
8
20
  // eslint-disable-next-line
9
21
  const server = new PsychicServer();
10
22
  // eslint-disable-next-line
11
23
  await server.boot();
12
24
  // eslint-disable-next-line
25
+ store[CACHED_SPEC_SERVER_KEY] = server;
26
+ // eslint-disable-next-line
13
27
  return server;
14
28
  }
@@ -1,2 +1,2 @@
1
1
  import { Page } from 'puppeteer';
2
- export default function getAllTextContentFromPage(page: Page): Promise<string>;
2
+ export default function getAllTextContentFromPage(page: Page, selector?: string): Promise<string>;
@@ -1,7 +1,9 @@
1
1
  import { Page, WaitForSelectorOptions } from 'puppeteer';
2
- export default function toMatchTextContent(page: Page, text: string, opts?: {
2
+ export type TextContentMatcherExpected = string | RegExp;
3
+ export type TextContentMatcherOpts = {
3
4
  selector?: string;
4
- } & WaitForSelectorOptions): Promise<{
5
- pass: boolean;
5
+ } & WaitForSelectorOptions;
6
+ export default function toMatchTextContent(argumentPassedToExpect: Page, expected: TextContentMatcherExpected, opts?: TextContentMatcherOpts): Promise<{
6
7
  message: () => string;
8
+ pass: boolean;
7
9
  }>;
@@ -1,5 +1,6 @@
1
- import { Page, WaitForSelectorOptions } from 'puppeteer';
2
- export default function toNotMatchTextContent(argumentPassedToExpect: Page, expected: string, opts?: WaitForSelectorOptions): Promise<{
1
+ import { Page } from 'puppeteer';
2
+ import type { TextContentMatcherExpected, TextContentMatcherOpts } from './toMatchTextContent.js';
3
+ export default function toNotMatchTextContent(argumentPassedToExpect: Page, expected: TextContentMatcherExpected, opts?: TextContentMatcherOpts): Promise<{
3
4
  message: () => string;
4
5
  pass: boolean;
5
6
  }>;
@@ -2,6 +2,7 @@ import { Page, WaitForSelectorOptions } from 'puppeteer';
2
2
  import { CustomMatcherResult } from './feature/helpers/providePuppeteerViteMatchers.js';
3
3
  import { ExpectToEvaluateOpts } from './feature/internal/evaluateWithRetryAndTimeout.js';
4
4
  import { ToFillMatcherOpts } from './feature/matchers/toFill.js';
5
+ import type { TextContentMatcherExpected, TextContentMatcherOpts } from './feature/matchers/toMatchTextContent.js';
5
6
  export { RequestBody as OpenapiRequestBody, RequestQueryParameters as OpenapiRequestQuery, ResponseBody as OpenapiResponseBody, ResponseCodeForUri as OpenapiResponseCodeForUri, } from './unit/helpers/openapiTypeHelpers.js';
6
7
  export { DreamRequestAttributes } from './unit/helpers/typeHelpers.js';
7
8
  export { default as createPsychicServer } from './unit/createPsychicServer.js';
@@ -39,8 +40,8 @@ interface PuppeteerAssertions {
39
40
  toMatchDreamModels(expected: any): CustomMatcherResult;
40
41
  toBeWithin(precision: number, expected: number): CustomMatcherResult;
41
42
  toEqualCalendarDate(expected: any): CustomMatcherResult;
42
- toMatchTextContent(expected: any, opts?: WaitForSelectorOptions): Promise<CustomMatcherResult>;
43
- toNotMatchTextContent(expected: any, opts?: WaitForSelectorOptions): Promise<CustomMatcherResult>;
43
+ toMatchTextContent(expected: TextContentMatcherExpected, opts?: TextContentMatcherOpts): Promise<CustomMatcherResult>;
44
+ toNotMatchTextContent(expected: TextContentMatcherExpected, opts?: TextContentMatcherOpts): Promise<CustomMatcherResult>;
44
45
  toHaveSelector(expected: any, opts?: WaitForSelectorOptions): Promise<CustomMatcherResult>;
45
46
  toNotHaveSelector(expected: any, opts?: WaitForSelectorOptions): Promise<CustomMatcherResult>;
46
47
  toCheck(expected: any, opts?: WaitForSelectorOptions): Promise<CustomMatcherResult>;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@rvoh/psychic-spec-helpers",
4
- "version": "3.1.0",
4
+ "version": "3.2.0",
5
5
  "description": "psychic framework spec helpers",
6
6
  "author": "RVO Health",
7
7
  "repository": {
@@ -80,5 +80,5 @@
80
80
  "dotenv": "^17.2.3",
81
81
  "pluralize-esm": "^9.0.5"
82
82
  },
83
- "packageManager": "pnpm@10.24.0+sha512.01ff8ae71b4419903b65c60fb2dc9d34cf8bb6e06d03bde112ef38f7a34d6904c424ba66bea5cdcf12890230bf39f9580473140ed9c946fef328b6e5238a345a"
83
+ "packageManager": "pnpm@11.1.3+sha512.c85357fe17ca12dd23dd7071822666dfd7e3cb76fe214e3370b5ea2fb34f2a231185509b63e717f3cd0acb38dd3f8d82bcd5e8172400ae678b70ea4fbed0896d"
84
84
  }
@@ -15,6 +15,10 @@ import toHavePath from '../matchers/toHavePath.js'
15
15
  import toHaveSelector from '../matchers/toHaveSelector.js'
16
16
  import toHaveUnchecked from '../matchers/toHaveUnchecked.js'
17
17
  import toHaveUrl from '../matchers/toHaveUrl.js'
18
+ import type {
19
+ TextContentMatcherExpected,
20
+ TextContentMatcherOpts,
21
+ } from '../matchers/toMatchTextContent.js'
18
22
  import toMatchTextContent from '../matchers/toMatchTextContent.js'
19
23
  import toNotHaveSelector from '../matchers/toNotHaveSelector.js'
20
24
  import toNotMatchTextContent from '../matchers/toNotMatchTextContent.js'
@@ -35,14 +39,18 @@ export default function providePuppeteerViteMatchers() {
35
39
  ;(global as any).expect.extend({
36
40
  async toMatchTextContent(
37
41
  page: Page,
38
- text: string,
39
- opts?: { selector?: string } & WaitForSelectorOptions
42
+ text: TextContentMatcherExpected,
43
+ opts?: TextContentMatcherOpts
40
44
  ) {
41
45
  return await toMatchTextContent(page, text, opts)
42
46
  },
43
47
 
44
- async toNotMatchTextContent(page: Page, text: string) {
45
- return await toNotMatchTextContent(page, text)
48
+ async toNotMatchTextContent(
49
+ page: Page,
50
+ text: TextContentMatcherExpected,
51
+ opts?: TextContentMatcherOpts
52
+ ) {
53
+ return await toNotMatchTextContent(page, text, opts)
46
54
  },
47
55
 
48
56
  async toHaveSelector(page: Page, cssSelector: string, opts?: WaitForSelectorOptions) {
@@ -1,26 +1,42 @@
1
1
  import { Page } from 'puppeteer'
2
2
 
3
- export default async function getAllTextContentFromPage(page: Page) {
3
+ export default async function getAllTextContentFromPage(page: Page, selector = 'body') {
4
4
  // Evaluate and extract all text content on the page
5
- const allText = await page.evaluate(() => {
5
+ const allText = await page.evaluate(selector => {
6
6
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
7
7
  // @ts-ignore
8
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
9
- const elements = document.body.querySelectorAll('*')
8
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument
9
+ const roots = Array.from(document.querySelectorAll(selector))
10
10
 
11
11
  const textContentArray: string[] = []
12
12
 
13
13
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
14
- ;(elements as any[]).forEach(element => {
15
- // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
16
- if (element.textContent.trim() !== '') {
17
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
18
- textContentArray.push(element.innerText.trim())
19
- }
14
+ ;(roots as any[]).forEach(root => {
15
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument
16
+ const elements = [root, ...Array.from(root.querySelectorAll('*'))]
17
+
18
+ elements.forEach(element => {
19
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
20
+ const tagName = String(element.tagName || '').toLowerCase()
21
+ if (['script', 'style', 'noscript'].includes(tagName)) return
22
+
23
+ let elementText: string | undefined
24
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
25
+ if (typeof element.innerText === 'string') {
26
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
27
+ elementText = element.innerText
28
+ } else {
29
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
30
+ elementText = element.textContent
31
+ }
32
+
33
+ const normalizedText = elementText?.trim()
34
+ if (normalizedText) textContentArray.push(normalizedText)
35
+ })
20
36
  })
21
37
 
22
38
  return textContentArray.join(' ')
23
- })
39
+ }, selector)
24
40
 
25
41
  return allText
26
42
  }
@@ -1,31 +1,41 @@
1
1
  import { Page, WaitForSelectorOptions } from 'puppeteer'
2
- import applyDefaultWaitForOpts from '../helpers/applyDefaultWaitForOpts.js'
2
+ import evaluateWithRetryAndTimeout from '../internal/evaluateWithRetryAndTimeout.js'
3
+ import getAllTextContentFromPage from '../internal/getAllTextContentFromPage.js'
4
+ import requirePuppeteerPage from '../internal/requirePuppeteerPage.js'
5
+
6
+ export type TextContentMatcherExpected = string | RegExp
7
+ export type TextContentMatcherOpts = { selector?: string } & WaitForSelectorOptions
3
8
 
4
9
  export default async function toMatchTextContent(
5
- page: Page,
6
- text: string,
7
- opts: { selector?: string } & WaitForSelectorOptions = {}
10
+ argumentPassedToExpect: Page,
11
+ expected: TextContentMatcherExpected,
12
+ opts: TextContentMatcherOpts = {}
8
13
  ) {
9
- try {
10
- await page.waitForSelector(
11
- `${opts.selector || 'body'}::-p-text(${text.replace(/"/g, '\\"')})`,
12
- applyDefaultWaitForOpts(opts)
13
- )
14
- return {
15
- pass: true,
16
- message: () => {
14
+ return await evaluateWithRetryAndTimeout(
15
+ argumentPassedToExpect,
16
+ async () => {
17
+ requirePuppeteerPage(argumentPassedToExpect)
18
+
19
+ const actual = await getAllTextContentFromPage(argumentPassedToExpect, opts.selector)
20
+ if (expected instanceof RegExp) expected.lastIndex = 0
21
+
22
+ return {
23
+ pass: typeof expected === 'string' ? actual.includes(expected) : expected.test(actual),
24
+ actual,
25
+ }
26
+ },
27
+ {
28
+ successText: () => {
17
29
  throw new Error('Cannot negate toMatchTextContent, use toNotMatchTextContent instead')
18
30
  },
19
- }
20
- } catch {
21
- return {
22
- pass: false,
23
- message: () => `
31
+ failureText: actual => `
24
32
  expected ${opts.selector || 'body'} with text:
25
- ${text}
33
+ ${expected.toString()}
26
34
 
27
- but no text was found within that selector
35
+ but no matching text was found within that selector:
36
+ ${actual}
28
37
  `,
38
+ timeout: opts.timeout,
29
39
  }
30
- }
40
+ )
31
41
  }
@@ -1,21 +1,24 @@
1
- import { Page, WaitForSelectorOptions } from 'puppeteer'
1
+ import { Page } from 'puppeteer'
2
2
  import evaluateWithRetryAndTimeout from '../internal/evaluateWithRetryAndTimeout.js'
3
3
  import getAllTextContentFromPage from '../internal/getAllTextContentFromPage.js'
4
4
  import requirePuppeteerPage from '../internal/requirePuppeteerPage.js'
5
+ import type { TextContentMatcherExpected, TextContentMatcherOpts } from './toMatchTextContent.js'
5
6
 
6
7
  export default async function toNotMatchTextContent(
7
8
  argumentPassedToExpect: Page,
8
- expected: string,
9
- opts: WaitForSelectorOptions = {}
9
+ expected: TextContentMatcherExpected,
10
+ opts: TextContentMatcherOpts = {}
10
11
  ) {
11
12
  return await evaluateWithRetryAndTimeout(
12
13
  argumentPassedToExpect,
13
14
  async () => {
14
15
  requirePuppeteerPage(argumentPassedToExpect)
15
16
 
16
- const actual = await getAllTextContentFromPage(argumentPassedToExpect)
17
+ const actual = await getAllTextContentFromPage(argumentPassedToExpect, opts.selector)
18
+ if (expected instanceof RegExp) expected.lastIndex = 0
19
+
17
20
  return {
18
- pass: !actual.includes(expected),
21
+ pass: typeof expected === 'string' ? !actual.includes(expected) : !expected.test(actual),
19
22
  actual,
20
23
  }
21
24
  },
@@ -23,7 +26,7 @@ export default async function toNotMatchTextContent(
23
26
  successText: () => {
24
27
  throw new Error('Cannot negate toNotMatchTextContent, use toMatchTextContent instead')
25
28
  },
26
- failureText: r => `Expected ${r} to not match text ${expected}, but it did`,
29
+ failureText: r => `Expected ${r} to not match text ${expected.toString()}, but it did`,
27
30
  timeout: opts.timeout,
28
31
  }
29
32
  )
package/src/index.ts CHANGED
@@ -2,6 +2,10 @@ import { Page, WaitForSelectorOptions } from 'puppeteer'
2
2
  import { CustomMatcherResult } from './feature/helpers/providePuppeteerViteMatchers.js'
3
3
  import { ExpectToEvaluateOpts } from './feature/internal/evaluateWithRetryAndTimeout.js'
4
4
  import { ToFillMatcherOpts } from './feature/matchers/toFill.js'
5
+ import type {
6
+ TextContentMatcherExpected,
7
+ TextContentMatcherOpts,
8
+ } from './feature/matchers/toMatchTextContent.js'
5
9
  export {
6
10
  RequestBody as OpenapiRequestBody,
7
11
  RequestQueryParameters as OpenapiRequestQuery,
@@ -64,10 +68,14 @@ interface PuppeteerAssertions {
64
68
  toEqualCalendarDate(expected: any): CustomMatcherResult
65
69
 
66
70
  // begin: fspec matchers
67
- // eslint-disable-next-line
68
- toMatchTextContent(expected: any, opts?: WaitForSelectorOptions): Promise<CustomMatcherResult>
69
- // eslint-disable-next-line
70
- toNotMatchTextContent(expected: any, opts?: WaitForSelectorOptions): Promise<CustomMatcherResult>
71
+ toMatchTextContent(
72
+ expected: TextContentMatcherExpected,
73
+ opts?: TextContentMatcherOpts
74
+ ): Promise<CustomMatcherResult>
75
+ toNotMatchTextContent(
76
+ expected: TextContentMatcherExpected,
77
+ opts?: TextContentMatcherOpts
78
+ ): Promise<CustomMatcherResult>
71
79
  // eslint-disable-next-line
72
80
  toHaveSelector(expected: any, opts?: WaitForSelectorOptions): Promise<CustomMatcherResult>
73
81
  // eslint-disable-next-line
@@ -1,15 +1,31 @@
1
- // eslint-disable-next-line
2
- const _server: any = undefined
1
+ // The booted spec server is cached for the lifetime of the worker process so it
2
+ // is created and booted exactly once, then reused by every spec — instead of
3
+ // re-booting a brand-new PsychicServer for each spec, which re-runs application
4
+ // initialization and churns database/websocket connections (a significant
5
+ // source of slow suites and flaky, connection-exhaustion-driven failures).
6
+ //
7
+ // The cache lives on `globalThis` rather than in a module-scoped variable
8
+ // because isolating test runners (e.g. Vitest with the default `isolate: true`)
9
+ // reset the module registry between spec files. A module-scoped cache would be
10
+ // discarded at every file boundary, forcing a fresh boot per file; `globalThis`
11
+ // persists for the whole worker process, so the server boots once per worker.
12
+ const CACHED_SPEC_SERVER_KEY = Symbol.for('@rvoh/psychic-spec-helpers:cachedSpecServer')
3
13
 
4
14
  // eslint-disable-next-line
5
15
  export default async function createPsychicServer(PsychicServer: any) {
6
16
  // eslint-disable-next-line
7
- if (_server) return _server!
17
+ const store = globalThis as Record<symbol, any>
18
+
19
+ // eslint-disable-next-line
20
+ if (store[CACHED_SPEC_SERVER_KEY]) return store[CACHED_SPEC_SERVER_KEY]
8
21
 
9
22
  // eslint-disable-next-line
10
23
  const server = new PsychicServer()
11
24
  // eslint-disable-next-line
12
25
  await server.boot()
26
+
27
+ // eslint-disable-next-line
28
+ store[CACHED_SPEC_SERVER_KEY] = server
13
29
  // eslint-disable-next-line
14
30
  return server
15
31
  }