@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
@@ -0,0 +1,23 @@
1
+ import { Page } from 'puppeteer'
2
+ import evaluateWithRetryAndTimeout from '../internal/evaluateWithRetryAndTimeout.js'
3
+ import requirePuppeteerPage from '../internal/requirePuppeteerPage.js'
4
+
5
+ export default async function toHavePath(page: Page, expectedPath: string) {
6
+ requirePuppeteerPage(page)
7
+
8
+ return await evaluateWithRetryAndTimeout(
9
+ page,
10
+ async () => {
11
+ const pathname = new URL(page.url()).pathname
12
+
13
+ return {
14
+ pass: pathname === expectedPath,
15
+ actual: expectedPath,
16
+ }
17
+ },
18
+ {
19
+ successText: () => `Expected page to have path: "${expectedPath}"`,
20
+ failureText: () => `Expected page not to have path: "${expectedPath}"`,
21
+ }
22
+ )
23
+ }
@@ -0,0 +1,21 @@
1
+ import { Page } from 'puppeteer'
2
+ import evaluateWithRetryAndTimeout from '../internal/evaluateWithRetryAndTimeout.js'
3
+ import requirePuppeteerPage from '../internal/requirePuppeteerPage.js'
4
+
5
+ export default async function toHaveSelector(page: Page, expectedSelector: string) {
6
+ return await evaluateWithRetryAndTimeout(
7
+ page,
8
+ async () => {
9
+ requirePuppeteerPage(page)
10
+
11
+ return {
12
+ pass: !!(await page.$(expectedSelector)),
13
+ actual: expectedSelector,
14
+ }
15
+ },
16
+ {
17
+ successText: r => `Expected ${r} to have selector: ${expectedSelector}`,
18
+ failureText: r => `Expected ${r} not to have selector: ${expectedSelector}`,
19
+ }
20
+ )
21
+ }
@@ -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 toHaveUnchecked(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 unchecked checkbox with text: "${expectedText}"`,
23
+ failureText: r => `Expected page not to have unchecked checkbox with text: "${expectedText}"`,
24
+ }
25
+ )
26
+ }
@@ -0,0 +1,21 @@
1
+ import { Page } from 'puppeteer'
2
+ import evaluateWithRetryAndTimeout from '../internal/evaluateWithRetryAndTimeout.js'
3
+ import requirePuppeteerPage from '../internal/requirePuppeteerPage.js'
4
+
5
+ export default async function toHaveUrl(page: Page, expectedUrl: string) {
6
+ requirePuppeteerPage(page)
7
+
8
+ return await evaluateWithRetryAndTimeout(
9
+ page,
10
+ async () => {
11
+ return {
12
+ pass: page.url() === expectedUrl,
13
+ actual: expectedUrl,
14
+ }
15
+ },
16
+ {
17
+ successText: () => `Expected page to have path: "${expectedUrl}"`,
18
+ failureText: () => `Expected page not to have path: "${expectedUrl}"`,
19
+ }
20
+ )
21
+ }
@@ -0,0 +1,23 @@
1
+ import { Page } from 'puppeteer'
2
+ import evaluateWithRetryAndTimeout from '../internal/evaluateWithRetryAndTimeout.js'
3
+ import getAllTextContentFromPage from '../internal/getAllTextContentFromPage.js'
4
+ import requirePuppeteerPage from '../internal/requirePuppeteerPage.js'
5
+
6
+ export default async function toMatchTextContent(argumentPassedToExpect: Page, expected: string) {
7
+ return await evaluateWithRetryAndTimeout(
8
+ argumentPassedToExpect,
9
+ async () => {
10
+ requirePuppeteerPage(argumentPassedToExpect)
11
+
12
+ const actual = await getAllTextContentFromPage(argumentPassedToExpect)
13
+ return {
14
+ pass: actual.includes(expected),
15
+ actual,
16
+ }
17
+ },
18
+ {
19
+ successText: r => `Expected ${r} to match text ${expected}`,
20
+ failureText: r => `Expected ${r} not to match text ${expected}`,
21
+ }
22
+ )
23
+ }
@@ -0,0 +1,21 @@
1
+ import { Page } from 'puppeteer'
2
+ import evaluateWithRetryAndTimeout from '../internal/evaluateWithRetryAndTimeout.js'
3
+ import requirePuppeteerPage from '../internal/requirePuppeteerPage.js'
4
+
5
+ export default async function toNotHaveSelector(page: Page, expectedSelector: string) {
6
+ return await evaluateWithRetryAndTimeout(
7
+ page,
8
+ async () => {
9
+ requirePuppeteerPage(page)
10
+
11
+ return {
12
+ pass: !(await page.$(expectedSelector)),
13
+ actual: expectedSelector,
14
+ }
15
+ },
16
+ {
17
+ successText: r => `Expected ${r} not to have selector: ${expectedSelector}`,
18
+ failureText: r => `Expected ${r} to have selector: ${expectedSelector}`,
19
+ }
20
+ )
21
+ }
@@ -0,0 +1,26 @@
1
+ import { Page } from 'puppeteer'
2
+ import evaluateWithRetryAndTimeout from '../internal/evaluateWithRetryAndTimeout.js'
3
+ import getAllTextContentFromPage from '../internal/getAllTextContentFromPage.js'
4
+ import requirePuppeteerPage from '../internal/requirePuppeteerPage.js'
5
+
6
+ export default async function toNotMatchTextContent(
7
+ argumentPassedToExpect: Page,
8
+ expected: string
9
+ ) {
10
+ return await evaluateWithRetryAndTimeout(
11
+ argumentPassedToExpect,
12
+ async () => {
13
+ requirePuppeteerPage(argumentPassedToExpect)
14
+
15
+ const actual = await getAllTextContentFromPage(argumentPassedToExpect)
16
+ return {
17
+ pass: !actual.includes(expected),
18
+ actual,
19
+ }
20
+ },
21
+ {
22
+ successText: r => `Expected ${r} not to match text ${expected}`,
23
+ failureText: r => `Expected ${r} to match text ${expected}`,
24
+ }
25
+ )
26
+ }
@@ -0,0 +1,35 @@
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 toUncheck(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 unchecked`
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 uncheckable checkbox with text: "${expectedText}"`,
31
+ failureText: r =>
32
+ `Expected page not to have uncheckable checkbox with text: "${expectedText}"`,
33
+ }
34
+ )
35
+ }
package/src/index.ts ADDED
@@ -0,0 +1,56 @@
1
+ import { ExpectToEvaluateOpts } from './feature/internal/evaluateWithRetryAndTimeout.js'
2
+ import { CustomMatcherResult } from './feature/helpers/providePuppeteerViteMatchers.js'
3
+
4
+ // unit spec helpers
5
+ export { default as specRequest } from './unit/SpecRequest.js'
6
+ export { default as createPsychicServer } from './unit/createPsychicServer.js'
7
+ export { SpecRequest } from './unit/SpecRequest.js'
8
+ export { SpecSession } from './unit/SpecSession.js'
9
+
10
+ // feature spec helpers
11
+ export { default as providePuppeteerViteMatchers } from './feature/helpers/providePuppeteerViteMatchers.js'
12
+ export { default as launchBrowser } from './feature/helpers/launchBrowser.js'
13
+ export { default as launchPage } from './feature/helpers/launchPage.js'
14
+ export { default as launchViteServer, stopViteServer } from './feature/helpers/launchViteServer.js'
15
+ export { default as visit } from './feature/helpers/visit.js'
16
+
17
+ declare global {
18
+ function context(description: string, callback: () => void): void
19
+ }
20
+
21
+ declare module 'vitest' {
22
+ interface ExpectStatic extends PuppeteerAssertions {}
23
+ interface Assertion extends PuppeteerAssertions {}
24
+ }
25
+
26
+ interface PuppeteerAssertions {
27
+ // begin: dream matchers
28
+ toMatchDreamModel(expected: any): CustomMatcherResult
29
+ toMatchDreamModels(expected: any): CustomMatcherResult
30
+ toBeWithin(precision: number, expected: number): CustomMatcherResult
31
+ toEqualCalendarDate(expected: any): CustomMatcherResult
32
+
33
+ // begin: fspec matchers
34
+ toMatchTextContent(expected: any): Promise<CustomMatcherResult>
35
+ toNotMatchTextContent(expected: any): Promise<CustomMatcherResult>
36
+ toHaveSelector(expected: any): Promise<CustomMatcherResult>
37
+ toNotHaveSelector(expected: any): Promise<CustomMatcherResult>
38
+ toCheck(expected: any): Promise<CustomMatcherResult>
39
+ toClick(expected: any): Promise<CustomMatcherResult>
40
+ toClickLink(expected: any): Promise<CustomMatcherResult>
41
+ toClickLink(expected: any): Promise<CustomMatcherResult>
42
+ toClickSelector(expected: any): Promise<CustomMatcherResult>
43
+ toHavePath(expected: any): Promise<CustomMatcherResult>
44
+ toHaveUrl(expected: any): Promise<CustomMatcherResult>
45
+ toHaveChecked(expected: any): Promise<CustomMatcherResult>
46
+ toHaveUnchecked(expected: any): Promise<CustomMatcherResult>
47
+ toHaveLink(expected: any): Promise<CustomMatcherResult>
48
+ toFill(cssSelector: string, text: string): Promise<CustomMatcherResult>
49
+ toUncheck(expected: any): Promise<CustomMatcherResult>
50
+ toEvaluate(
51
+ expected: (a: any) => boolean | Promise<boolean>,
52
+ opts: ExpectToEvaluateOpts
53
+ ): Promise<CustomMatcherResult>
54
+ }
55
+
56
+ export default {}
@@ -0,0 +1,7 @@
1
+ export default async function sleep(ms: number) {
2
+ return await new Promise(accept => {
3
+ setTimeout(() => {
4
+ accept(undefined)
5
+ }, ms)
6
+ })
7
+ }
@@ -0,0 +1,160 @@
1
+ import supertest, { Response } from 'supertest'
2
+ import { createPsychicServer } from '../index.js'
3
+ import supersession, { HttpMethod } from './supersession.js'
4
+ import { SpecSession } from './SpecSession.js'
5
+
6
+ export class SpecRequest {
7
+ private PsychicServer: any
8
+ private server: any
9
+
10
+ public async get(
11
+ uri: string,
12
+ expectedStatus: number,
13
+ opts: SpecRequestOptsGet = {}
14
+ ): Promise<Response> {
15
+ return await this.makeRequest('get', uri, expectedStatus, opts as SpecRequestOptsAll)
16
+ }
17
+
18
+ public async post(
19
+ uri: string,
20
+ expectedStatus: number,
21
+ opts: SpecRequestOptsPost = {}
22
+ ): Promise<Response> {
23
+ return await this.makeRequest('post', uri, expectedStatus, opts as SpecRequestOptsAll)
24
+ }
25
+
26
+ public async put(
27
+ uri: string,
28
+ expectedStatus: number,
29
+ opts: SpecRequestOptsPost = {}
30
+ ): Promise<Response> {
31
+ return await this.makeRequest('put', uri, expectedStatus, opts as SpecRequestOptsAll)
32
+ }
33
+
34
+ public async patch(
35
+ uri: string,
36
+ expectedStatus: number,
37
+ opts: SpecRequestOptsPost = {}
38
+ ): Promise<Response> {
39
+ return await this.makeRequest('patch', uri, expectedStatus, opts as SpecRequestOptsAll)
40
+ }
41
+
42
+ public async delete(
43
+ uri: string,
44
+ expectedStatus: number,
45
+ opts: SpecRequestOptsPost = {}
46
+ ): Promise<Response> {
47
+ return await this.makeRequest('delete', uri, expectedStatus, opts as SpecRequestOptsAll)
48
+ }
49
+
50
+ public async init(PsychicServer: any) {
51
+ this.PsychicServer = PsychicServer
52
+ this.server ||= await createPsychicServer(PsychicServer)
53
+ }
54
+
55
+ public async session(
56
+ uri: string,
57
+ credentials: object,
58
+ expectedStatus: number,
59
+ opts: SpecRequestSessionOpts = {}
60
+ ): Promise<SpecSession> {
61
+ return await new Promise((accept, reject) => {
62
+ createPsychicServer(this.PsychicServer)
63
+ .then(server => {
64
+ const session = supersession(server)
65
+
66
+ // supersession is borrowed from a non-typescript repo, which
67
+ // does not have strong types around http methods, so we need to any cast
68
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any
69
+ ;(session[(opts.httpMethod || 'post') as keyof typeof session] as any)(uri)
70
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
71
+ .send(credentials)
72
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
73
+ .expect(expectedStatus)
74
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
75
+ .query(opts.query || {})
76
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
77
+ .set(opts.headers || {})
78
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
79
+ .end((err: Error) => {
80
+ if (err) return reject(err)
81
+
82
+ return accept(new SpecSession(session))
83
+ })
84
+ })
85
+ .catch(err => {
86
+ throw err
87
+ })
88
+ })
89
+ }
90
+
91
+ private async makeRequest(
92
+ method: 'get' | 'post' | 'put' | 'patch' | 'delete',
93
+ uri: string,
94
+ expectedStatus: number,
95
+ opts: SpecRequestOptsAll = {}
96
+ ) {
97
+ // TODO: find out why this is necessary. Currently, without initializing the server
98
+ // at the beginning of the specs, supertest is unable to use our server to handle requests.
99
+ // it gives the appearance of being an issue with a runaway promise (i.e. missing await)
100
+ // but I can't find it anywhere, so I am putting this init method in as a temporary fix.
101
+ if (!this.server)
102
+ throw new Error(
103
+ `
104
+ ERROR:
105
+ When making use of the send spec helper, you must first call "await specRequest.init(PsychicServer)"
106
+ from a beforEach hook at the root of your specs.
107
+ `
108
+ )
109
+
110
+ if (expectedStatus === 500) {
111
+ process.env.PSYCHIC_EXPECTING_INTERNAL_SERVER_ERROR = '1'
112
+ }
113
+
114
+ const req = supertest.agent(this.server.expressApp)
115
+ let request = req[method](uri)
116
+ if (opts.headers) request = request.set(opts.headers)
117
+ if (opts.query) request = request.query(opts.query)
118
+ if (method !== 'get') request = request.send(opts.data)
119
+
120
+ try {
121
+ const res = await request.expect(expectedStatus)
122
+ process.env.PSYCHIC_EXPECTING_INTERNAL_SERVER_ERROR = undefined
123
+ return res
124
+ } catch (err) {
125
+ // without manually console logging, you get no stack trace here
126
+ console.error(err)
127
+ console.trace()
128
+
129
+ process.env.PSYCHIC_EXPECTING_INTERNAL_SERVER_ERROR = undefined
130
+
131
+ throw err
132
+ }
133
+ }
134
+ }
135
+
136
+ export interface SpecRequestOptsAll extends SpecRequestOpts {
137
+ query?: Record<string, unknown>
138
+ data?: Record<string, unknown>
139
+ }
140
+
141
+ export interface SpecRequestOptsGet extends SpecRequestOpts {
142
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
143
+ query?: any
144
+ }
145
+
146
+ export interface SpecRequestOptsPost extends SpecRequestOpts {
147
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
148
+ data?: any
149
+ }
150
+
151
+ export interface SpecRequestOpts {
152
+ headers?: Record<string, string>
153
+ allowMocks?: boolean
154
+ }
155
+
156
+ export interface SpecRequestSessionOpts extends SpecRequestOptsAll {
157
+ httpMethod?: HttpMethod
158
+ }
159
+
160
+ export default new SpecRequest()
@@ -0,0 +1,103 @@
1
+ import { Response } from 'supertest'
2
+ import supersession, { HttpMethod } from './supersession.js'
3
+
4
+ // like SpecRequest, but meant to be bound to an instance
5
+ // of supersession, enabling chained requests to collect cookies
6
+ export class SpecSession {
7
+ constructor(private _session: ReturnType<typeof supersession>) {}
8
+
9
+ public async get(
10
+ uri: string,
11
+ expectedStatus: number,
12
+ opts: SpecRequestOptsGet = {}
13
+ ): Promise<Response> {
14
+ return await this.makeRequest('get', uri, expectedStatus, opts as SpecRequestOptsAll)
15
+ }
16
+
17
+ public async post(
18
+ uri: string,
19
+ expectedStatus: number,
20
+ opts: SpecRequestOptsPost = {}
21
+ ): Promise<Response> {
22
+ return await this.makeRequest('post', uri, expectedStatus, opts as SpecRequestOptsAll)
23
+ }
24
+
25
+ public async put(
26
+ uri: string,
27
+ expectedStatus: number,
28
+ opts: SpecRequestOptsPost = {}
29
+ ): Promise<Response> {
30
+ return await this.makeRequest('put', uri, expectedStatus, opts as SpecRequestOptsAll)
31
+ }
32
+
33
+ public async patch(
34
+ uri: string,
35
+ expectedStatus: number,
36
+ opts: SpecRequestOptsPost = {}
37
+ ): Promise<Response> {
38
+ return await this.makeRequest('patch', uri, expectedStatus, opts as SpecRequestOptsAll)
39
+ }
40
+
41
+ public async delete(
42
+ uri: string,
43
+ expectedStatus: number,
44
+ opts: SpecRequestOptsPost = {}
45
+ ): Promise<Response> {
46
+ return await this.makeRequest('delete', uri, expectedStatus, opts as SpecRequestOptsAll)
47
+ }
48
+
49
+ private async makeRequest(
50
+ method: 'get' | 'post' | 'put' | 'patch' | 'delete',
51
+ uri: string,
52
+ expectedStatus: number,
53
+ opts: SpecRequestOptsAll = {}
54
+ ) {
55
+ if (expectedStatus === 500) {
56
+ process.env.PSYCHIC_EXPECTING_INTERNAL_SERVER_ERROR = '1'
57
+ }
58
+
59
+ const req = this._session
60
+ let request = req[method](uri)
61
+ if (opts.headers) request = request.set(opts.headers)
62
+ if (opts.query) request = request.query(opts.query)
63
+ if (method !== 'get') request = request.send(opts.data)
64
+
65
+ try {
66
+ const res = await request.expect(expectedStatus)
67
+ process.env.PSYCHIC_EXPECTING_INTERNAL_SERVER_ERROR = undefined
68
+ return res
69
+ } catch (err) {
70
+ // without manually console logging, you get no stack trace here
71
+ console.error(err)
72
+ console.trace()
73
+
74
+ process.env.PSYCHIC_EXPECTING_INTERNAL_SERVER_ERROR = undefined
75
+
76
+ throw err
77
+ }
78
+ }
79
+ }
80
+
81
+ export interface SpecRequestOptsAll extends SpecRequestOpts {
82
+ query?: Record<string, unknown>
83
+ data?: Record<string, unknown>
84
+ }
85
+
86
+ export interface SpecRequestOptsGet extends SpecRequestOpts {
87
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
88
+ query?: any
89
+ }
90
+
91
+ export interface SpecRequestOptsPost extends SpecRequestOpts {
92
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
93
+ data?: any
94
+ }
95
+
96
+ export interface SpecRequestOpts {
97
+ headers?: Record<string, string>
98
+ allowMocks?: boolean
99
+ }
100
+
101
+ export interface SpecRequestSessionOpts extends SpecRequestOptsAll {
102
+ httpMethod?: HttpMethod
103
+ }
@@ -0,0 +1,8 @@
1
+ const _server: any = undefined
2
+ export default async function createPsychicServer(PsychicServer: any) {
3
+ if (_server) return _server!
4
+
5
+ const server = new PsychicServer()
6
+ await server.boot()
7
+ return server
8
+ }
@@ -0,0 +1,100 @@
1
+ import { CookieAccessInfo as CookieAccess } from 'cookiejar'
2
+ import http from 'http'
3
+ import request from 'supertest'
4
+ import URL, { UrlWithStringQuery } from 'url'
5
+
6
+ // NOTE: this is not original code.
7
+ // it was adapted from a non-typescript library with an uncertain future:
8
+ //
9
+ // https://github.com/rjz/supertest-session/blob/master/index.js
10
+
11
+ type AgentOptions = Parameters<typeof request.agent>[1]
12
+
13
+ class Supersession {
14
+ private app: http.Server
15
+ private agent: ReturnType<typeof request.agent>
16
+ private url: UrlWithStringQuery
17
+ private cookieAccess: CookieAccess
18
+
19
+ constructor(
20
+ server: any,
21
+ private options: AgentOptions = {}
22
+ ) {
23
+ if (!server.expressApp) {
24
+ throw new Error('Supersession requires an `app`')
25
+ }
26
+
27
+ this.agent = request.agent(server.expressApp, options)
28
+
29
+ const app = http.createServer(server.expressApp)
30
+ const url = (request as any).Test.prototype.serverAddress(app, '/')
31
+
32
+ this.app = app
33
+ this.url = URL.parse(url)
34
+
35
+ this.reset()
36
+
37
+ // typescript is telling me I don't need to worry about options.helpers,
38
+ // but leaving this commented out in case we need to revisit
39
+ // if (this.options.helpers instanceof Object) {
40
+ // assign(this, this.options.helpers)
41
+ // }
42
+ }
43
+
44
+ get cookies() {
45
+ return (this.agent as any).jar.getCookies(this.cookieAccess)
46
+ }
47
+
48
+ public reset() {
49
+ // Unset supertest-session options before forwarding options to superagent.
50
+ var agentOptions = {
51
+ ...this.options,
52
+ before: undefined,
53
+ cookieAccess: undefined,
54
+ destroy: undefined,
55
+ helpers: undefined,
56
+ }
57
+
58
+ this.agent = request.agent(this.app, agentOptions)
59
+
60
+ const domain = this.url.hostname
61
+ const path = this.url.path
62
+ const secure = 'https:' == this.url.protocol
63
+ const script = false
64
+ this.cookieAccess = (CookieAccess as any)(domain, path, secure, script)
65
+ }
66
+
67
+ destroy() {
68
+ if ((this.options as any).destroy) (this.options as any).destroy.call(this)
69
+ this.reset()
70
+ }
71
+
72
+ request(method: HttpMethod, route: string) {
73
+ var test = this.agent[method](route)
74
+
75
+ // typescript is telling me I don't need to worry about options.before,
76
+ // but leaving this commented out in case we need to revisit
77
+ // if (this.options.before) {
78
+ // this.options.before.call(this, test)
79
+ // }
80
+
81
+ return test
82
+ }
83
+ }
84
+
85
+ export const HttpMethods = ['get', 'post', 'put', 'patch', 'delete', 'options'] as const
86
+ export type HttpMethod = (typeof HttpMethods)[number]
87
+
88
+ HttpMethods.forEach(function (m) {
89
+ ;(Supersession as any).prototype[m as any] = function () {
90
+ var args = [].slice.call(arguments)
91
+ return this.request.apply(this, [m].concat(args))
92
+ }
93
+ })
94
+
95
+ export default function supersession(
96
+ server: any,
97
+ config: AgentOptions = {}
98
+ ): ReturnType<typeof request> {
99
+ return new Supersession(server, config) as unknown as ReturnType<typeof request>
100
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "./tsconfig.esm.build.json",
3
+ "include": ["src/**/*"],
4
+ "compilerOptions": {
5
+ "types": ["vitest/globals"]
6
+ }
7
+ }