@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.
- package/LICENSE +23 -0
- package/README.md +68 -0
- package/dist/esm/src/feature/helpers/launchBrowser.js +9 -0
- package/dist/esm/src/feature/helpers/launchPage.js +5 -0
- package/dist/esm/src/feature/helpers/launchViteServer.js +89 -0
- package/dist/esm/src/feature/helpers/providePuppeteerViteMatchers.js +73 -0
- package/dist/esm/src/feature/helpers/visit.js +6 -0
- package/dist/esm/src/feature/internal/evaluateWithRetryAndTimeout.js +29 -0
- package/dist/esm/src/feature/internal/evaluationFailure.js +6 -0
- package/dist/esm/src/feature/internal/evaluationSuccess.js +6 -0
- package/dist/esm/src/feature/internal/getAllTextContentFromPage.js +19 -0
- package/dist/esm/src/feature/internal/isPuppeteerPage.js +3 -0
- package/dist/esm/src/feature/internal/matchFailure.js +6 -0
- package/dist/esm/src/feature/internal/matchSuccess.js +6 -0
- package/dist/esm/src/feature/internal/requirePuppeteerPage.js +6 -0
- package/dist/esm/src/feature/matchers/toCheck.js +23 -0
- package/dist/esm/src/feature/matchers/toClick.js +25 -0
- package/dist/esm/src/feature/matchers/toClickButton.js +25 -0
- package/dist/esm/src/feature/matchers/toClickLink.js +25 -0
- package/dist/esm/src/feature/matchers/toClickSelector.js +25 -0
- package/dist/esm/src/feature/matchers/toFill.js +21 -0
- package/dist/esm/src/feature/matchers/toHaveChecked.js +19 -0
- package/dist/esm/src/feature/matchers/toHaveLink.js +25 -0
- package/dist/esm/src/feature/matchers/toHavePath.js +15 -0
- package/dist/esm/src/feature/matchers/toHaveSelector.js +14 -0
- package/dist/esm/src/feature/matchers/toHaveUnchecked.js +19 -0
- package/dist/esm/src/feature/matchers/toHaveUrl.js +14 -0
- package/dist/esm/src/feature/matchers/toMatchTextContent.js +16 -0
- package/dist/esm/src/feature/matchers/toNotHaveSelector.js +14 -0
- package/dist/esm/src/feature/matchers/toNotMatchTextContent.js +16 -0
- package/dist/esm/src/feature/matchers/toUncheck.js +23 -0
- package/dist/esm/src/index.js +12 -0
- package/dist/esm/src/shared/sleep.js +7 -0
- package/dist/esm/src/unit/SpecRequest.js +89 -0
- package/dist/esm/src/unit/SpecSession.js +48 -0
- package/dist/esm/src/unit/createPsychicServer.js +8 -0
- package/dist/esm/src/unit/supersession.js +72 -0
- package/dist/types/src/feature/helpers/launchBrowser.d.ts +2 -0
- package/dist/types/src/feature/helpers/launchPage.d.ts +2 -0
- package/dist/types/src/feature/helpers/launchViteServer.d.ts +6 -0
- package/dist/types/src/feature/helpers/providePuppeteerViteMatchers.d.ts +5 -0
- package/dist/types/src/feature/helpers/visit.d.ts +5 -0
- package/dist/types/src/feature/internal/evaluateWithRetryAndTimeout.d.ts +14 -0
- package/dist/types/src/feature/internal/evaluationFailure.d.ts +4 -0
- package/dist/types/src/feature/internal/evaluationSuccess.d.ts +4 -0
- package/dist/types/src/feature/internal/getAllTextContentFromPage.d.ts +2 -0
- package/dist/types/src/feature/internal/isPuppeteerPage.d.ts +1 -0
- package/dist/types/src/feature/internal/matchFailure.d.ts +4 -0
- package/dist/types/src/feature/internal/matchSuccess.d.ts +4 -0
- package/dist/types/src/feature/internal/requirePuppeteerPage.d.ts +1 -0
- package/dist/types/src/feature/matchers/toCheck.d.ts +5 -0
- package/dist/types/src/feature/matchers/toClick.d.ts +5 -0
- package/dist/types/src/feature/matchers/toClickButton.d.ts +5 -0
- package/dist/types/src/feature/matchers/toClickLink.d.ts +5 -0
- package/dist/types/src/feature/matchers/toClickSelector.d.ts +5 -0
- package/dist/types/src/feature/matchers/toFill.d.ts +5 -0
- package/dist/types/src/feature/matchers/toHaveChecked.d.ts +5 -0
- package/dist/types/src/feature/matchers/toHaveLink.d.ts +5 -0
- package/dist/types/src/feature/matchers/toHavePath.d.ts +5 -0
- package/dist/types/src/feature/matchers/toHaveSelector.d.ts +5 -0
- package/dist/types/src/feature/matchers/toHaveUnchecked.d.ts +5 -0
- package/dist/types/src/feature/matchers/toHaveUrl.d.ts +5 -0
- package/dist/types/src/feature/matchers/toMatchTextContent.d.ts +5 -0
- package/dist/types/src/feature/matchers/toNotHaveSelector.d.ts +5 -0
- package/dist/types/src/feature/matchers/toNotMatchTextContent.d.ts +5 -0
- package/dist/types/src/feature/matchers/toUncheck.d.ts +5 -0
- package/dist/types/src/index.d.ts +45 -0
- package/dist/types/src/shared/sleep.d.ts +1 -0
- package/dist/types/src/unit/SpecRequest.d.ts +34 -0
- package/dist/types/src/unit/SpecSession.d.ts +29 -0
- package/dist/types/src/unit/createPsychicServer.d.ts +1 -0
- package/dist/types/src/unit/supersession.d.ts +6 -0
- package/package.json +47 -0
- package/src/feature/helpers/launchBrowser.ts +10 -0
- package/src/feature/helpers/launchPage.ts +7 -0
- package/src/feature/helpers/launchViteServer.ts +104 -0
- package/src/feature/helpers/providePuppeteerViteMatchers.ts +102 -0
- package/src/feature/helpers/visit.ts +11 -0
- package/src/feature/internal/evaluateWithRetryAndTimeout.ts +49 -0
- package/src/feature/internal/evaluationFailure.ts +6 -0
- package/src/feature/internal/evaluationSuccess.ts +6 -0
- package/src/feature/internal/getAllTextContentFromPage.ts +26 -0
- package/src/feature/internal/isPuppeteerPage.ts +3 -0
- package/src/feature/internal/matchFailure.ts +6 -0
- package/src/feature/internal/matchSuccess.ts +6 -0
- package/src/feature/internal/requirePuppeteerPage.ts +7 -0
- package/src/feature/matchers/toCheck.ts +34 -0
- package/src/feature/matchers/toClick.ts +30 -0
- package/src/feature/matchers/toClickButton.ts +30 -0
- package/src/feature/matchers/toClickLink.ts +30 -0
- package/src/feature/matchers/toClickSelector.ts +30 -0
- package/src/feature/matchers/toFill.ts +28 -0
- package/src/feature/matchers/toHaveChecked.ts +26 -0
- package/src/feature/matchers/toHaveLink.ts +30 -0
- package/src/feature/matchers/toHavePath.ts +23 -0
- package/src/feature/matchers/toHaveSelector.ts +21 -0
- package/src/feature/matchers/toHaveUnchecked.ts +26 -0
- package/src/feature/matchers/toHaveUrl.ts +21 -0
- package/src/feature/matchers/toMatchTextContent.ts +23 -0
- package/src/feature/matchers/toNotHaveSelector.ts +21 -0
- package/src/feature/matchers/toNotMatchTextContent.ts +26 -0
- package/src/feature/matchers/toUncheck.ts +35 -0
- package/src/index.ts +56 -0
- package/src/shared/sleep.ts +7 -0
- package/src/unit/SpecRequest.ts +160 -0
- package/src/unit/SpecSession.ts +103 -0
- package/src/unit/createPsychicServer.ts +8 -0
- package/src/unit/supersession.ts +100 -0
- 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,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,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
|
+
}
|