@rvoh/psychic-spec-helpers 3.0.0 → 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.
@@ -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
+ }
@@ -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.0",
4
+ "version": "3.1.0",
5
5
  "description": "psychic framework spec helpers",
6
6
  "author": "RVO Health",
7
7
  "repository": {
@@ -29,24 +29,17 @@
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 && npx tsc -p ./tsconfig.esm.build.json",
33
- "build:test-app": "rm -rf dist && echo \"building test app to esm...\" && npx tsc -p ./tsconfig.esm.build.test-app.json && echo \"building test app to cjs...\" && npx tsc -p ./tsconfig.cjs.build.test-app.json",
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"
37
37
  },
38
- "pnpm": {
39
- "overrides": {
40
- "minimatch@3.1.2": "3.1.3",
41
- "minimatch@5.1.6": "5.1.7",
42
- "minimatch@9.0.5": "9.0.6"
43
- }
44
- },
45
38
  "peerDependencies": {
46
39
  "@rvoh/dream": "*",
47
40
  "@types/node": "*",
48
41
  "@types/supertest": "*",
49
- "puppeteer": "*",
42
+ "puppeteer": ">=24.15.0",
50
43
  "supertest": "*",
51
44
  "typescript": "*"
52
45
  },
@@ -55,31 +48,31 @@
55
48
  "@koa/cors": "^5.0.0",
56
49
  "@koa/etag": "^5.0.2",
57
50
  "@koa/router": "^15.3.0",
58
- "@rvoh/dream": "^2.1.1",
51
+ "@rvoh/dream": "^2.5.7",
59
52
  "@rvoh/dream-spec-helpers": "^2.0.0",
60
- "@rvoh/psychic": "^3.0.0-alpha.4",
53
+ "@rvoh/psychic": "^3.0.5",
61
54
  "@types/cookiejar": "^2",
62
55
  "@types/node": "^22.5.1",
63
56
  "@types/pg": "^8",
64
57
  "@types/supertest": "^6.0.3",
65
- "@typescript-eslint/parser": "^8.48.1",
66
- "eslint": "^9.39.1",
58
+ "@typescript-eslint/parser": "^8.57.1",
59
+ "eslint": "^9.39.4",
67
60
  "globals": "^16.5.0",
68
61
  "koa-bodyparser": "^4.4.1",
69
62
  "koa-conditional-get": "^3.0.0",
70
- "kysely": "^0.28.5",
63
+ "kysely": "^0.28.14",
71
64
  "kysely-codegen": "~0.19.0",
72
65
  "luxon-jest-matchers": "^0.1.14",
73
66
  "openapi-typescript": "^7.10.1",
74
67
  "pg": "^8.13.1",
75
68
  "prettier": "^3.5.3",
76
- "puppeteer": "^24.25.0",
77
- "supertest": "^7.1.4",
69
+ "puppeteer": "^24.39.1",
70
+ "supertest": "^7.2.2",
78
71
  "ts-node": "^10.9.2",
79
72
  "tsx": "^4.20.6",
80
73
  "typescript": "^5.5.4",
81
- "typescript-eslint": "^8.48.1",
82
- "vitest": "^4.0.10"
74
+ "typescript-eslint": "^8.57.1",
75
+ "vitest": "^4.1.0"
83
76
  },
84
77
  "dependencies": {
85
78
  "commander": "^14.0.2",
@@ -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 {