pursr 0.7.0 → 0.7.1
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/bin/pursr.mjs +4 -3
- package/package.json +1 -1
- package/src/diff.js +18 -7
- package/src/plugin-audit.js +20 -2
package/bin/pursr.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
// pursr CLI. Thin wrapper around src/* that mirrors the npm bin.
|
|
3
3
|
|
|
4
4
|
import { VERSION } from "../src/index.js";
|
|
@@ -79,14 +79,15 @@ await loadPlugins(pluginPaths);
|
|
|
79
79
|
case "diff": {
|
|
80
80
|
if (!url) die("missing url"); const ref = b; if (!ref) die("diff: missing <ref.png>");
|
|
81
81
|
const out = c || makeOut("diff.png"); const threshold = d !== undefined ? Number(d) : 0.1;
|
|
82
|
+
const flags = parseFlags(argv.slice(5));
|
|
82
83
|
// --ai / --ai-model / --ai-base-url / --ai-api-key
|
|
83
84
|
const useAi = argv.includes("--ai");
|
|
84
85
|
const aiModel = (() => { const i = argv.indexOf("--ai-model"); return i >= 0 && i + 1 < argv.length ? argv[i + 1] : undefined; })();
|
|
85
86
|
const aiBaseUrl = (() => { const i = argv.indexOf("--ai-base-url"); return i >= 0 && i + 1 < argv.length ? argv[i + 1] : undefined; })();
|
|
86
87
|
const aiApiKey = (() => { const i = argv.indexOf("--ai-api-key"); return i >= 0 && i + 1 < argv.length ? argv[i + 1] : undefined; })();
|
|
87
88
|
const r = useAi
|
|
88
|
-
? await runDiffWithAi(url, ref, out, threshold, { model: aiModel, baseUrl: aiBaseUrl, apiKey: aiApiKey })
|
|
89
|
-
: await runDiff(url, ref, out, threshold);
|
|
89
|
+
? await runDiffWithAi(url, ref, out, threshold, flags, { model: aiModel, baseUrl: aiBaseUrl, apiKey: aiApiKey })
|
|
90
|
+
: await runDiff(url, ref, out, threshold, flags);
|
|
90
91
|
console.log(JSON.stringify(r, null, 2)); break;
|
|
91
92
|
}
|
|
92
93
|
case "seq": { if (!url) die("missing url"); const actions = readArg(b); if (!actions) die("seq: missing <actions.json> (or @file)"); const out = c || makeOut("seq.png"); const r = await runSeq(url, actions, out); console.log(JSON.stringify(r, null, 2)); break; }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pursr",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "pursr — Visual QA, audit, and MCP for the browser. One CLI + one MCP server for screenshots, sweeps, baselines, diffs, axe-core a11y audits, HAR capture, and auth state — with parallel sweep workers, auto-healing selectors, and a plugin system. Zero browser bundled: drives your system Chrome via Playwright.",
|
|
6
6
|
"homepage": "https://github.com/0xheycat/pursr",
|
package/src/diff.js
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
4
4
|
import { join, dirname } from "node:path";
|
|
5
5
|
import { launch, newPage } from "./runway.js";
|
|
6
|
-
import {
|
|
7
|
-
import { gotoOrThrow, settle } from "./overlays.js";
|
|
8
|
-
import { requireArg } from "./util.js";
|
|
6
|
+
import { resolveViewport } from "./viewport.js";
|
|
7
|
+
import { gotoOrThrow, settle, applyCamera, waitForStableFrame } from "./overlays.js";
|
|
8
|
+
import { asNum, requireArg } from "./util.js";
|
|
9
9
|
import { aiDiffSidecar } from "./ai-diff.js";
|
|
10
10
|
|
|
11
11
|
const DIFF_DEFAULT_THRESHOLD = 0.1;
|
|
@@ -20,7 +20,7 @@ async function loadPixelmatch() {
|
|
|
20
20
|
catch { throw new Error("pixelmatch not found. Install: npm i pixelmatch"); }
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
export async function runDiff(url, refPath, out, threshold, browser) {
|
|
23
|
+
export async function runDiff(url, refPath, out, threshold, flags = {}, browser) {
|
|
24
24
|
requireArg("url", url, "string");
|
|
25
25
|
requireArg("refPath", refPath, "string");
|
|
26
26
|
const t = threshold !== undefined ? Number(threshold) : DIFF_DEFAULT_THRESHOLD;
|
|
@@ -28,10 +28,21 @@ export async function runDiff(url, refPath, out, threshold, browser) {
|
|
|
28
28
|
const PNG = await loadPngjs();
|
|
29
29
|
const pixelmatch = await loadPixelmatch();
|
|
30
30
|
const ownBrowser = !browser;
|
|
31
|
+
const viewport = resolveViewport(flags || {});
|
|
31
32
|
browser = browser || await launch();
|
|
32
33
|
try {
|
|
33
|
-
const page = await newPage(browser,
|
|
34
|
+
const page = await newPage(browser, viewport);
|
|
34
35
|
const r = await gotoOrThrow(page, url); await settle(page);
|
|
36
|
+
if (flags && (flags["wait-frame"] || flags["no-animation"])) {
|
|
37
|
+
await waitForStableFrame(page, asNum(flags["wait-frame"], 600));
|
|
38
|
+
}
|
|
39
|
+
if (flags && (flags.zoom || flags.panX || flags.panY)) {
|
|
40
|
+
await applyCamera(page, {
|
|
41
|
+
zoom: asNum(flags.zoom, 1),
|
|
42
|
+
panX: asNum(flags.panX, 0),
|
|
43
|
+
panY: asNum(flags.panY, 0),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
35
46
|
const currentPath = out ? out.replace(/\.png$/i, "-current.png") : join(dirname(refPath), "current.png");
|
|
36
47
|
await page.screenshot({ path: currentPath, fullPage: false });
|
|
37
48
|
const refPng = PNG.sync.read(readFileSync(refPath));
|
|
@@ -53,8 +64,8 @@ export async function runDiff(url, refPath, out, threshold, browser) {
|
|
|
53
64
|
* summary of the visual differences. The AI summary is written to <out>.ai.json
|
|
54
65
|
* and also returned on the result object.
|
|
55
66
|
*/
|
|
56
|
-
export async function runDiffWithAi(url, refPath, out, threshold, aiOpts, browser) {
|
|
57
|
-
const r = await runDiff(url, refPath, out, threshold, browser);
|
|
67
|
+
export async function runDiffWithAi(url, refPath, out, threshold, flags, aiOpts, browser) {
|
|
68
|
+
const r = await runDiff(url, refPath, out, threshold, flags, browser);
|
|
58
69
|
if (r.error) return r;
|
|
59
70
|
try {
|
|
60
71
|
const curPath = r.currentPath;
|
package/src/plugin-audit.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// Dependencies: axe-core (npm i axe-core)
|
|
11
11
|
|
|
12
12
|
import { launch, newPage } from "./runway.js";
|
|
13
|
+
import { createRequire } from "node:module";
|
|
13
14
|
import { resolveViewport } from "./viewport.js";
|
|
14
15
|
import { gotoOrThrow, settle } from "./overlays.js";
|
|
15
16
|
import { writeFileSync, mkdirSync, readFileSync, existsSync } from "node:fs";
|
|
@@ -87,11 +88,29 @@ let _axeSource = null;
|
|
|
87
88
|
async function getAxeSource() {
|
|
88
89
|
if (_axeSource) return _axeSource;
|
|
89
90
|
// Try node_modules/axe-core
|
|
91
|
+
// Primary: use createRequire to resolve axe-core from this module (works for the
|
|
92
|
+
// pursr package itself, the linked repo, or any project that has axe-core installed).
|
|
93
|
+
try {
|
|
94
|
+
const req = createRequire(import.meta.url);
|
|
95
|
+
const resolved = req.resolve("axe-core");
|
|
96
|
+
const dir = resolved.replace(/[\\\/][^\\\/]+$/, "");
|
|
97
|
+
for (const fname of ["axe.min.js", "axe.js"]) {
|
|
98
|
+
const p = dir + "/" + fname;
|
|
99
|
+
if (existsSync(p)) { _axeSource = readFileSync(p, "utf8"); return _axeSource; }
|
|
100
|
+
}
|
|
101
|
+
} catch { /* fall through to path-based lookup */ }
|
|
90
102
|
const paths = [
|
|
91
103
|
join(process.cwd(), "node_modules", "axe-core", "axe.min.js"),
|
|
92
104
|
join(process.cwd(), "node_modules", "axe-core", "axe.js"),
|
|
93
105
|
new URL("..", import.meta.url).pathname && join(dirname(new URL(import.meta.url).pathname), "node_modules", "axe-core", "axe.min.js"),
|
|
94
106
|
join(dirname(process.execPath), "node_modules", "axe-core", "axe.min.js"),
|
|
107
|
+
// Global npm install (Windows: %APPDATA%\npm\node_modules, Unix: /usr/lib/node_modules)
|
|
108
|
+
join(process.env.APPDATA || "", "npm", "node_modules", "axe-core", "axe.min.js"),
|
|
109
|
+
join(process.env.HOME || "", ".npm-global", "lib", "node_modules", "axe-core", "axe.min.js"),
|
|
110
|
+
join("/usr", "lib", "node_modules", "axe-core", "axe.min.js"),
|
|
111
|
+
join("/usr", "local", "lib", "node_modules", "axe-core", "axe.min.js"),
|
|
112
|
+
// The pursr package's own node_modules (when running from local repo or via node bin)
|
|
113
|
+
join(dirname(new URL(import.meta.url).pathname), "..", "node_modules", "axe-core", "axe.min.js"),
|
|
95
114
|
];
|
|
96
115
|
for (const p of paths) {
|
|
97
116
|
if (p && existsSync(p)) {
|
|
@@ -99,8 +118,7 @@ async function getAxeSource() {
|
|
|
99
118
|
return _axeSource;
|
|
100
119
|
}
|
|
101
120
|
}
|
|
102
|
-
|
|
103
|
-
}
|
|
121
|
+
throw new Error("axe-core not found. Install: npm i axe-core");}
|
|
104
122
|
|
|
105
123
|
// ─── Group helper ───────────────────────────────────────────────────────
|
|
106
124
|
|