@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.
- package/dist/esm/src/feature/helpers/providePuppeteerViteMatchers.js +2 -2
- package/dist/esm/src/feature/internal/getAllTextContentFromPage.js +27 -11
- package/dist/esm/src/feature/matchers/toMatchTextContent.js +21 -17
- package/dist/esm/src/feature/matchers/toNotMatchTextContent.js +5 -3
- package/dist/esm/src/unit/createPsychicServer.js +18 -4
- package/dist/types/src/feature/internal/getAllTextContentFromPage.d.ts +1 -1
- package/dist/types/src/feature/matchers/toMatchTextContent.d.ts +5 -3
- package/dist/types/src/feature/matchers/toNotMatchTextContent.d.ts +3 -2
- package/dist/types/src/index.d.ts +3 -2
- package/package.json +2 -2
- package/src/feature/helpers/providePuppeteerViteMatchers.ts +12 -4
- package/src/feature/internal/getAllTextContentFromPage.ts +27 -11
- package/src/feature/matchers/toMatchTextContent.ts +30 -20
- package/src/feature/matchers/toNotMatchTextContent.ts +9 -6
- package/src/index.ts +12 -4
- package/src/unit/createPsychicServer.ts +19 -3
|
@@ -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-
|
|
7
|
-
const
|
|
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
|
-
|
|
10
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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:
|
|
7
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
${
|
|
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
|
-
//
|
|
2
|
-
|
|
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
|
-
|
|
7
|
-
|
|
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
|
|
2
|
+
export type TextContentMatcherExpected = string | RegExp;
|
|
3
|
+
export type TextContentMatcherOpts = {
|
|
3
4
|
selector?: string;
|
|
4
|
-
} & WaitForSelectorOptions
|
|
5
|
-
|
|
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
|
|
2
|
-
|
|
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:
|
|
43
|
-
toNotMatchTextContent(expected:
|
|
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.
|
|
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@
|
|
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:
|
|
39
|
-
opts?:
|
|
42
|
+
text: TextContentMatcherExpected,
|
|
43
|
+
opts?: TextContentMatcherOpts
|
|
40
44
|
) {
|
|
41
45
|
return await toMatchTextContent(page, text, opts)
|
|
42
46
|
},
|
|
43
47
|
|
|
44
|
-
async toNotMatchTextContent(
|
|
45
|
-
|
|
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-
|
|
9
|
-
const
|
|
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
|
-
;(
|
|
15
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
opts:
|
|
10
|
+
argumentPassedToExpect: Page,
|
|
11
|
+
expected: TextContentMatcherExpected,
|
|
12
|
+
opts: TextContentMatcherOpts = {}
|
|
8
13
|
) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
${
|
|
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
|
|
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:
|
|
9
|
-
opts:
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
//
|
|
2
|
-
|
|
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
|
-
|
|
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
|
}
|