pi-lean-dimension 0.1.0 → 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/README.md +96 -36
- package/node_modules/pi-lean-portal/AGENTS.md +146 -4
- package/node_modules/pi-lean-portal/README.md +112 -50
- package/node_modules/pi-lean-portal/__tests__/browser-data.test.ts +124 -0
- package/node_modules/pi-lean-portal/__tests__/browser-inspect.test.ts +192 -16
- package/node_modules/pi-lean-portal/__tests__/browser-toggle-profile.test.ts +3 -3
- package/node_modules/pi-lean-portal/__tests__/browser-toggle.test.ts +49 -35
- package/node_modules/pi-lean-portal/__tests__/chromium-py-persistence.test.ts +21 -195
- package/node_modules/pi-lean-portal/__tests__/chromium-py.test.ts +17 -81
- package/node_modules/pi-lean-portal/__tests__/chromium.test.ts +25 -0
- package/node_modules/pi-lean-portal/__tests__/contributed/invisible-py/invisible-py.test.ts +299 -0
- package/node_modules/pi-lean-portal/__tests__/cookie-persistence.test.ts +22 -182
- package/node_modules/pi-lean-portal/__tests__/fetch-backend.test.ts +1 -1
- package/node_modules/pi-lean-portal/__tests__/firefox-py-persistence.test.ts +21 -184
- package/node_modules/pi-lean-portal/__tests__/firefox-py.test.ts +17 -101
- package/node_modules/pi-lean-portal/__tests__/firefox.test.ts +2 -18
- package/node_modules/pi-lean-portal/__tests__/helpers/__pycache__/mock-python-bridge.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/create-py-backend-harness.ts +105 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/load-plugin-config-from-file.ts +53 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/mock-plugin.ts +11 -7
- package/node_modules/pi-lean-portal/__tests__/helpers/mock-python-bridge.py +4 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/persistence-suite.ts +218 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/plugin-contract.ts +198 -318
- package/node_modules/pi-lean-portal/__tests__/helpers/probe-user-backend.ts +198 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/test-server.ts +14 -0
- package/node_modules/pi-lean-portal/__tests__/plugin-config-browser.test.ts +150 -15
- package/node_modules/pi-lean-portal/__tests__/plugin-loading.test.ts +120 -18
- package/node_modules/pi-lean-portal/__tests__/plugin-registry.test.ts +6 -67
- package/node_modules/pi-lean-portal/__tests__/probe-user-backend.test.ts +236 -0
- package/node_modules/pi-lean-portal/__tests__/python-adapter.test.ts +401 -11
- package/node_modules/pi-lean-portal/__tests__/router-session.test.ts +4 -1
- package/node_modules/pi-lean-portal/__tests__/run-contributed-suites.test.ts +318 -0
- package/node_modules/pi-lean-portal/__tests__/session-manager.test.ts +50 -0
- package/node_modules/pi-lean-portal/__tests__/snapshot-cache.test.ts +2 -2
- package/node_modules/pi-lean-portal/__tests__/url-safety.test.ts +1 -1
- package/node_modules/pi-lean-portal/backends/chromium/index.ts +5 -13
- package/node_modules/pi-lean-portal/backends/chromium-py/__pycache__/bridge.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/chromium-py/bridge.py +0 -2
- package/node_modules/pi-lean-portal/backends/firefox/index.ts +6 -5
- package/node_modules/pi-lean-portal/backends/firefox-py/__pycache__/bridge.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/firefox-py/bridge.py +7 -6
- package/node_modules/pi-lean-portal/backends/playwright-base/playwright-plugin.ts +241 -398
- package/node_modules/pi-lean-portal/backends/python-adapter.ts +182 -83
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__init__.py +1 -42
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/__init__.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/__init__.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/accessibility.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/accessibility.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bot_detection.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bot_detection.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bridge.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bridge.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/browser_data.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/browser_data.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/patch_playwright.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/patch_playwright.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/transport.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/transport.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/accessibility.py +12 -147
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/bot_detection.py +12 -37
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/bridge.py +249 -322
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/browser_data.py +92 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/patch_playwright.py +321 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/playwright_base.py +511 -299
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/conftest.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/{test_accessibility.cpython-313-pytest-9.1.0.pyc → test_accessibility.cpython-313-pytest-9.1.1.pyc} +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_accessibility.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_bot_detection.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_bot_detection.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_browser_data.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_browser_data.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/{test_chromium_py_bridge.cpython-313-pytest-9.1.0.pyc → test_chromium_py_bridge.cpython-313-pytest-9.1.1.pyc} +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/{test_firefox_py_bridge.cpython-313-pytest-9.1.0.pyc → test_firefox_py_bridge.cpython-313-pytest-9.1.1.pyc} +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_playwright_base_quirks.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_playwright_base_quirks.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_py_bridges.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/{test_transport.cpython-313-pytest-9.1.0.pyc → test_transport.cpython-313-pytest-9.1.1.pyc} +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/conftest.py +95 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_accessibility.py +8 -132
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_bot_detection.py +8 -147
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_browser_data.py +131 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_playwright_base_quirks.py +768 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_py_bridges.py +198 -0
- package/node_modules/pi-lean-portal/browser-toggle.ts +33 -69
- package/node_modules/pi-lean-portal/contributed/CHOOSING.md +126 -0
- package/node_modules/pi-lean-portal/contributed/README.md +304 -0
- package/node_modules/pi-lean-portal/contributed/camoufox-py/bridge.py +216 -0
- package/node_modules/pi-lean-portal/contributed/invisible-py/__pycache__/bridge.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/contributed/invisible-py/bridge.py +434 -0
- package/node_modules/pi-lean-portal/core/fetch-backend.ts +0 -5
- package/node_modules/pi-lean-portal/core/plugin-api.ts +6 -33
- package/node_modules/pi-lean-portal/core/plugin-config.ts +75 -58
- package/node_modules/pi-lean-portal/core/plugin-registry.ts +11 -49
- package/node_modules/pi-lean-portal/core/router.ts +59 -98
- package/node_modules/pi-lean-portal/core/shared/accessibility-tree.ts +10 -143
- package/node_modules/pi-lean-portal/core/shared/bot-detection.ts +31 -77
- package/node_modules/pi-lean-portal/core/shared/browser-data.json +183 -0
- package/node_modules/pi-lean-portal/core/shared/browser-data.ts +50 -0
- package/node_modules/pi-lean-portal/core/shared/browser-events.ts +6 -6
- package/node_modules/pi-lean-portal/core/shared/dom-extractor.ts +97 -36
- package/node_modules/pi-lean-portal/core/shared/nav-settle.ts +12 -15
- package/node_modules/pi-lean-portal/core/shared/paths.ts +3 -0
- package/node_modules/pi-lean-portal/core/shared/session-manager.ts +7 -21
- package/node_modules/pi-lean-portal/{verify-ship-manifest.ts → core/shared/ship-manifest.ts} +34 -9
- package/node_modules/pi-lean-portal/core/shared/snapshot-cache.ts +7 -6
- package/node_modules/pi-lean-portal/core/shared/storage-state.ts +40 -9
- package/node_modules/pi-lean-portal/index.ts +42 -7
- package/node_modules/pi-lean-portal/package.json +8 -3
- package/node_modules/pi-lean-portal/ship-manifest.test.ts +8 -3
- package/node_modules/pi-lean-portal/tools/browser-inspect.ts +2 -6
- package/node_modules/pi-lean-portal/tools/browser-navigate.ts +7 -5
- package/node_modules/pi-lean-portal/tools/browser-snapshot.ts +2 -5
- package/node_modules/pi-lean-portal/tools/utils.ts +22 -4
- package/node_modules/pi-lean-portal/tools/web-fetch.ts +3 -4
- package/node_modules/pi-lean-search/README.md +6 -2
- package/node_modules/pi-lean-search/__tests__/web-search.test.ts +11 -0
- package/node_modules/pi-lean-search/index.ts +4 -21
- package/node_modules/pi-lean-search/package.json +2 -2
- package/node_modules/pi-lean-search/verify-ship-manifest.ts +6 -92
- package/node_modules/pi-lean-search/web-search-tool.ts +19 -13
- package/package.json +4 -4
- package/node_modules/pi-lean-portal/__tests__/helpers/reddit-fixture.ts +0 -264
- package/node_modules/pi-lean-portal/__tests__/helpers/toggle-test-utils.ts +0 -31
- package/node_modules/pi-lean-portal/__tests__/reddit-dialog.test.ts +0 -302
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/occlusion.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_bot_detection.cpython-313-pytest-9.1.0.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_chromium_py_bridge.py +0 -281
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_firefox_py_bridge.py +0 -212
|
@@ -1,196 +1,36 @@
|
|
|
1
|
-
|
|
2
|
-
* Cookie Persistence Integration Test — end-to-end validation of Bug A
|
|
3
|
-
* (router loads storage state) and Bug B (plugin saves state on re-navigate).
|
|
4
|
-
*
|
|
5
|
-
* Uses a live Chromium browser against a local HTTP test server that serves
|
|
6
|
-
* a page with a cookie-based consent dialog. The test:
|
|
7
|
-
*
|
|
8
|
-
* 1. Navigates with a named profile — dialog visible
|
|
9
|
-
* 2. Clicks "Accept All" — sets a consent cookie
|
|
10
|
-
* 3. Navigates again with the same profile — dialog should NOT reappear
|
|
11
|
-
* because `_persistState` saves the cookie to disk before closing the
|
|
12
|
-
* old context, and the returned state is passed to the new context
|
|
13
|
-
* via the in-memory fallback (options?.storageState ?? savedState).
|
|
14
|
-
*
|
|
15
|
-
* Auto-skips the entire suite when Playwright Chromium is not installed.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
19
2
|
import { chromium } from "playwright";
|
|
3
|
+
import { describe, beforeAll, afterAll } from "vitest";
|
|
20
4
|
import { ChromiumPlugin } from "../backends/chromium/index.js";
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
COOKIE_PERSISTENCE_HTML,
|
|
24
|
-
} from "./helpers/test-server.js";
|
|
25
|
-
import {
|
|
26
|
-
loadStorageState,
|
|
27
|
-
deleteStorageState,
|
|
28
|
-
} from "../core/shared/storage-state.js";
|
|
29
|
-
import { sessionManager } from "../core/shared/session-manager.js";
|
|
30
|
-
|
|
31
|
-
// ─── Chromium availability check — skip gracefully if absent ───────
|
|
32
|
-
|
|
33
|
-
let chromiumAvailable = false;
|
|
34
|
-
try {
|
|
35
|
-
chromium.executablePath();
|
|
36
|
-
chromiumAvailable = true;
|
|
37
|
-
} catch {
|
|
38
|
-
chromiumAvailable = false;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const describeIfChromium = chromiumAvailable ? describe : describe.skip;
|
|
5
|
+
import { startCookiePersistenceServer } from "./helpers/test-server.js";
|
|
6
|
+
import { runPersistenceSuite } from "./helpers/persistence-suite.js";
|
|
42
7
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
* See ``COOKIE_PERSISTENCE_HTML`` in ``helpers/test-server.ts``.
|
|
51
|
-
*/
|
|
52
|
-
|
|
53
|
-
// ─── Test Server Setup (always runs, server is cheap to start/stop) ─
|
|
8
|
+
const describeIfAvailable = (() => {
|
|
9
|
+
try {
|
|
10
|
+
return existsSync(chromium.executablePath()) ? describe : describe.skip;
|
|
11
|
+
} catch {
|
|
12
|
+
return describe.skip;
|
|
13
|
+
}
|
|
14
|
+
})();
|
|
54
15
|
|
|
55
16
|
let serverUrl: string;
|
|
56
17
|
let stopServer: () => Promise<void>;
|
|
57
18
|
|
|
58
19
|
beforeAll(async () => {
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
63
|
-
res.end(COOKIE_PERSISTENCE_HTML);
|
|
64
|
-
} else {
|
|
65
|
-
res.writeHead(404, { "Content-Type": "text/html; charset=utf-8" });
|
|
66
|
-
res.end("404");
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
serverUrl = server.url;
|
|
70
|
-
stopServer = server.stop;
|
|
20
|
+
const s = await startCookiePersistenceServer();
|
|
21
|
+
serverUrl = s.url;
|
|
22
|
+
stopServer = s.stop;
|
|
71
23
|
});
|
|
72
|
-
|
|
73
24
|
afterAll(async () => {
|
|
74
25
|
await stopServer();
|
|
75
26
|
});
|
|
76
27
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const NAV_TIMEOUT = 30_000;
|
|
86
|
-
const TASK_ID = "cookie-persistence";
|
|
87
|
-
|
|
88
|
-
// ═════════════════════════════════════════════════════════════════
|
|
89
|
-
// Tests — only run if Chromium is available
|
|
90
|
-
// ═════════════════════════════════════════════════════════════════
|
|
91
|
-
|
|
92
|
-
describeIfChromium(
|
|
93
|
-
"Cookie persistence across navigations",
|
|
94
|
-
() => {
|
|
95
|
-
const TEST_PROFILE = `cookie-persistence-test-${Date.now()}`;
|
|
96
|
-
|
|
97
|
-
let plugin: ChromiumPlugin;
|
|
98
|
-
|
|
99
|
-
beforeAll(async () => {
|
|
100
|
-
plugin = await createPlugin();
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
afterAll(async () => {
|
|
104
|
-
await plugin.cleanupAll().catch(() => {});
|
|
105
|
-
deleteStorageState(TEST_PROFILE);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it("navigates with a named profile — consent dialog is visible", async () => {
|
|
109
|
-
const nav = await plugin.navigate(serverUrl, TASK_ID, NAV_TIMEOUT, {
|
|
110
|
-
profileName: TEST_PROFILE,
|
|
111
|
-
profileMode: "named",
|
|
112
|
-
});
|
|
113
|
-
expect(nav.success).toBe(true);
|
|
114
|
-
expect(nav.title).toContain("Cookie Persistence Test");
|
|
115
|
-
expect(nav.snapshot).toContain("Consent");
|
|
116
|
-
expect(nav.snapshot).toContain("Accept All");
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it("clicks Accept All — cookie set, consent dialog closes", async () => {
|
|
120
|
-
const snap = await plugin.snapshot(TASK_ID);
|
|
121
|
-
expect(snap.success).toBe(true);
|
|
122
|
-
|
|
123
|
-
// Find "Accept All" button in the snapshot and extract its @e ref
|
|
124
|
-
const btnMatch = snap.snapshot.match(/@(e\d+)\b.*?button.*?Accept All/);
|
|
125
|
-
expect(btnMatch).toBeTruthy();
|
|
126
|
-
if (!btnMatch) return;
|
|
127
|
-
|
|
128
|
-
const ref = `@${btnMatch[1]!}`;
|
|
129
|
-
const clickResult = await plugin.click(TASK_ID, ref);
|
|
130
|
-
expect(clickResult.success).toBe(true);
|
|
131
|
-
|
|
132
|
-
// Verify dialog is gone from snapshot
|
|
133
|
-
if (clickResult.snapshot) {
|
|
134
|
-
expect(clickResult.snapshot).not.toContain("Consent");
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it("creates session and sets persistState (simulating the router's setup)", async () => {
|
|
139
|
-
// In production, router.navigate() creates the session and sets
|
|
140
|
-
// persistState & profileName before calling plugin.navigate().
|
|
141
|
-
// Since we're testing the plugin directly, we simulate this setup.
|
|
142
|
-
sessionManager.createSession(TASK_ID, "chromium");
|
|
143
|
-
const session = sessionManager.getSession(TASK_ID);
|
|
144
|
-
expect(session).toBeDefined();
|
|
145
|
-
if (session) {
|
|
146
|
-
session.persistState = true;
|
|
147
|
-
session.profileName = TEST_PROFILE;
|
|
148
|
-
}
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it("navigates again with same profile — consent dialog does NOT reappear", async () => {
|
|
152
|
-
// This navigate triggers getOrCreateContext, which:
|
|
153
|
-
// 1. Calls _persistState → saves cookies to disk, returns state object
|
|
154
|
-
// 2. Closes old context
|
|
155
|
-
// 3. Uses savedState as fallback for the new context's storageState
|
|
156
|
-
// The cookie set during step 2 is thus available for the new context.
|
|
157
|
-
const nav = await plugin.navigate(serverUrl, TASK_ID, NAV_TIMEOUT, {
|
|
158
|
-
profileName: TEST_PROFILE,
|
|
159
|
-
profileMode: "named",
|
|
160
|
-
});
|
|
161
|
-
expect(nav.success).toBe(true);
|
|
162
|
-
|
|
163
|
-
// Consent dialog should NOT be present — cookies were restored
|
|
164
|
-
// from the in-memory fallback (savedState returned by _persistState).
|
|
165
|
-
expect(nav.snapshot).not.toContain("Consent");
|
|
166
|
-
expect(nav.snapshot).not.toContain("Accept All");
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
it("storage-state.json exists on disk with the consent cookie", async () => {
|
|
170
|
-
const state = loadStorageState(TEST_PROFILE);
|
|
171
|
-
expect(state).not.toBeNull();
|
|
172
|
-
expect(state!.cookies.length).toBeGreaterThan(0);
|
|
173
|
-
|
|
174
|
-
const consentCookie = state!.cookies.find(
|
|
175
|
-
(c) => c.name === "consent" && c.value === "accepted",
|
|
176
|
-
);
|
|
177
|
-
expect(consentCookie).toBeDefined();
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it("third navigate also has no consent dialog (full disk-path round-trip)", async () => {
|
|
181
|
-
// The state saved to disk during the second navigate (test 4's
|
|
182
|
-
// _persistState call) is now available. The in-memory fallback
|
|
183
|
-
// returns it again, and the disk copy exists for the router to load.
|
|
184
|
-
const nav = await plugin.navigate(serverUrl, TASK_ID, NAV_TIMEOUT, {
|
|
185
|
-
profileName: TEST_PROFILE,
|
|
186
|
-
profileMode: "named",
|
|
187
|
-
});
|
|
188
|
-
expect(nav.success).toBe(true);
|
|
189
|
-
|
|
190
|
-
// Dialog should still be absent
|
|
191
|
-
expect(nav.snapshot).not.toContain("Consent");
|
|
192
|
-
expect(nav.snapshot).not.toContain("Accept All");
|
|
193
|
-
});
|
|
28
|
+
runPersistenceSuite(describeIfAvailable, {
|
|
29
|
+
name: "chromium",
|
|
30
|
+
getServerUrl: () => serverUrl,
|
|
31
|
+
createPlugin: async () => {
|
|
32
|
+
const p = new ChromiumPlugin();
|
|
33
|
+
await p.init({});
|
|
34
|
+
return p;
|
|
194
35
|
},
|
|
195
|
-
|
|
196
|
-
);
|
|
36
|
+
});
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
10
|
-
import { webFetch, cleanupFetchTempFiles } from "../core/fetch-backend";
|
|
10
|
+
import { webFetch, cleanupFetchTempFiles } from "../core/fetch-backend.js";
|
|
11
11
|
|
|
12
12
|
// ─── Setup / Teardown ────────────────────────────────────────
|
|
13
13
|
|
|
@@ -1,192 +1,29 @@
|
|
|
1
|
-
|
|
2
|
-
* Firefox-Py Cookie Persistence Integration Test — end-to-end validation
|
|
3
|
-
* of cross-process storage-state flow for the Python adapter / firefox-py bridge.
|
|
4
|
-
*
|
|
5
|
-
* Uses a live Firefox browser via the Firefox-Py bridge against a local
|
|
6
|
-
* HTTP test server that serves a page with a cookie-based consent dialog.
|
|
7
|
-
*
|
|
8
|
-
* Auto-skips the entire suite when Python/firefox-py prerequisites are
|
|
9
|
-
* unavailable.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
13
|
-
import { resolve } from "node:path";
|
|
14
|
-
import { existsSync } from "node:fs";
|
|
15
|
-
import { spawnSync } from "node:child_process";
|
|
16
|
-
|
|
1
|
+
import { describe, beforeAll, afterAll } from "vitest";
|
|
17
2
|
import { PythonPluginAdapter } from "../backends/python-adapter.js";
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
} from "./helpers/test-server.js";
|
|
22
|
-
import {
|
|
23
|
-
loadStorageState,
|
|
24
|
-
deleteStorageState,
|
|
25
|
-
} from "../core/shared/storage-state.js";
|
|
26
|
-
import { sessionManager } from "../core/shared/session-manager.js";
|
|
27
|
-
|
|
28
|
-
// ─── Paths ──────────────────────────────────────────────────────────
|
|
29
|
-
|
|
30
|
-
const BRIDGE_SCRIPT = resolve(__dirname, "../backends/firefox-py/bridge.py");
|
|
31
|
-
const PYTHON_PATH = resolve(
|
|
32
|
-
__dirname,
|
|
33
|
-
"../backends/python-base/.venv/bin/python3",
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
// ─── Prerequisites check — skip gracefully if absent ────────────────
|
|
37
|
-
|
|
38
|
-
const prerequisitesMet = (() => {
|
|
39
|
-
if (!existsSync(PYTHON_PATH)) return false;
|
|
40
|
-
if (!existsSync(BRIDGE_SCRIPT)) return false;
|
|
41
|
-
const result = spawnSync(PYTHON_PATH, ["--version"], {
|
|
42
|
-
stdio: "ignore",
|
|
43
|
-
timeout: 5_000,
|
|
44
|
-
});
|
|
45
|
-
if (result.status !== 0) return false;
|
|
46
|
-
|
|
47
|
-
// Quick check: can we import playwright AND is Firefox installed?
|
|
48
|
-
const check = spawnSync(
|
|
49
|
-
PYTHON_PATH,
|
|
50
|
-
[
|
|
51
|
-
"-c",
|
|
52
|
-
"from playwright.sync_api import sync_playwright; " +
|
|
53
|
-
"p = sync_playwright().start(); " +
|
|
54
|
-
"import os; ok = os.path.exists(p.firefox.executable_path); " +
|
|
55
|
-
"p.stop(); print('ok' if ok else 'no')",
|
|
56
|
-
],
|
|
57
|
-
{ stdio: "pipe", timeout: 10_000 },
|
|
58
|
-
);
|
|
59
|
-
return check.status === 0 && check.stdout.toString().trim() === "ok";
|
|
60
|
-
})();
|
|
61
|
-
|
|
62
|
-
const describeIfAvailable = prerequisitesMet ? describe : describe.skip;
|
|
63
|
-
|
|
64
|
-
// ─── Test Server Setup (always runs, server is cheap to start/stop) ─
|
|
65
|
-
|
|
66
|
-
let serverUrl: string;
|
|
67
|
-
let stopServer: () => Promise<void>;
|
|
3
|
+
import { createPyBackendTestHarness } from "./helpers/create-py-backend-harness.js";
|
|
4
|
+
import { startCookiePersistenceServer } from "./helpers/test-server.js";
|
|
5
|
+
import { runPersistenceSuite } from "./helpers/persistence-suite.js";
|
|
68
6
|
|
|
7
|
+
const { bridgeScript, pythonPath, prerequisitesMet } =
|
|
8
|
+
createPyBackendTestHarness("firefox-py", { engine: "firefox" });
|
|
9
|
+
let serverUrl: string, stopServer: () => Promise<void>;
|
|
69
10
|
beforeAll(async () => {
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
74
|
-
res.end(COOKIE_PERSISTENCE_HTML);
|
|
75
|
-
} else {
|
|
76
|
-
res.writeHead(404, { "Content-Type": "text/html; charset=utf-8" });
|
|
77
|
-
res.end("404");
|
|
78
|
-
}
|
|
79
|
-
});
|
|
80
|
-
serverUrl = server.url;
|
|
81
|
-
stopServer = server.stop;
|
|
11
|
+
const s = await startCookiePersistenceServer();
|
|
12
|
+
serverUrl = s.url;
|
|
13
|
+
stopServer = s.stop;
|
|
82
14
|
});
|
|
83
|
-
|
|
84
15
|
afterAll(async () => {
|
|
85
16
|
await stopServer();
|
|
86
17
|
});
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
() => {
|
|
98
|
-
const TEST_PROFILE = `firefox-py-persist-${Date.now()}`;
|
|
99
|
-
|
|
100
|
-
let plugin: PythonPluginAdapter;
|
|
101
|
-
|
|
102
|
-
beforeAll(async () => {
|
|
103
|
-
plugin = new PythonPluginAdapter("firefox-py-persist", {
|
|
104
|
-
bridgeScript: BRIDGE_SCRIPT,
|
|
105
|
-
pythonPath: PYTHON_PATH,
|
|
106
|
-
});
|
|
107
|
-
await plugin.init();
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
afterAll(async () => {
|
|
111
|
-
await plugin.cleanupAll().catch(() => {});
|
|
112
|
-
deleteStorageState(TEST_PROFILE);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it("navigates with a named profile — consent dialog is visible", async () => {
|
|
116
|
-
const nav = await plugin.navigate(serverUrl, TASK_ID, NAV_TIMEOUT, {
|
|
117
|
-
profileName: TEST_PROFILE,
|
|
118
|
-
profileMode: "named",
|
|
119
|
-
});
|
|
120
|
-
expect(nav.success).toBe(true);
|
|
121
|
-
expect(nav.title).toContain("Cookie Persistence Test");
|
|
122
|
-
expect(nav.snapshot).toContain("Consent");
|
|
123
|
-
expect(nav.snapshot).toContain("Accept All");
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it("clicks Accept All — cookie set, consent dialog closes", async () => {
|
|
127
|
-
const snap = await plugin.snapshot(TASK_ID);
|
|
128
|
-
expect(snap.success).toBe(true);
|
|
129
|
-
|
|
130
|
-
// Find "Accept All" button in the snapshot and extract its @e ref
|
|
131
|
-
const btnMatch = snap.snapshot.match(/@(e\d+)\b.*?button.*?Accept All/);
|
|
132
|
-
expect(btnMatch).toBeTruthy();
|
|
133
|
-
if (!btnMatch) return;
|
|
134
|
-
|
|
135
|
-
const ref = `@${btnMatch[1]!}`;
|
|
136
|
-
const clickResult = await plugin.click(TASK_ID, ref);
|
|
137
|
-
expect(clickResult.success).toBe(true);
|
|
138
|
-
|
|
139
|
-
// Verify dialog is gone from snapshot
|
|
140
|
-
if (clickResult.snapshot) {
|
|
141
|
-
expect(clickResult.snapshot).not.toContain("Consent");
|
|
142
|
-
}
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it("creates session and sets persistState (simulating the router's setup)", async () => {
|
|
146
|
-
sessionManager.createSession(TASK_ID, "firefox-py-persist");
|
|
147
|
-
const session = sessionManager.getSession(TASK_ID);
|
|
148
|
-
expect(session).toBeDefined();
|
|
149
|
-
if (session) {
|
|
150
|
-
session.persistState = true;
|
|
151
|
-
session.profileName = TEST_PROFILE;
|
|
152
|
-
}
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it("navigates again with same profile — consent dialog does NOT reappear", async () => {
|
|
156
|
-
const nav = await plugin.navigate(serverUrl, TASK_ID, NAV_TIMEOUT, {
|
|
157
|
-
profileName: TEST_PROFILE,
|
|
158
|
-
profileMode: "named",
|
|
159
|
-
});
|
|
160
|
-
expect(nav.success).toBe(true);
|
|
161
|
-
|
|
162
|
-
// Consent dialog should NOT be present — cookies survived in-context
|
|
163
|
-
expect(nav.snapshot).not.toContain("Consent");
|
|
164
|
-
expect(nav.snapshot).not.toContain("Accept All");
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it("storage-state.json exists on disk with the consent cookie", async () => {
|
|
168
|
-
const state = loadStorageState(TEST_PROFILE);
|
|
169
|
-
expect(state).not.toBeNull();
|
|
170
|
-
expect(state!.cookies.length).toBeGreaterThan(0);
|
|
171
|
-
|
|
172
|
-
const consentCookie = state!.cookies.find(
|
|
173
|
-
(c: { name: string; value: string }) =>
|
|
174
|
-
c.name === "consent" && c.value === "accepted",
|
|
175
|
-
);
|
|
176
|
-
expect(consentCookie).toBeDefined();
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it("third navigate also has no consent dialog (persistence confirmed)", async () => {
|
|
180
|
-
const nav = await plugin.navigate(serverUrl, TASK_ID, NAV_TIMEOUT, {
|
|
181
|
-
profileName: TEST_PROFILE,
|
|
182
|
-
profileMode: "named",
|
|
183
|
-
});
|
|
184
|
-
expect(nav.success).toBe(true);
|
|
185
|
-
|
|
186
|
-
// Dialog should still be absent
|
|
187
|
-
expect(nav.snapshot).not.toContain("Consent");
|
|
188
|
-
expect(nav.snapshot).not.toContain("Accept All");
|
|
189
|
-
});
|
|
18
|
+
runPersistenceSuite(prerequisitesMet ? describe : describe.skip, {
|
|
19
|
+
name: "firefox-py",
|
|
20
|
+
getServerUrl: () => serverUrl,
|
|
21
|
+
createPlugin: async () => {
|
|
22
|
+
const p = new PythonPluginAdapter("firefox-py-persist", {
|
|
23
|
+
bridgeScript,
|
|
24
|
+
pythonPath,
|
|
25
|
+
});
|
|
26
|
+
await p.init();
|
|
27
|
+
return p;
|
|
190
28
|
},
|
|
191
|
-
|
|
192
|
-
);
|
|
29
|
+
});
|
|
@@ -1,107 +1,23 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Contract tests for the Firefox-Py backend — validates that the
|
|
3
|
-
* PythonPluginAdapter + Firefox-Py bridge satisfies the BrowserPlugin
|
|
4
|
-
* contract against a real browser.
|
|
5
|
-
*
|
|
6
|
-
* This test file instantiates a PythonPluginAdapter pointing at the
|
|
7
|
-
* Firefox-Py bridge script and runs both structural and behavioral
|
|
8
|
-
* contract tests. The Python subprocess is managed by the adapter;
|
|
9
|
-
* no manual setup is needed beyond having Playwright Firefox installed
|
|
10
|
-
* in the Python virtual environment.
|
|
11
|
-
*
|
|
12
|
-
* Prerequisites:
|
|
13
|
-
* - Python venv at backends/python-base/.venv with playwright installed
|
|
14
|
-
* - Playwright Firefox browsers installed (playwright install firefox)
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { resolve } from "node:path";
|
|
18
|
-
import { existsSync } from "node:fs";
|
|
19
|
-
import { spawnSync } from "node:child_process";
|
|
20
|
-
|
|
21
1
|
import { PythonPluginAdapter } from "../backends/python-adapter.js";
|
|
22
2
|
import { runContractTests } from "./helpers/plugin-contract.js";
|
|
3
|
+
import { createPyBackendTestHarness } from "./helpers/create-py-backend-harness.js";
|
|
23
4
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const BRIDGE_SCRIPT = resolve(__dirname, "../backends/firefox-py/bridge.py");
|
|
27
|
-
|
|
28
|
-
const PYTHON_PATH = resolve(
|
|
29
|
-
__dirname,
|
|
30
|
-
"../backends/python-base/.venv/bin/python3",
|
|
31
|
-
);
|
|
32
|
-
|
|
33
|
-
// ─── Environment check ───────────────────────────────────────────────
|
|
34
|
-
|
|
35
|
-
const pythonAvailable = (() => {
|
|
36
|
-
if (!existsSync(PYTHON_PATH)) return false;
|
|
37
|
-
const result = spawnSync(PYTHON_PATH, ["--version"], {
|
|
38
|
-
stdio: "ignore",
|
|
39
|
-
timeout: 5_000,
|
|
40
|
-
});
|
|
41
|
-
return result.status === 0;
|
|
42
|
-
})();
|
|
43
|
-
|
|
44
|
-
const bridgeExists = existsSync(BRIDGE_SCRIPT);
|
|
45
|
-
|
|
46
|
-
const firefoxAvailable = (() => {
|
|
47
|
-
if (!PYTHON_PATH) return false;
|
|
48
|
-
try {
|
|
49
|
-
const result = spawnSync(
|
|
50
|
-
PYTHON_PATH,
|
|
51
|
-
[
|
|
52
|
-
"-c",
|
|
53
|
-
"from playwright.sync_api import sync_playwright; " +
|
|
54
|
-
"p = sync_playwright().start(); " +
|
|
55
|
-
"import os; print(os.path.exists(p.firefox.executable_path)); " +
|
|
56
|
-
"p.stop()",
|
|
57
|
-
],
|
|
58
|
-
{ stdio: "pipe", timeout: 10_000 },
|
|
59
|
-
);
|
|
60
|
-
return result.status === 0 && result.stdout.toString().trim() === "True";
|
|
61
|
-
} catch {
|
|
62
|
-
return false;
|
|
63
|
-
}
|
|
64
|
-
})();
|
|
65
|
-
|
|
66
|
-
// ─── Plugin factory ─────────────────────────────────────────────────
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Create a PythonPluginAdapter that drives the Firefox-Py bridge.
|
|
70
|
-
*
|
|
71
|
-
* The adapter is lazy — the Python subprocess won't start until the
|
|
72
|
-
* first operation. We pass the venv Python path so the bridge has
|
|
73
|
-
* access to playwright and the shared pi_browser_bridge library.
|
|
74
|
-
*/
|
|
75
|
-
function createFirefoxPyPlugin(): PythonPluginAdapter {
|
|
76
|
-
return new PythonPluginAdapter("firefox-py", {
|
|
77
|
-
bridgeScript: BRIDGE_SCRIPT,
|
|
78
|
-
pythonPath: PYTHON_PATH,
|
|
79
|
-
// Firefox-Py supports everything except AbortSignal
|
|
80
|
-
capabilities: {
|
|
81
|
-
supportsFullPageScreenshot: true,
|
|
82
|
-
supportsConsoleCapture: true,
|
|
83
|
-
supportsJavaScriptEvaluate: true,
|
|
84
|
-
supportsBotDetection: true,
|
|
85
|
-
supportsDialogAutoDismissal: true,
|
|
86
|
-
supportsAbortSignal: false,
|
|
87
|
-
engine: "firefox",
|
|
88
|
-
},
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// ─── Run contract tests ──────────────────────────────────────────────
|
|
93
|
-
|
|
94
|
-
// Skip the entire suite if prerequisites aren't met (CI without
|
|
95
|
-
// Playwright installed, local dev without venv, etc.)
|
|
96
|
-
const describeIfAvailable =
|
|
97
|
-
pythonAvailable && bridgeExists && firefoxAvailable
|
|
98
|
-
? describe
|
|
99
|
-
: describe.skip;
|
|
5
|
+
const { bridgeScript, pythonPath, describeIfAvailable } =
|
|
6
|
+
createPyBackendTestHarness("firefox-py", { engine: "firefox" });
|
|
100
7
|
|
|
101
8
|
describeIfAvailable("Firefox-Py contract tests", () => {
|
|
102
|
-
runContractTests(
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
9
|
+
runContractTests(
|
|
10
|
+
"firefox-py",
|
|
11
|
+
() =>
|
|
12
|
+
new PythonPluginAdapter("firefox-py", {
|
|
13
|
+
bridgeScript,
|
|
14
|
+
pythonPath,
|
|
15
|
+
capabilities: {
|
|
16
|
+
supportsFullPageScreenshot: true,
|
|
17
|
+
supportsJavaScriptEvaluate: true,
|
|
18
|
+
engine: "firefox",
|
|
19
|
+
},
|
|
20
|
+
}),
|
|
21
|
+
{ realBrowser: true, navigateTimeout: 30_000, navigationSettle: true },
|
|
22
|
+
);
|
|
107
23
|
});
|
|
@@ -1,28 +1,12 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* FirefoxPlugin contract tests.
|
|
3
|
-
*
|
|
4
|
-
* Runs the shared `runContractTests` suite against the Node Firefox backend.
|
|
5
|
-
* Skips automatically when Playwright Firefox is unavailable (no browser installed).
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
1
|
import { existsSync } from "node:fs";
|
|
9
2
|
import { firefox } from "playwright";
|
|
10
3
|
import { describe } from "vitest";
|
|
11
4
|
import { runContractTests } from "./helpers/plugin-contract.js";
|
|
12
5
|
import { FirefoxPlugin } from "../backends/firefox/index.js";
|
|
13
6
|
|
|
14
|
-
function createFirefoxPlugin(): FirefoxPlugin {
|
|
15
|
-
return new FirefoxPlugin();
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// Skip the entire suite if Playwright Firefox isn't installed.
|
|
19
|
-
// We must check the file exists on disk — executablePath() returns a
|
|
20
|
-
// path even when the browser hasn't been installed.
|
|
21
7
|
const describeIfAvailable = (() => {
|
|
22
8
|
try {
|
|
23
|
-
|
|
24
|
-
if (!existsSync(ffPath)) return describe.skip;
|
|
25
|
-
return describe;
|
|
9
|
+
return existsSync(firefox.executablePath()) ? describe : describe.skip;
|
|
26
10
|
} catch {
|
|
27
11
|
return describe.skip;
|
|
28
12
|
}
|
|
@@ -31,7 +15,7 @@ const describeIfAvailable = (() => {
|
|
|
31
15
|
describeIfAvailable(
|
|
32
16
|
"FirefoxPlugin contract tests",
|
|
33
17
|
() => {
|
|
34
|
-
runContractTests("firefox",
|
|
18
|
+
runContractTests("firefox", () => new FirefoxPlugin(), {
|
|
35
19
|
realBrowser: true,
|
|
36
20
|
navigateTimeout: 30_000,
|
|
37
21
|
navigationSettle: true,
|