pursor 0.2.0 → 0.3.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 +654 -595
- package/bin/pursor.mjs +226 -190
- package/package.json +75 -73
- package/src/auth.js +92 -0
- package/src/har.js +159 -0
- package/src/index.js +94 -89
- package/src/runway.js +5 -2
- package/src/shoot.js +13 -1
- package/src/sweep.js +118 -104
package/src/auth.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// pursor — auth state (browser storage state) management.
|
|
2
|
+
//
|
|
3
|
+
// Playwright's `storageState` is the canonical way to persist
|
|
4
|
+
// cookies + localStorage between browser sessions. pursor wraps it
|
|
5
|
+
// in a small CLI/library API so users can:
|
|
6
|
+
// 1. login once interactively and save the state
|
|
7
|
+
// 2. reuse the saved state in any subsequent capture (CI included)
|
|
8
|
+
//
|
|
9
|
+
// Storage layout:
|
|
10
|
+
// ~/.pursor/auth/<project>/<name>.json
|
|
11
|
+
//
|
|
12
|
+
// Override with PURSOR_AUTH_DIR.
|
|
13
|
+
//
|
|
14
|
+
// Public API:
|
|
15
|
+
// saveAuthState({ project, name, state }) -> manifest + state file
|
|
16
|
+
// loadAuthState({ project, name }) -> state object (Playwright shape)
|
|
17
|
+
// listAuthStates(project) -> [{ name, ts, ... }]
|
|
18
|
+
// deleteAuthState({ project, name }) -> bool
|
|
19
|
+
//
|
|
20
|
+
// CLI:
|
|
21
|
+
// pursor auth save <project> <name> --from <state.json>
|
|
22
|
+
// pursor auth load <project> <name> --out <state.json>
|
|
23
|
+
// pursor auth list [project]
|
|
24
|
+
// pursor auth delete <project> <name>
|
|
25
|
+
|
|
26
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSync, rmSync } from "node:fs";
|
|
27
|
+
import { join } from "node:path";
|
|
28
|
+
import { homedir } from "node:os";
|
|
29
|
+
import { nowIso } from "./util.js";
|
|
30
|
+
|
|
31
|
+
function authRoot() {
|
|
32
|
+
return process.env.PURSOR_AUTH_DIR || join(homedir(), ".pursor", "auth");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function authPath(project, name) {
|
|
36
|
+
const root = authRoot();
|
|
37
|
+
const proj = (project || "default").replace(/[^a-zA-Z0-9._-]+/g, "_");
|
|
38
|
+
const nm = String(name || "default").replace(/[^a-zA-Z0-9._-]+/g, "_");
|
|
39
|
+
return join(root, proj, `${nm}.json`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function saveAuthState({ project, name, state }) {
|
|
43
|
+
if (!state) throw new Error("saveAuthState: missing state object");
|
|
44
|
+
// state shape from Playwright: { cookies, origins }
|
|
45
|
+
if (!Array.isArray(state.cookies)) state.cookies = [];
|
|
46
|
+
if (!Array.isArray(state.origins)) state.origins = [];
|
|
47
|
+
const file = authPath(project, name);
|
|
48
|
+
mkdirSync(join(file, ".."), { recursive: true });
|
|
49
|
+
const blob = {
|
|
50
|
+
_meta: { project: project || "default", name, ts: nowIso() },
|
|
51
|
+
cookies: state.cookies,
|
|
52
|
+
origins: state.origins,
|
|
53
|
+
};
|
|
54
|
+
writeFileSync(file, JSON.stringify(blob, null, 2), "utf8");
|
|
55
|
+
return { file, ...blob._meta };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function loadAuthState({ project, name }) {
|
|
59
|
+
const file = authPath(project, name);
|
|
60
|
+
if (!existsSync(file)) return null;
|
|
61
|
+
try {
|
|
62
|
+
const blob = JSON.parse(readFileSync(file, "utf8"));
|
|
63
|
+
return { cookies: blob.cookies || [], origins: blob.origins || [] };
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function listAuthStates(project) {
|
|
70
|
+
const root = join(authRoot(), (project || "default").replace(/[^a-zA-Z0-9._-]+/g, "_"));
|
|
71
|
+
if (!existsSync(root)) return [];
|
|
72
|
+
const out = [];
|
|
73
|
+
for (const f of readdirSync(root)) {
|
|
74
|
+
if (!f.endsWith(".json")) continue;
|
|
75
|
+
try {
|
|
76
|
+
const blob = JSON.parse(readFileSync(join(root, f), "utf8"));
|
|
77
|
+
out.push({
|
|
78
|
+
name: blob?._meta?.name || f.replace(/\.json$/, ""),
|
|
79
|
+
ts: blob?._meta?.ts || null,
|
|
80
|
+
cookies: (blob.cookies || []).length,
|
|
81
|
+
origins: (blob.origins || []).length,
|
|
82
|
+
});
|
|
83
|
+
} catch {}
|
|
84
|
+
}
|
|
85
|
+
return out.sort((a, b) => (b.ts || "").localeCompare(a.ts || ""));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function deleteAuthState({ project, name }) {
|
|
89
|
+
const file = authPath(project, name);
|
|
90
|
+
if (!existsSync(file)) return false;
|
|
91
|
+
try { rmSync(file, { force: true }); return true; } catch { return false; }
|
|
92
|
+
}
|
package/src/har.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// pursor — HAR (HTTP Archive) capture.
|
|
2
|
+
//
|
|
3
|
+
// Hooks page.on("request") / page.on("response") / page.on("requestfailed")
|
|
4
|
+
// on an active page and produces a HAR 1.2 spec blob:
|
|
5
|
+
// { log: { version: "1.2", creator: {...}, browser: {...}, pages: [...], entries: [...] } }
|
|
6
|
+
//
|
|
7
|
+
// Use in code:
|
|
8
|
+
// const { startHarCapture, stopHarCapture } = await import("pursor/har");
|
|
9
|
+
// const har = await startHarCapture(page);
|
|
10
|
+
// await page.goto(url);
|
|
11
|
+
// await stopHarCapture(page); // returns the HAR object
|
|
12
|
+
//
|
|
13
|
+
// Or via CLI/sweep:
|
|
14
|
+
// pursor shoot <url> --har ./out/req.har.json
|
|
15
|
+
//
|
|
16
|
+
// HAR is useful for:
|
|
17
|
+
// - killing flakiness from analytics/ads/CDN by mocking responses later
|
|
18
|
+
// - inspecting what the page actually fetched during a capture
|
|
19
|
+
// - regression diffing along with visual diffs
|
|
20
|
+
|
|
21
|
+
import { writeFileSync, mkdirSync } from "node:fs";
|
|
22
|
+
import { dirname } from "node:path";
|
|
23
|
+
import { nowIso } from "./util.js";
|
|
24
|
+
|
|
25
|
+
const SYM = Symbol.for("pursor.har.capture");
|
|
26
|
+
|
|
27
|
+
function makeEntry(req, resp, startTs, endTs) {
|
|
28
|
+
const url = req.url();
|
|
29
|
+
const u = new URL(url);
|
|
30
|
+
const headersList = (obj) => {
|
|
31
|
+
if (!obj) return [];
|
|
32
|
+
try {
|
|
33
|
+
const out = [];
|
|
34
|
+
for (const [name, value] of Object.entries(obj)) {
|
|
35
|
+
if (Array.isArray(value)) {
|
|
36
|
+
for (const v of value) out.push({ name, value: String(v) });
|
|
37
|
+
} else {
|
|
38
|
+
out.push({ name, value: String(value) });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
} catch { return []; }
|
|
43
|
+
};
|
|
44
|
+
const queryString = u.search ? u.search.slice(1).split("&").filter(Boolean).map(kv => {
|
|
45
|
+
const [k, v = ""] = kv.split("=");
|
|
46
|
+
return { name: decodeURIComponent(k), value: decodeURIComponent(v) };
|
|
47
|
+
}) : [];
|
|
48
|
+
const postData = req.postData() ? { mimeType: req.postData() || "application/octet-stream", text: req.postData() } : undefined;
|
|
49
|
+
const entry = {
|
|
50
|
+
pageref: req.frame()?.url?.() || "_top",
|
|
51
|
+
startedDateTime: new Date(startTs).toISOString(),
|
|
52
|
+
time: Math.max(0, endTs - startTs),
|
|
53
|
+
request: {
|
|
54
|
+
method: req.method(),
|
|
55
|
+
url,
|
|
56
|
+
httpVersion: "HTTP/1.1",
|
|
57
|
+
cookies: [],
|
|
58
|
+
headers: headersList(req.headers()),
|
|
59
|
+
queryString,
|
|
60
|
+
headersSize: -1,
|
|
61
|
+
bodySize: postData?.text?.length || 0,
|
|
62
|
+
postData,
|
|
63
|
+
},
|
|
64
|
+
response: resp ? {
|
|
65
|
+
status: resp.status(),
|
|
66
|
+
statusText: resp.statusText() || "",
|
|
67
|
+
httpVersion: "HTTP/1.1",
|
|
68
|
+
cookies: [],
|
|
69
|
+
headers: headersList(resp.headers()),
|
|
70
|
+
content: { size: -1, mimeType: resp.headers()?.["content-type"] || "" },
|
|
71
|
+
redirectURL: resp.headers()?.location || "",
|
|
72
|
+
headersSize: -1,
|
|
73
|
+
bodySize: -1,
|
|
74
|
+
} : { status: 0, statusText: "Failed", httpVersion: "HTTP/1.1", cookies: [], headers: [], content: { size: 0, mimeType: "" }, redirectURL: "", headersSize: -1, bodySize: -1 },
|
|
75
|
+
cache: {},
|
|
76
|
+
timings: { send: 0, wait: Math.max(0, endTs - startTs), receive: 0 },
|
|
77
|
+
serverIPAddress: "",
|
|
78
|
+
connection: "",
|
|
79
|
+
};
|
|
80
|
+
return entry;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function startHarCapture(page, opts = {}) {
|
|
84
|
+
if (!page) throw new Error("startHarCapture: page required");
|
|
85
|
+
if (page[SYM]) return page[SYM]; // idempotent
|
|
86
|
+
const started = Date.now();
|
|
87
|
+
const entries = [];
|
|
88
|
+
const pending = new Map(); // req -> startTs
|
|
89
|
+
const state = {
|
|
90
|
+
started,
|
|
91
|
+
creator: { name: "pursor", version: opts.version || "0.3.0" },
|
|
92
|
+
browser: { name: "chromium", version: "playwright-core" },
|
|
93
|
+
pages: [],
|
|
94
|
+
entries,
|
|
95
|
+
pending,
|
|
96
|
+
};
|
|
97
|
+
const onReq = (req) => {
|
|
98
|
+
pending.set(req, Date.now());
|
|
99
|
+
};
|
|
100
|
+
const onResp = async (resp) => {
|
|
101
|
+
try {
|
|
102
|
+
const req = resp.request();
|
|
103
|
+
const startTs = pending.get(req) || Date.now();
|
|
104
|
+
pending.delete(req);
|
|
105
|
+
const endTs = Date.now();
|
|
106
|
+
entries.push(makeEntry(req, resp, startTs, endTs));
|
|
107
|
+
} catch {}
|
|
108
|
+
};
|
|
109
|
+
const onFailed = (req) => {
|
|
110
|
+
try {
|
|
111
|
+
const startTs = pending.get(req) || Date.now();
|
|
112
|
+
pending.delete(req);
|
|
113
|
+
entries.push(makeEntry(req, null, startTs, Date.now()));
|
|
114
|
+
} catch {}
|
|
115
|
+
};
|
|
116
|
+
page.on("request", onReq);
|
|
117
|
+
page.on("response", onResp);
|
|
118
|
+
page.on("requestfailed", onFailed);
|
|
119
|
+
state._teardown = () => {
|
|
120
|
+
try { page.off("request", onReq); } catch {}
|
|
121
|
+
try { page.off("response", onResp); } catch {}
|
|
122
|
+
try { page.off("requestfailed", onFailed); } catch {}
|
|
123
|
+
};
|
|
124
|
+
page[SYM] = state;
|
|
125
|
+
return state;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function stopHarCapture(page) {
|
|
129
|
+
if (!page || !page[SYM]) return null;
|
|
130
|
+
const state = page[SYM];
|
|
131
|
+
try { state._teardown?.(); } catch {}
|
|
132
|
+
delete page[SYM];
|
|
133
|
+
return finalizeHar(state);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function finalizeHar(state) {
|
|
137
|
+
if (!state) return null;
|
|
138
|
+
return {
|
|
139
|
+
log: {
|
|
140
|
+
version: "1.2",
|
|
141
|
+
creator: state.creator,
|
|
142
|
+
browser: state.browser,
|
|
143
|
+
pages: state.pages,
|
|
144
|
+
entries: state.entries,
|
|
145
|
+
},
|
|
146
|
+
_meta: {
|
|
147
|
+
started: state.started,
|
|
148
|
+
finished: Date.now(),
|
|
149
|
+
entryCount: state.entries.length,
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function writeHar(har, file) {
|
|
155
|
+
if (!har || !file) return null;
|
|
156
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
157
|
+
writeFileSync(file, JSON.stringify(har, null, 2), "utf8");
|
|
158
|
+
return file;
|
|
159
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,90 +1,95 @@
|
|
|
1
|
-
// pursor — public library API.
|
|
2
|
-
//
|
|
3
|
-
// This is the entry point for consumers who want to embed pursor
|
|
4
|
-
// inside their own scripts (instead of using the CLI). The CLI in
|
|
5
|
-
// bin/pursor.mjs is a thin wrapper around the same exports.
|
|
6
|
-
//
|
|
7
|
-
// All capture / sweep helpers return a `Result` object: the path to the
|
|
8
|
-
// PNG, a sidecar JSON metadata object, and timing info. They never throw
|
|
9
|
-
// on capture-time errors — those are reported in the result so the
|
|
10
|
-
// caller can decide how to react.
|
|
11
|
-
//
|
|
12
|
-
// The plugin system lives in src/plugin.js — see that file for how to
|
|
13
|
-
// write a custom plugin (viewport, sweep-op, before/after hooks).
|
|
14
|
-
|
|
15
|
-
import { runShoot } from "./shoot.js";
|
|
16
|
-
import { runSweep } from "./sweep.js";
|
|
17
|
-
import { runEveryViewport } from "./every-viewport.js";
|
|
18
|
-
import { runFrames } from "./frames.js";
|
|
19
|
-
import { runHover } from "./hover.js";
|
|
20
|
-
import { runDiff } from "./diff.js";
|
|
21
|
-
import { runProbe } from "./probe.js";
|
|
22
|
-
import { runShot } from "./shot.js";
|
|
23
|
-
import { runEval } from "./eval.js";
|
|
24
|
-
import { runClick, runType, runWait, runSeq } from "./interact.js";
|
|
25
|
-
import { listViewports, resolveViewport, VIEWPORTS } from "./viewport.js";
|
|
26
|
-
import { applyCamera, waitForStableFrame } from "./overlays.js";
|
|
27
|
-
import { loadPlugins, registerPlugin, listPlugins, getSweepOp, getViewportPreset, listViewportPresets, getFlagHelp } from "./plugin.js";
|
|
28
|
-
import { launch, newPage } from "./runway.js";
|
|
29
|
-
import { parseFlags, asNum, asBool, nowIso, shortHash, escapeHtml, renderSweepHtml, renderEveryViewportHtml, findStepPng, readArg, makeOut } from "./util.js";
|
|
30
|
-
import { resolveLocator, parseTextSelector } from "./selector.js";
|
|
31
|
-
import { captureDomSnapshot, captureDomSnapshotSidecar } from "./dom-snapshot.js";
|
|
32
|
-
import { runAudit } from "./plugin-audit.js";
|
|
33
|
-
import { resolveHealedSelector, healStepAction } from "./selector-heal.js";
|
|
34
|
-
import { writeCiOutput } from "./ci-output.js";
|
|
35
|
-
import { PursorMCPServer, loadConfig as loadMcpConfig, MCP_VERSION } from "./mcp.js";
|
|
36
|
-
import { createRequire } from "node:module";
|
|
37
|
-
import { saveBaseline, loadBaseline, listBaselines, approveBaseline, diffKey, resolveBaselinePath } from "./baseline.js";
|
|
38
|
-
import { validateSweepPlan, registerSweepOp } from "./sweep-schema.js";
|
|
39
|
-
import { listResources, readResource, recordResource } from "./mcp-resources.js";
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
1
|
+
// pursor — public library API.
|
|
2
|
+
//
|
|
3
|
+
// This is the entry point for consumers who want to embed pursor
|
|
4
|
+
// inside their own scripts (instead of using the CLI). The CLI in
|
|
5
|
+
// bin/pursor.mjs is a thin wrapper around the same exports.
|
|
6
|
+
//
|
|
7
|
+
// All capture / sweep helpers return a `Result` object: the path to the
|
|
8
|
+
// PNG, a sidecar JSON metadata object, and timing info. They never throw
|
|
9
|
+
// on capture-time errors — those are reported in the result so the
|
|
10
|
+
// caller can decide how to react.
|
|
11
|
+
//
|
|
12
|
+
// The plugin system lives in src/plugin.js — see that file for how to
|
|
13
|
+
// write a custom plugin (viewport, sweep-op, before/after hooks).
|
|
14
|
+
|
|
15
|
+
import { runShoot } from "./shoot.js";
|
|
16
|
+
import { runSweep } from "./sweep.js";
|
|
17
|
+
import { runEveryViewport } from "./every-viewport.js";
|
|
18
|
+
import { runFrames } from "./frames.js";
|
|
19
|
+
import { runHover } from "./hover.js";
|
|
20
|
+
import { runDiff } from "./diff.js";
|
|
21
|
+
import { runProbe } from "./probe.js";
|
|
22
|
+
import { runShot } from "./shot.js";
|
|
23
|
+
import { runEval } from "./eval.js";
|
|
24
|
+
import { runClick, runType, runWait, runSeq } from "./interact.js";
|
|
25
|
+
import { listViewports, resolveViewport, VIEWPORTS } from "./viewport.js";
|
|
26
|
+
import { applyCamera, waitForStableFrame } from "./overlays.js";
|
|
27
|
+
import { loadPlugins, registerPlugin, listPlugins, getSweepOp, getViewportPreset, listViewportPresets, getFlagHelp } from "./plugin.js";
|
|
28
|
+
import { launch, newPage } from "./runway.js";
|
|
29
|
+
import { parseFlags, asNum, asBool, nowIso, shortHash, escapeHtml, renderSweepHtml, renderEveryViewportHtml, findStepPng, readArg, makeOut } from "./util.js";
|
|
30
|
+
import { resolveLocator, parseTextSelector } from "./selector.js";
|
|
31
|
+
import { captureDomSnapshot, captureDomSnapshotSidecar } from "./dom-snapshot.js";
|
|
32
|
+
import { runAudit } from "./plugin-audit.js";
|
|
33
|
+
import { resolveHealedSelector, healStepAction } from "./selector-heal.js";
|
|
34
|
+
import { writeCiOutput } from "./ci-output.js";
|
|
35
|
+
import { PursorMCPServer, loadConfig as loadMcpConfig, MCP_VERSION } from "./mcp.js";
|
|
36
|
+
import { createRequire } from "node:module";
|
|
37
|
+
import { saveBaseline, loadBaseline, listBaselines, approveBaseline, diffKey, resolveBaselinePath } from "./baseline.js";
|
|
38
|
+
import { validateSweepPlan, registerSweepOp } from "./sweep-schema.js";
|
|
39
|
+
import { listResources, readResource, recordResource } from "./mcp-resources.js";
|
|
40
|
+
import { startHarCapture, stopHarCapture, writeHar } from "./har.js";
|
|
41
|
+
import { saveAuthState, loadAuthState, listAuthStates, deleteAuthState } from "./auth.js";
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
// Derive VERSION from package.json to prevent drift
|
|
45
|
+
const __require = createRequire(import.meta.url);
|
|
46
|
+
const pkg = __require("../package.json");
|
|
47
|
+
const VERSION = pkg.version;
|
|
48
|
+
|
|
49
|
+
export {
|
|
50
|
+
// CLI-style actions
|
|
51
|
+
runProbe, runShot, runEval, runClick, runType, runWait, runSeq,
|
|
52
|
+
runShoot, runFrames, runHover, runSweep, runDiff, runEveryViewport,
|
|
53
|
+
// v3: audit, DOM snapshot, MCP
|
|
54
|
+
runAudit, captureDomSnapshot, captureDomSnapshotSidecar,
|
|
55
|
+
// viewport + camera helpers
|
|
56
|
+
listViewports, resolveViewport, VIEWPORTS,
|
|
57
|
+
applyCamera, waitForStableFrame,
|
|
58
|
+
// plugin system
|
|
59
|
+
loadPlugins, registerPlugin, listPlugins, getSweepOp, getViewportPreset, listViewportPresets, getFlagHelp,
|
|
60
|
+
// low-level helpers (for plugin authors)
|
|
61
|
+
launch, newPage,
|
|
62
|
+
parseFlags, asNum, asBool, nowIso, shortHash, escapeHtml, renderSweepHtml, renderEveryViewportHtml, findStepPng, readArg, makeOut,
|
|
63
|
+
resolveLocator, parseTextSelector,
|
|
64
|
+
// v3: selector healing, CI output, MCP server
|
|
65
|
+
resolveHealedSelector, healStepAction,
|
|
66
|
+
writeCiOutput,
|
|
67
|
+
PursorMCPServer, loadMcpConfig, MCP_VERSION,
|
|
68
|
+
// v4: baselines, sweep validation, MCP resources
|
|
69
|
+
saveBaseline, loadBaseline, listBaselines, approveBaseline, diffKey, resolveBaselinePath,
|
|
70
|
+
validateSweepPlan, registerSweepOp,
|
|
71
|
+
listResources, readResource, recordResource,
|
|
72
|
+
// v5: HAR capture, auth state, parallel sweep
|
|
73
|
+
startHarCapture, stopHarCapture, writeHar,
|
|
74
|
+
saveAuthState, loadAuthState, listAuthStates, deleteAuthState,
|
|
75
|
+
VERSION,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export default {
|
|
79
|
+
runProbe, runShot, runEval, runClick, runType, runWait, runSeq,
|
|
80
|
+
runShoot, runFrames, runHover, runSweep, runDiff, runEveryViewport,
|
|
81
|
+
runAudit, captureDomSnapshot, captureDomSnapshotSidecar,
|
|
82
|
+
listViewports, resolveViewport, VIEWPORTS,
|
|
83
|
+
applyCamera, waitForStableFrame,
|
|
84
|
+
loadPlugins, registerPlugin, listPlugins, getSweepOp, getViewportPreset, listViewportPresets, getFlagHelp,
|
|
85
|
+
launch, newPage,
|
|
86
|
+
parseFlags, asNum, asBool, nowIso, shortHash, escapeHtml, renderSweepHtml, renderEveryViewportHtml, findStepPng, readArg, makeOut,
|
|
87
|
+
resolveLocator, parseTextSelector,
|
|
88
|
+
resolveHealedSelector, healStepAction,
|
|
89
|
+
writeCiOutput,
|
|
90
|
+
PursorMCPServer, loadMcpConfig, MCP_VERSION,
|
|
91
|
+
saveBaseline, loadBaseline, listBaselines, approveBaseline, diffKey, resolveBaselinePath,
|
|
92
|
+
validateSweepPlan, registerSweepOp,
|
|
93
|
+
listResources, readResource, recordResource,
|
|
94
|
+
VERSION,
|
|
90
95
|
};
|
package/src/runway.js
CHANGED
|
@@ -50,7 +50,7 @@ export async function launch() {
|
|
|
50
50
|
return await chromium.launch({ headless: true, executablePath: exec, args: BROWSER_ARGS });
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
export async function newPage(browser, viewport) {
|
|
53
|
+
export async function newPage(browser, viewport, opts = {}) {
|
|
54
54
|
const ctx = await browser.newContext({
|
|
55
55
|
viewport: { width: viewport.width, height: viewport.height },
|
|
56
56
|
deviceScaleFactor: viewport.dpr || 1,
|
|
@@ -58,6 +58,9 @@ export async function newPage(browser, viewport) {
|
|
|
58
58
|
colorScheme: "light",
|
|
59
59
|
hasTouch: !!(viewport.name && viewport.name.startsWith("mobile")),
|
|
60
60
|
isMobile: !!(viewport.name && viewport.name.startsWith("mobile")),
|
|
61
|
+
storageState: opts.storageState || undefined,
|
|
61
62
|
});
|
|
62
|
-
|
|
63
|
+
const page = await ctx.newPage();
|
|
64
|
+
page._pursorContext = ctx;
|
|
65
|
+
return page;
|
|
63
66
|
}
|
package/src/shoot.js
CHANGED
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
} from "./overlays.js";
|
|
10
10
|
import { asNum, asBool, nowIso, writeSidecar, requireArg } from "./util.js";
|
|
11
11
|
import { runBeforeShoot, runAfterShoot } from "./plugin.js";
|
|
12
|
+
import { startHarCapture, stopHarCapture, writeHar } from "./har.js";
|
|
13
|
+
import { loadAuthState } from "./auth.js";
|
|
12
14
|
|
|
13
15
|
export async function runShoot({ url, out, flags = {}, prepare, browser: extBrowser }) {
|
|
14
16
|
requireArg("url", url, "string");
|
|
@@ -18,7 +20,9 @@ export async function runShoot({ url, out, flags = {}, prepare, browser: extBrow
|
|
|
18
20
|
const cleanups = [];
|
|
19
21
|
try {
|
|
20
22
|
return await (async () => {
|
|
21
|
-
const page = await newPage(browser, viewport
|
|
23
|
+
const page = await newPage(browser, viewport, {
|
|
24
|
+
storageState: flags["auth-state"] ? loadAuthState({ project: flags["auth-project"] || "default", name: flags["auth-state"] }) : undefined,
|
|
25
|
+
});
|
|
22
26
|
const r = await gotoOrThrow(page, url);
|
|
23
27
|
await settle(page);
|
|
24
28
|
|
|
@@ -41,8 +45,16 @@ export async function runShoot({ url, out, flags = {}, prepare, browser: extBrow
|
|
|
41
45
|
await page.waitForTimeout(400);
|
|
42
46
|
}
|
|
43
47
|
|
|
48
|
+
// Optional HAR capture
|
|
49
|
+
const harState = flags.har ? await startHarCapture(page) : null;
|
|
44
50
|
await page.screenshot({ path: out, fullPage: asBool(flags.full, false) });
|
|
45
51
|
const meta = { url, out, ts: nowIso(), status: r.status, title: r.title, viewport, flags: { ...flags } };
|
|
52
|
+
if (harState) {
|
|
53
|
+
const har = stopHarCapture(page);
|
|
54
|
+
const harFile = await writeHar(har, String(flags.har));
|
|
55
|
+
meta.har = harFile;
|
|
56
|
+
meta.harEntryCount = har?._meta?.entryCount || 0;
|
|
57
|
+
}
|
|
46
58
|
await runAfterShoot(ctx, meta);
|
|
47
59
|
return meta;
|
|
48
60
|
})().catch(e => ({ url, out, ts: nowIso(), error: e.message, viewport, flags: { ...flags } }));
|