@rvoh/psychic-spec-helpers 3.0.1 → 3.1.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/resetBrowserState.js +62 -0
- package/dist/esm/src/index.js +1 -0
- package/dist/types/src/feature/helpers/resetBrowserState.d.ts +20 -0
- package/dist/types/src/index.d.ts +1 -0
- package/package.json +4 -4
- package/src/feature/helpers/resetBrowserState.ts +66 -0
- package/src/index.ts +1 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
async function bestEffort(fn) {
|
|
2
|
+
try {
|
|
3
|
+
await fn();
|
|
4
|
+
}
|
|
5
|
+
catch (err) {
|
|
6
|
+
// Per-spec teardown must never throw: a failure cleaning one spec's state
|
|
7
|
+
// would fail an unrelated spec's `afterEach`. Swallow and move on.
|
|
8
|
+
void err;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Per-spec browser cleanup for feature specs that share a single browser
|
|
13
|
+
* across spec files. Call in `afterEach` so every spec starts from a clean
|
|
14
|
+
* slate and so server-side resources are released between specs.
|
|
15
|
+
*
|
|
16
|
+
* Three best-effort steps, in this order:
|
|
17
|
+
*
|
|
18
|
+
* 1. Clear `localStorage` / `sessionStorage` for the current origin (auth
|
|
19
|
+
* tokens, app state). Done first, while the page is still on the app
|
|
20
|
+
* origin — storage is inaccessible once we navigate to `about:blank`.
|
|
21
|
+
* 2. Clear cookies for the page's browser context (context-scoped so
|
|
22
|
+
* parallel contexts stay isolated).
|
|
23
|
+
* 3. Navigate to `about:blank`. Besides the clean slate, this cancels any
|
|
24
|
+
* still-in-flight requests, releasing server-side resources they held
|
|
25
|
+
* (e.g. a pooled database client) so server teardown isn't blocked.
|
|
26
|
+
*
|
|
27
|
+
* The shared browser is intentionally left open and reusable. Operates on
|
|
28
|
+
* the global `page`; a no-op if there is no open page.
|
|
29
|
+
*/
|
|
30
|
+
export default async function resetBrowserState() {
|
|
31
|
+
const page = globalThis.page;
|
|
32
|
+
if (!page || page.isClosed())
|
|
33
|
+
return;
|
|
34
|
+
if (/^https?:/.test(page.url())) {
|
|
35
|
+
await bestEffort(() => page.evaluate(() => {
|
|
36
|
+
localStorage.clear();
|
|
37
|
+
sessionStorage.clear();
|
|
38
|
+
// `document` isn't in the Node lib this package is type-checked
|
|
39
|
+
// against (localStorage/sessionStorage are), so reach it through a
|
|
40
|
+
// typed globalThis cast — it exists at runtime in the browser.
|
|
41
|
+
const { document } = globalThis;
|
|
42
|
+
// Expire every JS-visible cookie on this origin. The browser-context
|
|
43
|
+
// cookie API below covers HttpOnly cookies, but does not reliably
|
|
44
|
+
// surface document.cookie-set cookies across every Puppeteer browser
|
|
45
|
+
// backend (notably Firefox/WebDriver-BiDi), so sweep here too.
|
|
46
|
+
for (const entry of document.cookie.split(';')) {
|
|
47
|
+
const eq = entry.indexOf('=');
|
|
48
|
+
const name = (eq > -1 ? entry.slice(0, eq) : entry).trim();
|
|
49
|
+
if (name)
|
|
50
|
+
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/`;
|
|
51
|
+
}
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
// Catches HttpOnly cookies (and any the JS sweep above could not reach).
|
|
55
|
+
await bestEffort(async () => {
|
|
56
|
+
const context = page.browserContext();
|
|
57
|
+
const cookies = await context.cookies();
|
|
58
|
+
if (cookies.length)
|
|
59
|
+
await context.deleteCookie(...cookies);
|
|
60
|
+
});
|
|
61
|
+
await bestEffort(() => page.goto('about:blank'));
|
|
62
|
+
}
|
package/dist/esm/src/index.js
CHANGED
|
@@ -9,5 +9,6 @@ export { default as launchBrowser } from './feature/helpers/launchBrowser.js';
|
|
|
9
9
|
export { default as launchDevServer, stopDevServer, stopDevServers, } from './feature/helpers/launchDevServer.js';
|
|
10
10
|
export { default as launchPage } from './feature/helpers/launchPage.js';
|
|
11
11
|
export { default as providePuppeteerViteMatchers } from './feature/helpers/providePuppeteerViteMatchers.js';
|
|
12
|
+
export { default as resetBrowserState } from './feature/helpers/resetBrowserState.js';
|
|
12
13
|
export { default as visit } from './feature/helpers/visit.js';
|
|
13
14
|
export default {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-spec browser cleanup for feature specs that share a single browser
|
|
3
|
+
* across spec files. Call in `afterEach` so every spec starts from a clean
|
|
4
|
+
* slate and so server-side resources are released between specs.
|
|
5
|
+
*
|
|
6
|
+
* Three best-effort steps, in this order:
|
|
7
|
+
*
|
|
8
|
+
* 1. Clear `localStorage` / `sessionStorage` for the current origin (auth
|
|
9
|
+
* tokens, app state). Done first, while the page is still on the app
|
|
10
|
+
* origin — storage is inaccessible once we navigate to `about:blank`.
|
|
11
|
+
* 2. Clear cookies for the page's browser context (context-scoped so
|
|
12
|
+
* parallel contexts stay isolated).
|
|
13
|
+
* 3. Navigate to `about:blank`. Besides the clean slate, this cancels any
|
|
14
|
+
* still-in-flight requests, releasing server-side resources they held
|
|
15
|
+
* (e.g. a pooled database client) so server teardown isn't blocked.
|
|
16
|
+
*
|
|
17
|
+
* The shared browser is intentionally left open and reusable. Operates on
|
|
18
|
+
* the global `page`; a no-op if there is no open page.
|
|
19
|
+
*/
|
|
20
|
+
export default function resetBrowserState(): Promise<void>;
|
|
@@ -13,6 +13,7 @@ export { default as launchBrowser } from './feature/helpers/launchBrowser.js';
|
|
|
13
13
|
export { default as launchDevServer, stopDevServer, stopDevServers, } from './feature/helpers/launchDevServer.js';
|
|
14
14
|
export { default as launchPage } from './feature/helpers/launchPage.js';
|
|
15
15
|
export { default as providePuppeteerViteMatchers } from './feature/helpers/providePuppeteerViteMatchers.js';
|
|
16
|
+
export { default as resetBrowserState } from './feature/helpers/resetBrowserState.js';
|
|
16
17
|
export { default as visit } from './feature/helpers/visit.js';
|
|
17
18
|
declare global {
|
|
18
19
|
const context: (typeof import('vitest'))['describe'];
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@rvoh/psychic-spec-helpers",
|
|
4
|
-
"version": "3.0
|
|
4
|
+
"version": "3.1.0",
|
|
5
5
|
"description": "psychic framework spec helpers",
|
|
6
6
|
"author": "RVO Health",
|
|
7
7
|
"repository": {
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"psy": "NODE_ENV=${NODE_ENV:-test} tsx test-app/src/cli/index.ts",
|
|
30
30
|
"uspec": "vitest --config ./spec/unit/vite.config.ts",
|
|
31
31
|
"fspec": "vitest --config ./spec/features/vite.config.ts",
|
|
32
|
-
"build": "echo \"building psychic-spec-helpers...\" && rm -rf dist &&
|
|
33
|
-
"build:test-app": "rm -rf dist && echo \"building test app to esm...\" &&
|
|
32
|
+
"build": "echo \"building psychic-spec-helpers...\" && rm -rf dist && tsc -p ./tsconfig.esm.build.json",
|
|
33
|
+
"build:test-app": "rm -rf dist && echo \"building test app to esm...\" && tsc -p ./tsconfig.esm.build.test-app.json && echo \"building test app to cjs...\" && npx tsc -p ./tsconfig.cjs.build.test-app.json",
|
|
34
34
|
"lint": "pnpm eslint --no-warn-ignored \"src/**/*.ts\" \"spec/**/*.ts\" && pnpm prettier . --check",
|
|
35
35
|
"format": "pnpm prettier . --write",
|
|
36
36
|
"prepack": "pnpm build"
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"globals": "^16.5.0",
|
|
61
61
|
"koa-bodyparser": "^4.4.1",
|
|
62
62
|
"koa-conditional-get": "^3.0.0",
|
|
63
|
-
"kysely": "^0.28.
|
|
63
|
+
"kysely": "^0.28.14",
|
|
64
64
|
"kysely-codegen": "~0.19.0",
|
|
65
65
|
"luxon-jest-matchers": "^0.1.14",
|
|
66
66
|
"openapi-typescript": "^7.10.1",
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Page } from 'puppeteer'
|
|
2
|
+
|
|
3
|
+
async function bestEffort(fn: () => Promise<unknown>): Promise<void> {
|
|
4
|
+
try {
|
|
5
|
+
await fn()
|
|
6
|
+
} catch (err) {
|
|
7
|
+
// Per-spec teardown must never throw: a failure cleaning one spec's state
|
|
8
|
+
// would fail an unrelated spec's `afterEach`. Swallow and move on.
|
|
9
|
+
void err
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Per-spec browser cleanup for feature specs that share a single browser
|
|
15
|
+
* across spec files. Call in `afterEach` so every spec starts from a clean
|
|
16
|
+
* slate and so server-side resources are released between specs.
|
|
17
|
+
*
|
|
18
|
+
* Three best-effort steps, in this order:
|
|
19
|
+
*
|
|
20
|
+
* 1. Clear `localStorage` / `sessionStorage` for the current origin (auth
|
|
21
|
+
* tokens, app state). Done first, while the page is still on the app
|
|
22
|
+
* origin — storage is inaccessible once we navigate to `about:blank`.
|
|
23
|
+
* 2. Clear cookies for the page's browser context (context-scoped so
|
|
24
|
+
* parallel contexts stay isolated).
|
|
25
|
+
* 3. Navigate to `about:blank`. Besides the clean slate, this cancels any
|
|
26
|
+
* still-in-flight requests, releasing server-side resources they held
|
|
27
|
+
* (e.g. a pooled database client) so server teardown isn't blocked.
|
|
28
|
+
*
|
|
29
|
+
* The shared browser is intentionally left open and reusable. Operates on
|
|
30
|
+
* the global `page`; a no-op if there is no open page.
|
|
31
|
+
*/
|
|
32
|
+
export default async function resetBrowserState(): Promise<void> {
|
|
33
|
+
const page = (globalThis as { page?: Page }).page
|
|
34
|
+
if (!page || page.isClosed()) return
|
|
35
|
+
|
|
36
|
+
if (/^https?:/.test(page.url())) {
|
|
37
|
+
await bestEffort(() =>
|
|
38
|
+
page.evaluate(() => {
|
|
39
|
+
localStorage.clear()
|
|
40
|
+
sessionStorage.clear()
|
|
41
|
+
// `document` isn't in the Node lib this package is type-checked
|
|
42
|
+
// against (localStorage/sessionStorage are), so reach it through a
|
|
43
|
+
// typed globalThis cast — it exists at runtime in the browser.
|
|
44
|
+
const { document } = globalThis as unknown as { document: { cookie: string } }
|
|
45
|
+
// Expire every JS-visible cookie on this origin. The browser-context
|
|
46
|
+
// cookie API below covers HttpOnly cookies, but does not reliably
|
|
47
|
+
// surface document.cookie-set cookies across every Puppeteer browser
|
|
48
|
+
// backend (notably Firefox/WebDriver-BiDi), so sweep here too.
|
|
49
|
+
for (const entry of document.cookie.split(';')) {
|
|
50
|
+
const eq = entry.indexOf('=')
|
|
51
|
+
const name = (eq > -1 ? entry.slice(0, eq) : entry).trim()
|
|
52
|
+
if (name) document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/`
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Catches HttpOnly cookies (and any the JS sweep above could not reach).
|
|
59
|
+
await bestEffort(async () => {
|
|
60
|
+
const context = page.browserContext()
|
|
61
|
+
const cookies = await context.cookies()
|
|
62
|
+
if (cookies.length) await context.deleteCookie(...cookies)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
await bestEffort(() => page.goto('about:blank'))
|
|
66
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -28,6 +28,7 @@ export {
|
|
|
28
28
|
} from './feature/helpers/launchDevServer.js'
|
|
29
29
|
export { default as launchPage } from './feature/helpers/launchPage.js'
|
|
30
30
|
export { default as providePuppeteerViteMatchers } from './feature/helpers/providePuppeteerViteMatchers.js'
|
|
31
|
+
export { default as resetBrowserState } from './feature/helpers/resetBrowserState.js'
|
|
31
32
|
export { default as visit } from './feature/helpers/visit.js'
|
|
32
33
|
|
|
33
34
|
declare global {
|