@rvoh/psychic-spec-helpers 0.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.
Files changed (109) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +68 -0
  3. package/dist/esm/src/feature/helpers/launchBrowser.js +9 -0
  4. package/dist/esm/src/feature/helpers/launchPage.js +5 -0
  5. package/dist/esm/src/feature/helpers/launchViteServer.js +89 -0
  6. package/dist/esm/src/feature/helpers/providePuppeteerViteMatchers.js +73 -0
  7. package/dist/esm/src/feature/helpers/visit.js +6 -0
  8. package/dist/esm/src/feature/internal/evaluateWithRetryAndTimeout.js +29 -0
  9. package/dist/esm/src/feature/internal/evaluationFailure.js +6 -0
  10. package/dist/esm/src/feature/internal/evaluationSuccess.js +6 -0
  11. package/dist/esm/src/feature/internal/getAllTextContentFromPage.js +19 -0
  12. package/dist/esm/src/feature/internal/isPuppeteerPage.js +3 -0
  13. package/dist/esm/src/feature/internal/matchFailure.js +6 -0
  14. package/dist/esm/src/feature/internal/matchSuccess.js +6 -0
  15. package/dist/esm/src/feature/internal/requirePuppeteerPage.js +6 -0
  16. package/dist/esm/src/feature/matchers/toCheck.js +23 -0
  17. package/dist/esm/src/feature/matchers/toClick.js +25 -0
  18. package/dist/esm/src/feature/matchers/toClickButton.js +25 -0
  19. package/dist/esm/src/feature/matchers/toClickLink.js +25 -0
  20. package/dist/esm/src/feature/matchers/toClickSelector.js +25 -0
  21. package/dist/esm/src/feature/matchers/toFill.js +21 -0
  22. package/dist/esm/src/feature/matchers/toHaveChecked.js +19 -0
  23. package/dist/esm/src/feature/matchers/toHaveLink.js +25 -0
  24. package/dist/esm/src/feature/matchers/toHavePath.js +15 -0
  25. package/dist/esm/src/feature/matchers/toHaveSelector.js +14 -0
  26. package/dist/esm/src/feature/matchers/toHaveUnchecked.js +19 -0
  27. package/dist/esm/src/feature/matchers/toHaveUrl.js +14 -0
  28. package/dist/esm/src/feature/matchers/toMatchTextContent.js +16 -0
  29. package/dist/esm/src/feature/matchers/toNotHaveSelector.js +14 -0
  30. package/dist/esm/src/feature/matchers/toNotMatchTextContent.js +16 -0
  31. package/dist/esm/src/feature/matchers/toUncheck.js +23 -0
  32. package/dist/esm/src/index.js +12 -0
  33. package/dist/esm/src/shared/sleep.js +7 -0
  34. package/dist/esm/src/unit/SpecRequest.js +89 -0
  35. package/dist/esm/src/unit/SpecSession.js +48 -0
  36. package/dist/esm/src/unit/createPsychicServer.js +8 -0
  37. package/dist/esm/src/unit/supersession.js +72 -0
  38. package/dist/types/src/feature/helpers/launchBrowser.d.ts +2 -0
  39. package/dist/types/src/feature/helpers/launchPage.d.ts +2 -0
  40. package/dist/types/src/feature/helpers/launchViteServer.d.ts +6 -0
  41. package/dist/types/src/feature/helpers/providePuppeteerViteMatchers.d.ts +5 -0
  42. package/dist/types/src/feature/helpers/visit.d.ts +5 -0
  43. package/dist/types/src/feature/internal/evaluateWithRetryAndTimeout.d.ts +14 -0
  44. package/dist/types/src/feature/internal/evaluationFailure.d.ts +4 -0
  45. package/dist/types/src/feature/internal/evaluationSuccess.d.ts +4 -0
  46. package/dist/types/src/feature/internal/getAllTextContentFromPage.d.ts +2 -0
  47. package/dist/types/src/feature/internal/isPuppeteerPage.d.ts +1 -0
  48. package/dist/types/src/feature/internal/matchFailure.d.ts +4 -0
  49. package/dist/types/src/feature/internal/matchSuccess.d.ts +4 -0
  50. package/dist/types/src/feature/internal/requirePuppeteerPage.d.ts +1 -0
  51. package/dist/types/src/feature/matchers/toCheck.d.ts +5 -0
  52. package/dist/types/src/feature/matchers/toClick.d.ts +5 -0
  53. package/dist/types/src/feature/matchers/toClickButton.d.ts +5 -0
  54. package/dist/types/src/feature/matchers/toClickLink.d.ts +5 -0
  55. package/dist/types/src/feature/matchers/toClickSelector.d.ts +5 -0
  56. package/dist/types/src/feature/matchers/toFill.d.ts +5 -0
  57. package/dist/types/src/feature/matchers/toHaveChecked.d.ts +5 -0
  58. package/dist/types/src/feature/matchers/toHaveLink.d.ts +5 -0
  59. package/dist/types/src/feature/matchers/toHavePath.d.ts +5 -0
  60. package/dist/types/src/feature/matchers/toHaveSelector.d.ts +5 -0
  61. package/dist/types/src/feature/matchers/toHaveUnchecked.d.ts +5 -0
  62. package/dist/types/src/feature/matchers/toHaveUrl.d.ts +5 -0
  63. package/dist/types/src/feature/matchers/toMatchTextContent.d.ts +5 -0
  64. package/dist/types/src/feature/matchers/toNotHaveSelector.d.ts +5 -0
  65. package/dist/types/src/feature/matchers/toNotMatchTextContent.d.ts +5 -0
  66. package/dist/types/src/feature/matchers/toUncheck.d.ts +5 -0
  67. package/dist/types/src/index.d.ts +45 -0
  68. package/dist/types/src/shared/sleep.d.ts +1 -0
  69. package/dist/types/src/unit/SpecRequest.d.ts +34 -0
  70. package/dist/types/src/unit/SpecSession.d.ts +29 -0
  71. package/dist/types/src/unit/createPsychicServer.d.ts +1 -0
  72. package/dist/types/src/unit/supersession.d.ts +6 -0
  73. package/package.json +47 -0
  74. package/src/feature/helpers/launchBrowser.ts +10 -0
  75. package/src/feature/helpers/launchPage.ts +7 -0
  76. package/src/feature/helpers/launchViteServer.ts +104 -0
  77. package/src/feature/helpers/providePuppeteerViteMatchers.ts +102 -0
  78. package/src/feature/helpers/visit.ts +11 -0
  79. package/src/feature/internal/evaluateWithRetryAndTimeout.ts +49 -0
  80. package/src/feature/internal/evaluationFailure.ts +6 -0
  81. package/src/feature/internal/evaluationSuccess.ts +6 -0
  82. package/src/feature/internal/getAllTextContentFromPage.ts +26 -0
  83. package/src/feature/internal/isPuppeteerPage.ts +3 -0
  84. package/src/feature/internal/matchFailure.ts +6 -0
  85. package/src/feature/internal/matchSuccess.ts +6 -0
  86. package/src/feature/internal/requirePuppeteerPage.ts +7 -0
  87. package/src/feature/matchers/toCheck.ts +34 -0
  88. package/src/feature/matchers/toClick.ts +30 -0
  89. package/src/feature/matchers/toClickButton.ts +30 -0
  90. package/src/feature/matchers/toClickLink.ts +30 -0
  91. package/src/feature/matchers/toClickSelector.ts +30 -0
  92. package/src/feature/matchers/toFill.ts +28 -0
  93. package/src/feature/matchers/toHaveChecked.ts +26 -0
  94. package/src/feature/matchers/toHaveLink.ts +30 -0
  95. package/src/feature/matchers/toHavePath.ts +23 -0
  96. package/src/feature/matchers/toHaveSelector.ts +21 -0
  97. package/src/feature/matchers/toHaveUnchecked.ts +26 -0
  98. package/src/feature/matchers/toHaveUrl.ts +21 -0
  99. package/src/feature/matchers/toMatchTextContent.ts +23 -0
  100. package/src/feature/matchers/toNotHaveSelector.ts +21 -0
  101. package/src/feature/matchers/toNotMatchTextContent.ts +26 -0
  102. package/src/feature/matchers/toUncheck.ts +35 -0
  103. package/src/index.ts +56 -0
  104. package/src/shared/sleep.ts +7 -0
  105. package/src/unit/SpecRequest.ts +160 -0
  106. package/src/unit/SpecSession.ts +103 -0
  107. package/src/unit/createPsychicServer.ts +8 -0
  108. package/src/unit/supersession.ts +100 -0
  109. package/tsconfig.json +7 -0
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "type": "module",
3
+ "name": "@rvoh/psychic-spec-helpers",
4
+ "version": "0.2.0",
5
+ "description": "psychic framework spec helpers",
6
+ "main": "./dist/cjs/src/index.js",
7
+ "module": "./dist/esm/src/index.js",
8
+ "types": "./dist/types/src/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "require": "./dist/cjs/src/index.js",
12
+ "import": "./dist/esm/src/index.js",
13
+ "types": "./dist/types/src/index.d.ts"
14
+ }
15
+ },
16
+ "files": [
17
+ "tsconfig.json",
18
+ "src/**/*",
19
+ "dist/**/*"
20
+ ],
21
+ "scripts": {
22
+ "build": "echo \"building psychic-spec-helpers...\" && rm -rf dist && npx tsc -p ./tsconfig.json",
23
+ "prepack": "yarn build"
24
+ },
25
+ "packageManager": "yarn@4.4.1",
26
+ "peerDependencies": {
27
+ "@types/node": "*",
28
+ "@types/supertest": "*",
29
+ "puppeteer": "*",
30
+ "supertest": "*",
31
+ "ts-node": "*",
32
+ "typescript": "*"
33
+ },
34
+ "devDependencies": {
35
+ "@types/cookiejar": "^2",
36
+ "@types/node": "^22.5.1",
37
+ "@types/supertest": "^6.0.2",
38
+ "puppeteer": "^24.4.0",
39
+ "supertest": "^7.0.0",
40
+ "ts-node": "^10.9.2",
41
+ "typescript": "^5.5.4",
42
+ "vitest": "^3.0.8"
43
+ },
44
+ "dependencies": {
45
+ "cookiejar": "^2.1.4"
46
+ }
47
+ }
@@ -0,0 +1,10 @@
1
+ import puppeteer, { LaunchOptions } from 'puppeteer'
2
+
3
+ export default async function launchBrowser(opts?: LaunchOptions) {
4
+ return await puppeteer.launch({
5
+ browser: 'firefox',
6
+ dumpio: process.env.DEBUG === '1',
7
+ headless: true,
8
+ ...opts,
9
+ })
10
+ }
@@ -0,0 +1,7 @@
1
+ import { LaunchOptions } from 'puppeteer'
2
+ import launchBrowser from './launchBrowser.js'
3
+
4
+ export default async function launchPage(opts?: LaunchOptions) {
5
+ const browser = await launchBrowser(opts)
6
+ return browser.newPage()
7
+ }
@@ -0,0 +1,104 @@
1
+ import { ChildProcessWithoutNullStreams, spawn } from 'child_process'
2
+ import { createServer } from 'net'
3
+ import sleep from '../../shared/sleep.js'
4
+
5
+ let serverProcess: ChildProcessWithoutNullStreams | undefined = undefined
6
+
7
+ export default async function launchViteServer({
8
+ port = 3000,
9
+ cmd = 'yarn client',
10
+ timeout = 5000,
11
+ }: { port?: number; cmd?: string; timeout?: number } = {}) {
12
+ if (serverProcess) return
13
+
14
+ if (process.env.DEBUG === '1') console.log('Starting server...')
15
+ const [_cmd, ...args] = cmd.split(' ')
16
+
17
+ serverProcess = spawn(_cmd, args, {
18
+ detached: true,
19
+ env: {
20
+ ...process.env,
21
+ BROWSER: 'none',
22
+ VITE_PSYCHIC_ENV: 'test',
23
+ },
24
+ })
25
+
26
+ await waitForPort(port, timeout)
27
+
28
+ serverProcess.stdout.on('data', data => {
29
+ if (process.env.DEBUG === '1') console.log(`Server output: ${data}`)
30
+ })
31
+
32
+ serverProcess.on('error', err => {
33
+ throw err
34
+ })
35
+
36
+ serverProcess.stdout.on('data', data => {
37
+ if (process.env.DEBUG === '1') console.log(`Server output: ${data}`)
38
+ })
39
+
40
+ serverProcess.stderr.on('data', data => {
41
+ if (process.env.DEBUG === '1') console.error(`Server error: ${data}`)
42
+ })
43
+
44
+ serverProcess.on('error', err => {
45
+ console.error(`Server process error: ${err as unknown as string}`)
46
+ })
47
+
48
+ serverProcess.on('close', code => {
49
+ if (process.env.DEBUG === '1') console.log(`Server process exited with code ${code}`)
50
+ })
51
+ }
52
+
53
+ export function stopViteServer() {
54
+ if (serverProcess?.pid) {
55
+ if (process.env.DEBUG === '1') console.log('Stopping server...')
56
+ // serverProcess.kill('SIGINT')
57
+ process.kill(-serverProcess.pid, 'SIGKILL')
58
+ serverProcess = undefined
59
+
60
+ if (process.env.DEBUG === '1') console.log('server stopped')
61
+ }
62
+ }
63
+
64
+ async function isPortAvailable(port: number): Promise<boolean> {
65
+ return new Promise(resolve => {
66
+ const server = createServer()
67
+ .once('error', err => {
68
+ if ((err as any).code === 'EADDRINUSE') {
69
+ resolve(false)
70
+ } else {
71
+ resolve(true)
72
+ }
73
+ })
74
+ .once('listening', () => {
75
+ server.close()
76
+ resolve(true)
77
+ })
78
+ .listen(port, '127.0.0.1')
79
+ })
80
+ }
81
+
82
+ async function waitForPort(port: number, timeout: number = 5000) {
83
+ if (await isPortAvailable(port)) {
84
+ return true
85
+ }
86
+
87
+ const startTime = Date.now()
88
+
89
+ async function recursiveWaitForPort() {
90
+ if (await isPortAvailable(port)) {
91
+ return true
92
+ }
93
+
94
+ if (Date.now() > startTime + timeout) {
95
+ stopViteServer()
96
+ throw new Error('waited too long for port: ' + port)
97
+ }
98
+
99
+ await sleep(50)
100
+ return await recursiveWaitForPort()
101
+ }
102
+
103
+ return await recursiveWaitForPort()
104
+ }
@@ -0,0 +1,102 @@
1
+ import { Page } from 'puppeteer'
2
+ import evaluateWithRetryAndTimeout, {
3
+ ExpectToEvaluateOpts,
4
+ ExpectToEvaluateReturnType,
5
+ } from '../internal/evaluateWithRetryAndTimeout.js'
6
+ import toHaveSelector from '../matchers/toHaveSelector.js'
7
+ import toMatchTextContent from '../matchers/toMatchTextContent.js'
8
+ import toNotHaveSelector from '../matchers/toNotHaveSelector.js'
9
+ import toNotMatchTextContent from '../matchers/toNotMatchTextContent.js'
10
+ import toCheck from '../matchers/toCheck.js'
11
+ import toUncheck from '../matchers/toUncheck.js'
12
+ import toClick from '../matchers/toClick.js'
13
+ import toClickLink from '../matchers/toClickLink.js'
14
+ import toClickButton from '../matchers/toClickButton.js'
15
+ import toClickSelector from '../matchers/toClickSelector.js'
16
+ import toHavePath from '../matchers/toHavePath.js'
17
+ import toHaveUrl from '../matchers/toHaveUrl.js'
18
+ import toHaveLink from '../matchers/toHaveLink.js'
19
+ import toHaveChecked from '../matchers/toHaveChecked.js'
20
+ import toHaveUnchecked from '../matchers/toHaveUnchecked.js'
21
+ import toFill from '../matchers/toFill.js'
22
+
23
+ export default function providePuppeteerViteMatchers() {
24
+ ;((global as any).expect as any).extend({
25
+ async toMatchTextContent(page: Page, text: string) {
26
+ return await toMatchTextContent(page, text)
27
+ },
28
+
29
+ async toNotMatchTextContent(page: Page, text: string) {
30
+ return await toNotMatchTextContent(page, text)
31
+ },
32
+
33
+ async toHaveSelector(page: Page, cssSelector: string) {
34
+ return await toHaveSelector(page, cssSelector)
35
+ },
36
+
37
+ async toNotHaveSelector(page: Page, cssSelector: string) {
38
+ return await toNotHaveSelector(page, cssSelector)
39
+ },
40
+
41
+ async toCheck(page: Page, text: string) {
42
+ return await toCheck(page, text)
43
+ },
44
+
45
+ async toClick(page: Page, text: string) {
46
+ return await toClick(page, text)
47
+ },
48
+
49
+ async toClickLink(page: Page, text: string) {
50
+ return await toClickLink(page, text)
51
+ },
52
+
53
+ async toClickButton(page: Page, text: string) {
54
+ return await toClickButton(page, text)
55
+ },
56
+
57
+ async toClickSelector(page: Page, cssSelector: string) {
58
+ return await toClickSelector(page, cssSelector)
59
+ },
60
+
61
+ async toHavePath(page: Page, path: string) {
62
+ return await toHavePath(page, path)
63
+ },
64
+
65
+ async toHaveUrl(page: Page, url: string) {
66
+ return await toHaveUrl(page, url)
67
+ },
68
+
69
+ async toHaveChecked(page: Page, text: string) {
70
+ return await toHaveChecked(page, text)
71
+ },
72
+
73
+ async toHaveUnchecked(page: Page, checked: string) {
74
+ return await toHaveUnchecked(page, checked)
75
+ },
76
+
77
+ async toHaveLink(page: Page, text: string) {
78
+ return await toHaveLink(page, text)
79
+ },
80
+
81
+ async toFill(page: Page, cssSelector: string, text: string) {
82
+ return await toFill(page, cssSelector, text)
83
+ },
84
+
85
+ async toUncheck(page: Page, text: string) {
86
+ return await toUncheck(page, text)
87
+ },
88
+
89
+ async toEvaluate(
90
+ argumentPassedToExpect: Page,
91
+ evaluationFn: (a: any) => ExpectToEvaluateReturnType | Promise<ExpectToEvaluateReturnType>,
92
+ opts: ExpectToEvaluateOpts
93
+ ) {
94
+ return await evaluateWithRetryAndTimeout(argumentPassedToExpect, evaluationFn, opts)
95
+ },
96
+ })
97
+ }
98
+
99
+ export interface CustomMatcherResult {
100
+ pass: boolean
101
+ message: (actual?: unknown) => string
102
+ }
@@ -0,0 +1,11 @@
1
+ import { Page } from 'puppeteer'
2
+ import launchPage from './launchPage.js'
3
+
4
+ export default async function visit(
5
+ path: string,
6
+ { page, baseUrl = 'http://localhost:3000' }: { page?: Page; baseUrl?: string } = {}
7
+ ) {
8
+ page ||= await launchPage()
9
+ await page.goto(`${baseUrl}/${path.replace(/^\//, '')}`)
10
+ return page
11
+ }
@@ -0,0 +1,49 @@
1
+ import sleep from '../../shared/sleep.js'
2
+
3
+ export default async function evaluateWithRetryAndTimeout(
4
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
5
+ argumentPassedToExpect: any,
6
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
+ evaluationFn: (a: any) => ExpectToEvaluateReturnType | Promise<ExpectToEvaluateReturnType>,
8
+ opts: ExpectToEvaluateOpts
9
+ ) {
10
+ const timeout = opts.timeout || 4000
11
+ const interval = opts.interval || 10
12
+ const startTime = Date.now()
13
+
14
+ async function intervalCb() {
15
+ const res = await evaluationFn(argumentPassedToExpect)
16
+ const expired = Date.now() >= startTime + timeout
17
+
18
+ if (res.pass) {
19
+ return {
20
+ message: () => (opts.successText ? opts.successText(res.actual) : 'Success'),
21
+ pass: true,
22
+ }
23
+ }
24
+
25
+ if (expired) {
26
+ return {
27
+ message: () => opts.failureText(res.actual),
28
+ pass: false,
29
+ }
30
+ }
31
+
32
+ await sleep(interval)
33
+ return await intervalCb()
34
+ }
35
+
36
+ return await intervalCb()
37
+ }
38
+
39
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
40
+ export type ExpectToEvaluateReturnType = { pass: boolean; actual: any }
41
+
42
+ export interface ExpectToEvaluateOpts {
43
+ timeout?: number
44
+ interval?: number
45
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
46
+ successText?: (received: any) => string
47
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
48
+ failureText: (received: any) => string
49
+ }
@@ -0,0 +1,6 @@
1
+ export default function evaluationFailure(actual: any) {
2
+ return {
3
+ pass: false,
4
+ actual,
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ export default function evaluationSuccess(actual: any) {
2
+ return {
3
+ pass: true,
4
+ actual,
5
+ }
6
+ }
@@ -0,0 +1,26 @@
1
+ import { Page } from 'puppeteer'
2
+
3
+ export default async function getAllTextContentFromPage(page: Page) {
4
+ // Evaluate and extract all text content on the page
5
+ const allText = await page.evaluate(() => {
6
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
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('*')
10
+
11
+ const textContentArray: string[] = []
12
+
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
+ }
20
+ })
21
+
22
+ return textContentArray.join(' ')
23
+ })
24
+
25
+ return allText
26
+ }
@@ -0,0 +1,3 @@
1
+ export default function isPuppeteerPage(page: any): boolean {
2
+ return typeof page === 'object' && page !== null && !!page.mouse
3
+ }
@@ -0,0 +1,6 @@
1
+ export default function matchFailure(message: string | ((expected: any) => string)) {
2
+ return {
3
+ pass: false,
4
+ message: (expected: any) => (typeof message === 'string' ? message : message(expected)),
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ export default function matchSuccess(message: string | ((expected: any) => string)) {
2
+ return {
3
+ pass: true,
4
+ message: (expected: any) => (typeof message === 'string' ? message : message(expected)),
5
+ }
6
+ }
@@ -0,0 +1,7 @@
1
+ import isPuppeteerPage from './isPuppeteerPage.js'
2
+
3
+ export default function requirePuppeteerPage(argumentPassedToExpect: any) {
4
+ if (!isPuppeteerPage(argumentPassedToExpect)) {
5
+ throw new Error('Must pass a puppeteer page to expect when calling toMatchTextContent')
6
+ }
7
+ }
@@ -0,0 +1,34 @@
1
+ import { Page } from 'puppeteer'
2
+ import evaluateWithRetryAndTimeout from '../internal/evaluateWithRetryAndTimeout.js'
3
+ import evaluationFailure from '../internal/evaluationFailure.js'
4
+ import requirePuppeteerPage from '../internal/requirePuppeteerPage.js'
5
+
6
+ export default async function toCheck(page: Page, expectedText: string) {
7
+ return await evaluateWithRetryAndTimeout(
8
+ page,
9
+ async () => {
10
+ requirePuppeteerPage(page)
11
+
12
+ const checkbox = await page.$(`input[type="checkbox"][value="${expectedText}"]`)
13
+ if (!checkbox) return evaluationFailure(`A checkbox was not found with "${expectedText}"`)
14
+
15
+ const isChecked = await page.evaluate(checkbox => checkbox.checked, checkbox)
16
+ if (isChecked)
17
+ return evaluationFailure(
18
+ `A checkbox was found with "${expectedText}", but it is already checked`
19
+ )
20
+
21
+ await checkbox.click()
22
+ const isCheckedNow = await page.evaluate(checkbox => checkbox.checked, checkbox)
23
+
24
+ return {
25
+ pass: isCheckedNow,
26
+ actual: expectedText,
27
+ }
28
+ },
29
+ {
30
+ successText: r => `Expected page to have checkable checkbox with text: "${expectedText}"`,
31
+ failureText: r => `Expected page not to have checkable checkbox with text: "${expectedText}"`,
32
+ }
33
+ )
34
+ }
@@ -0,0 +1,30 @@
1
+ import { Page, TimeoutError } from 'puppeteer'
2
+ import evaluateWithRetryAndTimeout from '../internal/evaluateWithRetryAndTimeout.js'
3
+ import evaluationFailure from '../internal/evaluationFailure.js'
4
+ import requirePuppeteerPage from '../internal/requirePuppeteerPage.js'
5
+
6
+ export default async function toClick(page: Page, expectedText: string) {
7
+ return await evaluateWithRetryAndTimeout(
8
+ page,
9
+ async () => {
10
+ requirePuppeteerPage(page)
11
+
12
+ try {
13
+ const el = await page.locator(`::-p-text(${expectedText})`).setTimeout(5000).wait()
14
+ await el.click()
15
+ } catch (err) {
16
+ if (err instanceof TimeoutError) return evaluationFailure(expectedText)
17
+ throw err
18
+ }
19
+
20
+ return {
21
+ pass: true,
22
+ actual: expectedText,
23
+ }
24
+ },
25
+ {
26
+ successText: () => `Expected page to have clickable element with text: "${expectedText}"`,
27
+ failureText: () => `Expected page not to have clickable element with text: "${expectedText}"`,
28
+ }
29
+ )
30
+ }
@@ -0,0 +1,30 @@
1
+ import { Page, TimeoutError } from 'puppeteer'
2
+ import evaluateWithRetryAndTimeout from '../internal/evaluateWithRetryAndTimeout.js'
3
+ import evaluationFailure from '../internal/evaluationFailure.js'
4
+ import requirePuppeteerPage from '../internal/requirePuppeteerPage.js'
5
+
6
+ export default async function toClickButton(page: Page, expectedText: string) {
7
+ return await evaluateWithRetryAndTimeout(
8
+ page,
9
+ async () => {
10
+ requirePuppeteerPage(page)
11
+
12
+ try {
13
+ const el = await page.locator(`button ::-p-text(${expectedText})`).setTimeout(10).wait()
14
+ await el.click()
15
+ } catch (err) {
16
+ if (err instanceof TimeoutError) return evaluationFailure(expectedText)
17
+ throw err
18
+ }
19
+
20
+ return {
21
+ pass: true,
22
+ actual: expectedText,
23
+ }
24
+ },
25
+ {
26
+ successText: () => `Expected page to have clickable button with text: "${expectedText}"`,
27
+ failureText: () => `Expected page not to have clickable button with text: "${expectedText}"`,
28
+ }
29
+ )
30
+ }
@@ -0,0 +1,30 @@
1
+ import { Page, TimeoutError } from 'puppeteer'
2
+ import evaluateWithRetryAndTimeout from '../internal/evaluateWithRetryAndTimeout.js'
3
+ import evaluationFailure from '../internal/evaluationFailure.js'
4
+ import requirePuppeteerPage from '../internal/requirePuppeteerPage.js'
5
+
6
+ export default async function toClickLink(page: Page, expectedText: string) {
7
+ return await evaluateWithRetryAndTimeout(
8
+ page,
9
+ async () => {
10
+ requirePuppeteerPage(page)
11
+
12
+ try {
13
+ const el = await page.locator(`a ::-p-text(${expectedText})`).setTimeout(5000).wait()
14
+ await el.click()
15
+ } catch (err) {
16
+ if (err instanceof TimeoutError) return evaluationFailure(expectedText)
17
+ throw err
18
+ }
19
+
20
+ return {
21
+ pass: true,
22
+ actual: expectedText,
23
+ }
24
+ },
25
+ {
26
+ successText: () => `Expected page to have clickable link with text: "${expectedText}"`,
27
+ failureText: () => `Expected page not to have clickable link with text: "${expectedText}"`,
28
+ }
29
+ )
30
+ }
@@ -0,0 +1,30 @@
1
+ import { Page, TimeoutError } from 'puppeteer'
2
+ import evaluateWithRetryAndTimeout from '../internal/evaluateWithRetryAndTimeout.js'
3
+ import evaluationFailure from '../internal/evaluationFailure.js'
4
+ import requirePuppeteerPage from '../internal/requirePuppeteerPage.js'
5
+
6
+ export default async function toClickSelector(page: Page, cssSelector: string) {
7
+ return await evaluateWithRetryAndTimeout(
8
+ page,
9
+ async () => {
10
+ requirePuppeteerPage(page)
11
+
12
+ try {
13
+ const el = await page.locator(cssSelector).setTimeout(10).wait()
14
+ await el.click()
15
+ } catch (err) {
16
+ if (err instanceof TimeoutError) return evaluationFailure(cssSelector)
17
+ throw err
18
+ }
19
+
20
+ return {
21
+ pass: true,
22
+ actual: cssSelector,
23
+ }
24
+ },
25
+ {
26
+ successText: () => `Expected page to have clickable selector with text: "${cssSelector}"`,
27
+ failureText: () => `Expected page not to have clickable selector with text: "${cssSelector}"`,
28
+ }
29
+ )
30
+ }
@@ -0,0 +1,28 @@
1
+ import { Page } from 'puppeteer'
2
+ import evaluateWithRetryAndTimeout from '../internal/evaluateWithRetryAndTimeout.js'
3
+ import evaluationFailure from '../internal/evaluationFailure.js'
4
+ import requirePuppeteerPage from '../internal/requirePuppeteerPage.js'
5
+
6
+ export default async function toFill(page: Page, cssSelector: string, text: string) {
7
+ return await evaluateWithRetryAndTimeout(
8
+ page,
9
+ async () => {
10
+ requirePuppeteerPage(page)
11
+
12
+ try {
13
+ await page.type(cssSelector, text)
14
+ } catch {
15
+ return evaluationFailure(text)
16
+ }
17
+
18
+ return {
19
+ pass: true,
20
+ actual: text,
21
+ }
22
+ },
23
+ {
24
+ successText: () => `Expected page to have clickable link with text: "${text}"`,
25
+ failureText: () => `Expected page not to have clickable link with text: "${text}"`,
26
+ }
27
+ )
28
+ }
@@ -0,0 +1,26 @@
1
+ import { Page } from 'puppeteer'
2
+ import evaluateWithRetryAndTimeout from '../internal/evaluateWithRetryAndTimeout.js'
3
+ import evaluationFailure from '../internal/evaluationFailure.js'
4
+ import requirePuppeteerPage from '../internal/requirePuppeteerPage.js'
5
+
6
+ export default async function toHaveChecked(page: Page, expectedText: string) {
7
+ return await evaluateWithRetryAndTimeout(
8
+ page,
9
+ async () => {
10
+ requirePuppeteerPage(page)
11
+
12
+ const checkbox = await page.$(`input[type="checkbox"][value="${expectedText}"]`)
13
+ if (!checkbox) return evaluationFailure(`A checkbox was not found with "${expectedText}"`)
14
+
15
+ const isChecked = await page.evaluate(checkbox => checkbox.checked, checkbox)
16
+ return {
17
+ pass: isChecked,
18
+ actual: expectedText,
19
+ }
20
+ },
21
+ {
22
+ successText: r => `Expected page to have checked checkbox with text: "${expectedText}"`,
23
+ failureText: r => `Expected page not to have checked checkbox with text: "${expectedText}"`,
24
+ }
25
+ )
26
+ }
@@ -0,0 +1,30 @@
1
+ import { Page, TimeoutError } from 'puppeteer'
2
+ import evaluateWithRetryAndTimeout from '../internal/evaluateWithRetryAndTimeout.js'
3
+ import evaluationFailure from '../internal/evaluationFailure.js'
4
+ import requirePuppeteerPage from '../internal/requirePuppeteerPage.js'
5
+
6
+ export default async function toHaveLink(page: Page, expectedText: string) {
7
+ return await evaluateWithRetryAndTimeout(
8
+ page,
9
+ async () => {
10
+ requirePuppeteerPage(page)
11
+
12
+ let el: any = undefined
13
+ try {
14
+ el = await page.locator(`a ::-p-text(${expectedText})`).setTimeout(10).wait()
15
+ } catch (err) {
16
+ if (err instanceof TimeoutError) return evaluationFailure(expectedText)
17
+ throw err
18
+ }
19
+
20
+ return {
21
+ pass: !!el,
22
+ actual: expectedText,
23
+ }
24
+ },
25
+ {
26
+ successText: () => `Expected page to have clickable link with text: "${expectedText}"`,
27
+ failureText: () => `Expected page not to have clickable link with text: "${expectedText}"`,
28
+ }
29
+ )
30
+ }