@mthanh/vespercli 2.2.0 → 2.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 +6 -3
- package/assets/CATALOG.md +9 -2
- package/assets/claude/.claude-plugin/marketplace.json +9 -9
- package/assets/claude/.claude-plugin/plugin.json +1 -1
- package/assets/claude/agents/code-planner.md +1 -1
- package/assets/claude/agents/code-reviewer.md +1 -1
- package/assets/claude/agents/code-tester.md +1 -1
- package/assets/claude/agents/copywriter.md +1 -1
- package/assets/claude/agents/feature-wrap-document.md +1 -1
- package/assets/claude/agents/janitor.md +1 -1
- package/assets/claude/agents/modernization.md +1 -1
- package/assets/claude/agents/principal-software-engineer.md +1 -1
- package/assets/claude/agents/refine-issue.md +1 -1
- package/assets/claude/agents/se-system-architecture-reviewer.md +1 -1
- package/assets/claude/agents/se-technical-writer.md +1 -1
- package/assets/claude/agents/skill-creator.md +1 -1
- package/assets/claude/agents/task-researcher.md +1 -1
- package/assets/claude/agents/technical-product-manager.md +1 -1
- package/assets/claude/agents/vision-analyst.md +1 -1
- package/assets/claude/agents/vitest.md +1 -1
- package/assets/claude/agents/workspace-learner.md +2 -2
- package/assets/claude/packs/backend/.claude-plugin/plugin.json +1 -1
- package/assets/claude/packs/backend/agents/firebase-developer.md +1 -1
- package/assets/claude/packs/backend/agents/nodejs-express-developer.md +1 -1
- package/assets/claude/packs/docs/.claude-plugin/plugin.json +1 -1
- package/assets/claude/packs/frontend/.claude-plugin/plugin.json +1 -1
- package/assets/claude/packs/frontend/agents/expert-nextjs-developer.md +1 -1
- package/assets/claude/packs/frontend/agents/expert-react-frontend-engineer.md +1 -1
- package/assets/claude/packs/frontend/agents/jest-react-component-analyzer.md +1 -1
- package/assets/claude/packs/frontend/agents/jest-react-scenario-scanner.md +1 -1
- package/assets/claude/packs/frontend/agents/jest-react-test-generator.md +1 -1
- package/assets/claude/packs/frontend/agents/jest-react-test-planner.md +1 -1
- package/assets/claude/packs/frontend/skills/design-taste-frontend/LICENSE +21 -0
- package/assets/claude/packs/frontend/skills/design-taste-frontend/SKILL.md +529 -0
- package/assets/claude/packs/frontend/skills/design-taste-frontend/UPSTREAM.json +7 -0
- package/assets/claude/packs/frontend/skills/design-taste-frontend/assets/app.css +216 -0
- package/assets/claude/packs/frontend/skills/design-taste-frontend/assets/base.css +105 -0
- package/assets/claude/packs/frontend/skills/design-taste-frontend/assets/base.js +37 -0
- package/assets/claude/packs/frontend/skills/design-taste-frontend/assets/charts.js +617 -0
- package/assets/claude/packs/frontend/skills/design-taste-frontend/assets/scenes/stage.css +28 -0
- package/assets/claude/packs/frontend/skills/design-taste-frontend/assets/scenes/stage.js +133 -0
- package/assets/claude/packs/frontend/skills/design-taste-frontend/evals/evals.json +76 -0
- package/assets/claude/packs/frontend/skills/design-taste-frontend/library/app.json +1327 -0
- package/assets/claude/packs/frontend/skills/design-taste-frontend/library/directions.json +1078 -0
- package/assets/claude/packs/frontend/skills/design-taste-frontend/library/heroes.json +1086 -0
- package/assets/claude/packs/frontend/skills/design-taste-frontend/library/motions.json +92 -0
- package/assets/claude/packs/frontend/skills/design-taste-frontend/library/palettes.json +1485 -0
- package/assets/claude/packs/frontend/skills/design-taste-frontend/library/photos.json +7240 -0
- package/assets/claude/packs/frontend/skills/design-taste-frontend/library/sections.json +4429 -0
- package/assets/claude/packs/frontend/skills/design-taste-frontend/library/sites.json +8350 -0
- package/assets/claude/packs/frontend/skills/design-taste-frontend/library/type.json +2024 -0
- package/assets/claude/packs/frontend/skills/design-taste-frontend/references/app-screens.md +1199 -0
- package/assets/claude/packs/frontend/skills/design-taste-frontend/references/colour.md +141 -0
- package/assets/claude/packs/frontend/skills/design-taste-frontend/references/copy.md +177 -0
- package/assets/claude/packs/frontend/skills/design-taste-frontend/references/critique.md +182 -0
- package/assets/claude/packs/frontend/skills/design-taste-frontend/references/imagery.md +791 -0
- package/assets/claude/packs/frontend/skills/design-taste-frontend/references/layout.md +292 -0
- package/assets/claude/packs/frontend/skills/design-taste-frontend/references/motion.md +980 -0
- package/assets/claude/packs/frontend/skills/design-taste-frontend/references/research.md +356 -0
- package/assets/claude/packs/frontend/skills/design-taste-frontend/references/scenes.md +174 -0
- package/assets/claude/packs/frontend/skills/design-taste-frontend/references/tells.md +115 -0
- package/assets/claude/packs/frontend/skills/design-taste-frontend/references/type.md +170 -0
- package/assets/claude/packs/frontend/skills/design-taste-frontend/scripts/assets.mjs +865 -0
- package/assets/claude/packs/frontend/skills/design-taste-frontend/scripts/draw.mjs +584 -0
- package/assets/claude/packs/frontend/skills/design-taste-frontend/scripts/inspect.mjs +303 -0
- package/assets/claude/packs/frontend/skills/design-taste-frontend/scripts/lib/app-draw.mjs +203 -0
- package/assets/claude/packs/frontend/skills/design-taste-frontend/scripts/lib/cdp.mjs +248 -0
- package/assets/claude/packs/frontend/skills/design-taste-frontend/scripts/lib/probe.mjs +909 -0
- package/assets/claude/packs/frontend/skills/design-taste-frontend/scripts/lib/rules.mjs +243 -0
- package/assets/claude/packs/frontend/skills/design-taste-frontend/scripts/lint.mjs +119 -0
- package/assets/claude/packs/frontend/skills/design-taste-frontend/scripts/scenes.mjs +124 -0
- package/assets/claude/packs/graphify/.claude-plugin/plugin.json +1 -1
- package/assets/claude/packs/integrations/.claude-plugin/plugin.json +1 -1
- package/assets/claude/packs/playwright/.claude-plugin/plugin.json +1 -1
- package/assets/claude/packs/playwright/agents/playwright-component-analyzer.md +1 -1
- package/assets/claude/packs/playwright/agents/playwright-scenario-scanner.md +1 -1
- package/assets/claude/packs/playwright/agents/playwright-test-generator.md +1 -1
- package/assets/claude/packs/playwright/agents/playwright-test-planner.md +1 -1
- package/assets/claude/packs/playwright/agents/playwright-tester.md +1 -1
- package/assets/claude/packs/polyglot/.claude-plugin/plugin.json +1 -1
- package/assets/claude/packs/styles/.claude-plugin/plugin.json +1 -1
- package/assets/copilot/agents/code-planner.agent.md +1 -1
- package/assets/copilot/agents/code-reviewer.agent.md +1 -1
- package/assets/copilot/agents/code-tester.agent.md +1 -1
- package/assets/copilot/agents/copywriter.agent.md +1 -1
- package/assets/copilot/agents/feature-wrap-document.agent.md +1 -1
- package/assets/copilot/agents/janitor.agent.md +1 -1
- package/assets/copilot/agents/modernization.agent.md +1 -1
- package/assets/copilot/agents/principal-software-engineer.agent.md +1 -1
- package/assets/copilot/agents/refine-issue.agent.md +1 -1
- package/assets/copilot/agents/se-system-architecture-reviewer.agent.md +1 -1
- package/assets/copilot/agents/se-technical-writer.agent.md +1 -1
- package/assets/copilot/agents/skill-creator.agent.md +1 -1
- package/assets/copilot/agents/task-researcher.agent.md +1 -1
- package/assets/copilot/agents/technical-product-manager.agent.md +1 -1
- package/assets/copilot/agents/vision-analyst.agent.md +1 -1
- package/assets/copilot/agents/vitest.agent.md +1 -1
- package/assets/copilot/agents/workspace-learner.agent.md +2 -2
- package/assets/copilot/packs/backend/agents/firebase-developer.agent.md +1 -1
- package/assets/copilot/packs/backend/agents/nodejs-express-developer.agent.md +1 -1
- package/assets/copilot/packs/frontend/agents/expert-nextjs-developer.agent.md +1 -1
- package/assets/copilot/packs/frontend/agents/expert-react-frontend-engineer.agent.md +1 -1
- package/assets/copilot/packs/frontend/agents/jest-react-component-analyzer.agent.md +1 -1
- package/assets/copilot/packs/frontend/agents/jest-react-scenario-scanner.agent.md +1 -1
- package/assets/copilot/packs/frontend/agents/jest-react-test-generator.agent.md +1 -1
- package/assets/copilot/packs/frontend/agents/jest-react-test-planner.agent.md +1 -1
- package/assets/copilot/packs/frontend/skills/design-taste-frontend/LICENSE +21 -0
- package/assets/copilot/packs/frontend/skills/design-taste-frontend/SKILL.md +529 -0
- package/assets/copilot/packs/frontend/skills/design-taste-frontend/UPSTREAM.json +7 -0
- package/assets/copilot/packs/frontend/skills/design-taste-frontend/assets/app.css +216 -0
- package/assets/copilot/packs/frontend/skills/design-taste-frontend/assets/base.css +105 -0
- package/assets/copilot/packs/frontend/skills/design-taste-frontend/assets/base.js +37 -0
- package/assets/copilot/packs/frontend/skills/design-taste-frontend/assets/charts.js +617 -0
- package/assets/copilot/packs/frontend/skills/design-taste-frontend/assets/scenes/stage.css +28 -0
- package/assets/copilot/packs/frontend/skills/design-taste-frontend/assets/scenes/stage.js +133 -0
- package/assets/copilot/packs/frontend/skills/design-taste-frontend/evals/evals.json +76 -0
- package/assets/copilot/packs/frontend/skills/design-taste-frontend/library/app.json +1327 -0
- package/assets/copilot/packs/frontend/skills/design-taste-frontend/library/directions.json +1078 -0
- package/assets/copilot/packs/frontend/skills/design-taste-frontend/library/heroes.json +1086 -0
- package/assets/copilot/packs/frontend/skills/design-taste-frontend/library/motions.json +92 -0
- package/assets/copilot/packs/frontend/skills/design-taste-frontend/library/palettes.json +1485 -0
- package/assets/copilot/packs/frontend/skills/design-taste-frontend/library/photos.json +7240 -0
- package/assets/copilot/packs/frontend/skills/design-taste-frontend/library/sections.json +4429 -0
- package/assets/copilot/packs/frontend/skills/design-taste-frontend/library/sites.json +8350 -0
- package/assets/copilot/packs/frontend/skills/design-taste-frontend/library/type.json +2024 -0
- package/assets/copilot/packs/frontend/skills/design-taste-frontend/references/app-screens.md +1199 -0
- package/assets/copilot/packs/frontend/skills/design-taste-frontend/references/colour.md +141 -0
- package/assets/copilot/packs/frontend/skills/design-taste-frontend/references/copy.md +177 -0
- package/assets/copilot/packs/frontend/skills/design-taste-frontend/references/critique.md +182 -0
- package/assets/copilot/packs/frontend/skills/design-taste-frontend/references/imagery.md +791 -0
- package/assets/copilot/packs/frontend/skills/design-taste-frontend/references/layout.md +292 -0
- package/assets/copilot/packs/frontend/skills/design-taste-frontend/references/motion.md +980 -0
- package/assets/copilot/packs/frontend/skills/design-taste-frontend/references/research.md +356 -0
- package/assets/copilot/packs/frontend/skills/design-taste-frontend/references/scenes.md +174 -0
- package/assets/copilot/packs/frontend/skills/design-taste-frontend/references/tells.md +115 -0
- package/assets/copilot/packs/frontend/skills/design-taste-frontend/references/type.md +170 -0
- package/assets/copilot/packs/frontend/skills/design-taste-frontend/scripts/assets.mjs +865 -0
- package/assets/copilot/packs/frontend/skills/design-taste-frontend/scripts/draw.mjs +584 -0
- package/assets/copilot/packs/frontend/skills/design-taste-frontend/scripts/inspect.mjs +303 -0
- package/assets/copilot/packs/frontend/skills/design-taste-frontend/scripts/lib/app-draw.mjs +203 -0
- package/assets/copilot/packs/frontend/skills/design-taste-frontend/scripts/lib/cdp.mjs +248 -0
- package/assets/copilot/packs/frontend/skills/design-taste-frontend/scripts/lib/probe.mjs +909 -0
- package/assets/copilot/packs/frontend/skills/design-taste-frontend/scripts/lib/rules.mjs +243 -0
- package/assets/copilot/packs/frontend/skills/design-taste-frontend/scripts/lint.mjs +119 -0
- package/assets/copilot/packs/frontend/skills/design-taste-frontend/scripts/scenes.mjs +124 -0
- package/assets/copilot/packs/playwright/agents/playwright-component-analyzer.agent.md +1 -1
- package/assets/copilot/packs/playwright/agents/playwright-scenario-scanner.agent.md +1 -1
- package/assets/copilot/packs/playwright/agents/playwright-test-generator.agent.md +1 -1
- package/assets/copilot/packs/playwright/agents/playwright-test-planner.agent.md +1 -1
- package/assets/copilot/packs/playwright/agents/playwright-tester.agent.md +1 -1
- package/assets/manifest.json +2 -0
- package/dist/cli.min.js +102 -102
- package/package.json +1 -1
- package/scripts/__snapshots__/generate-claude-assets.test.js.snap +1 -1
- package/scripts/generate-claude-assets.strict.test.js +1 -1
- package/scripts/generate-claude-assets.test.js +6 -6
- package/scripts/packs-layout.test.js +2 -1
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
// Zero-dependency Chrome DevTools Protocol client.
|
|
2
|
+
// Drives an installed Chrome / Edge / Chromium in headless mode over Node's built-in WebSocket.
|
|
3
|
+
// Requires Node 22+. No npm install.
|
|
4
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
5
|
+
import { existsSync, mkdtempSync, rmSync, writeFileSync, readFileSync, readdirSync } from 'node:fs';
|
|
6
|
+
import { tmpdir, hostname } from 'node:os';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
|
|
9
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
10
|
+
|
|
11
|
+
export function findChrome() {
|
|
12
|
+
const env = process.env.CHROME_PATH || process.env.PUPPETEER_EXECUTABLE_PATH;
|
|
13
|
+
if (env && existsSync(env)) return env;
|
|
14
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
15
|
+
const local = process.env.LOCALAPPDATA || '';
|
|
16
|
+
const pf = process.env.PROGRAMFILES || 'C:\\Program Files';
|
|
17
|
+
const pf86 = process.env['PROGRAMFILES(X86)'] || 'C:\\Program Files (x86)';
|
|
18
|
+
const candidates = [
|
|
19
|
+
// Windows
|
|
20
|
+
join(pf, 'Google/Chrome/Application/chrome.exe'),
|
|
21
|
+
join(pf86, 'Google/Chrome/Application/chrome.exe'),
|
|
22
|
+
join(local, 'Google/Chrome/Application/chrome.exe'),
|
|
23
|
+
join(pf86, 'Microsoft/Edge/Application/msedge.exe'),
|
|
24
|
+
join(pf, 'Microsoft/Edge/Application/msedge.exe'),
|
|
25
|
+
join(pf, 'BraveSoftware/Brave-Browser/Application/brave.exe'),
|
|
26
|
+
// macOS
|
|
27
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
28
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
29
|
+
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
|
|
30
|
+
'/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
|
|
31
|
+
join(home, 'Applications/Google Chrome.app/Contents/MacOS/Google Chrome'),
|
|
32
|
+
// Linux
|
|
33
|
+
'/usr/bin/google-chrome', '/usr/bin/google-chrome-stable', '/usr/bin/chromium',
|
|
34
|
+
'/usr/bin/chromium-browser', '/snap/bin/chromium', '/usr/bin/microsoft-edge',
|
|
35
|
+
];
|
|
36
|
+
for (const c of candidates) if (c && existsSync(c)) return c;
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Each profile carries an owner file naming the machine and the two processes that use it (the run and
|
|
41
|
+
// its Chrome). A later run removes a profile left by a killed run only when that file names this machine
|
|
42
|
+
// and neither process is alive. A profile without one (made before 2.1.4, or by another tool) is never touched.
|
|
43
|
+
const OWNER = 'taste-skill-owner.json';
|
|
44
|
+
|
|
45
|
+
function alive(pid) {
|
|
46
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
47
|
+
try { process.kill(pid, 0); return true; } catch (e) { return e.code === 'EPERM'; }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function sweepProfiles(base) {
|
|
51
|
+
let names;
|
|
52
|
+
try { names = readdirSync(base); } catch { return; }
|
|
53
|
+
for (const name of names) {
|
|
54
|
+
if (!name.startsWith('ad-chrome-')) continue;
|
|
55
|
+
const p = join(base, name);
|
|
56
|
+
try {
|
|
57
|
+
const o = JSON.parse(readFileSync(join(p, OWNER), 'utf8'));
|
|
58
|
+
if (o.host !== hostname() || !Number.isInteger(o.pid) || alive(o.pid) || alive(o.chrome)) continue;
|
|
59
|
+
rmSync(p, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
60
|
+
} catch {}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export class Browser {
|
|
65
|
+
static async launch({ width = 1440, height = 900, extraArgs = [] } = {}) {
|
|
66
|
+
if (typeof WebSocket === 'undefined') {
|
|
67
|
+
throw new Error('Node 22+ is required (built-in WebSocket missing). Current: ' + process.version);
|
|
68
|
+
}
|
|
69
|
+
const exe = findChrome();
|
|
70
|
+
if (!exe) {
|
|
71
|
+
throw new Error('No Chrome/Edge/Chromium found. Install one or set CHROME_PATH to its executable.');
|
|
72
|
+
}
|
|
73
|
+
// A throwaway profile per run, its cache capped (each run starts cold anyway), removed on exit. It is
|
|
74
|
+
// claimed with an owner file before Chrome starts, so another run's sweep can tell when it was abandoned.
|
|
75
|
+
const base = process.env.TASTE_SKILL_TMP || tmpdir();
|
|
76
|
+
sweepProfiles(base);
|
|
77
|
+
const dir = mkdtempSync(join(base, 'ad-chrome-'));
|
|
78
|
+
const claim = (chrome) => {
|
|
79
|
+
try { writeFileSync(join(dir, OWNER), JSON.stringify({ host: hostname(), pid: process.pid, chrome })); } catch {}
|
|
80
|
+
};
|
|
81
|
+
claim();
|
|
82
|
+
// On exit Chrome still holds the profile's files: stop it first, then remove the folder.
|
|
83
|
+
let pid = 0;
|
|
84
|
+
const drop = () => {
|
|
85
|
+
try { if (pid) { if (process.platform === 'win32') spawnSync('taskkill', ['/pid', String(pid), '/T', '/F'], { stdio: 'ignore' }); else process.kill(pid, 'SIGKILL'); } } catch {}
|
|
86
|
+
try { rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); } catch {}
|
|
87
|
+
};
|
|
88
|
+
process.once('exit', drop);
|
|
89
|
+
for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP']) process.once(sig, () => { drop(); process.exit(130); });
|
|
90
|
+
const args = [
|
|
91
|
+
'--headless=new', '--remote-debugging-port=0', `--user-data-dir=${dir}`,
|
|
92
|
+
'--disk-cache-size=16777216', '--media-cache-size=16777216',
|
|
93
|
+
'--no-first-run', '--no-default-browser-check', '--mute-audio', '--hide-scrollbars',
|
|
94
|
+
'--disable-background-timer-throttling', '--disable-renderer-backgrounding',
|
|
95
|
+
'--disable-backgrounding-occluded-windows', '--disable-features=Translate,MediaRouter',
|
|
96
|
+
'--force-color-profile=srgb', '--autoplay-policy=no-user-gesture-required',
|
|
97
|
+
'--enable-unsafe-swiftshader', `--window-size=${width},${height}`, ...extraArgs, 'about:blank',
|
|
98
|
+
];
|
|
99
|
+
const proc = spawn(exe, args, { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
100
|
+
pid = proc.pid;
|
|
101
|
+
claim(pid);
|
|
102
|
+
const wsUrl = await new Promise((resolve, reject) => {
|
|
103
|
+
let buf = '';
|
|
104
|
+
const t = setTimeout(() => reject(new Error('Chrome did not start within 20s')), 20000);
|
|
105
|
+
proc.stderr.on('data', (d) => {
|
|
106
|
+
buf += d.toString();
|
|
107
|
+
const m = buf.match(/DevTools listening on (ws:\/\/[^\s]+)/);
|
|
108
|
+
if (m) { clearTimeout(t); resolve(m[1]); }
|
|
109
|
+
});
|
|
110
|
+
proc.on('exit', (code) => { clearTimeout(t); reject(new Error('Chrome exited early, code ' + code + '\n' + buf.slice(-600))); });
|
|
111
|
+
});
|
|
112
|
+
const b = new Browser();
|
|
113
|
+
b.proc = proc; b.dir = dir; b.nextId = 1; b.pending = new Map(); b.listeners = new Map();
|
|
114
|
+
b.ws = new WebSocket(wsUrl);
|
|
115
|
+
await new Promise((res, rej) => { b.ws.onopen = res; b.ws.onerror = () => rej(new Error('CDP socket failed')); });
|
|
116
|
+
b.ws.onmessage = (ev) => {
|
|
117
|
+
const msg = JSON.parse(typeof ev.data === 'string' ? ev.data : Buffer.from(ev.data).toString());
|
|
118
|
+
if (msg.id && b.pending.has(msg.id)) {
|
|
119
|
+
const { resolve, reject, timer } = b.pending.get(msg.id);
|
|
120
|
+
clearTimeout(timer); b.pending.delete(msg.id);
|
|
121
|
+
if (msg.error) reject(new Error(msg.error.message)); else resolve(msg.result);
|
|
122
|
+
} else if (msg.method) {
|
|
123
|
+
const fns = b.listeners.get((msg.sessionId || '') + '|' + msg.method);
|
|
124
|
+
if (fns) for (const fn of [...fns]) fn(msg.params);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
return b;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
send(method, params = {}, sessionId, timeoutMs = 30000) {
|
|
131
|
+
const id = this.nextId++;
|
|
132
|
+
return new Promise((resolve, reject) => {
|
|
133
|
+
const timer = setTimeout(() => { this.pending.delete(id); reject(new Error('CDP timeout: ' + method)); }, timeoutMs);
|
|
134
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
135
|
+
this.ws.send(JSON.stringify(sessionId ? { id, method, params, sessionId } : { id, method, params }));
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
on(sessionId, method, fn) {
|
|
140
|
+
const k = (sessionId || '') + '|' + method;
|
|
141
|
+
if (!this.listeners.has(k)) this.listeners.set(k, new Set());
|
|
142
|
+
this.listeners.get(k).add(fn);
|
|
143
|
+
return () => this.listeners.get(k)?.delete(fn);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async newPage({ width = 1440, height = 900, dsf = 1, mobile = false, reducedMotion = false } = {}) {
|
|
147
|
+
const { targetId } = await this.send('Target.createTarget', { url: 'about:blank', newWindow: true });
|
|
148
|
+
const { sessionId } = await this.send('Target.attachToTarget', { targetId, flatten: true });
|
|
149
|
+
const p = new Page(this, targetId, sessionId);
|
|
150
|
+
await p.send('Page.enable');
|
|
151
|
+
await p.send('Runtime.enable');
|
|
152
|
+
await p.setViewport({ width, height, dsf, mobile });
|
|
153
|
+
if (reducedMotion) {
|
|
154
|
+
await p.send('Emulation.setEmulatedMedia', { features: [{ name: 'prefers-reduced-motion', value: 'reduce' }] });
|
|
155
|
+
}
|
|
156
|
+
return p;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async close() {
|
|
160
|
+
try { await this.send('Browser.close', {}, undefined, 3000); } catch {}
|
|
161
|
+
try { this.ws.close(); } catch {}
|
|
162
|
+
try { this.proc.kill(); } catch {}
|
|
163
|
+
await sleep(300);
|
|
164
|
+
try { rmSync(this.dir, { recursive: true, force: true, maxRetries: 3 }); } catch {}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export class Page {
|
|
169
|
+
constructor(browser, targetId, sessionId) {
|
|
170
|
+
this.browser = browser; this.targetId = targetId; this.sessionId = sessionId;
|
|
171
|
+
this.consoleErrors = [];
|
|
172
|
+
browser.on(sessionId, 'Runtime.exceptionThrown', (p) => {
|
|
173
|
+
this.consoleErrors.push((p.exceptionDetails?.exception?.description || p.exceptionDetails?.text || '').slice(0, 300));
|
|
174
|
+
});
|
|
175
|
+
// Never let a JS dialog block the page.
|
|
176
|
+
browser.on(sessionId, 'Page.javascriptDialogOpening', () => {
|
|
177
|
+
this.send('Page.handleJavaScriptDialog', { accept: false }).catch(() => {});
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
send(method, params = {}, timeoutMs = 30000) {
|
|
182
|
+
return this.browser.send(method, params, this.sessionId, timeoutMs);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async setViewport({ width, height, dsf = 1, mobile = false }) {
|
|
186
|
+
this.viewport = { width, height, dsf, mobile };
|
|
187
|
+
await this.send('Emulation.setDeviceMetricsOverride', { width, height, deviceScaleFactor: dsf, mobile });
|
|
188
|
+
await this.send('Emulation.setTouchEmulationEnabled', { enabled: !!mobile });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Navigates and resolves after the load event (or timeout), then waits `settleMs` for intros to finish.
|
|
192
|
+
async goto(url, { timeoutMs = 25000, settleMs = 1500 } = {}) {
|
|
193
|
+
let off;
|
|
194
|
+
const loaded = new Promise((resolve) => { off = this.browser.on(this.sessionId, 'Page.loadEventFired', () => resolve('load')); });
|
|
195
|
+
const nav = await this.send('Page.navigate', { url }, timeoutMs).catch((e) => ({ errorText: e.message }));
|
|
196
|
+
if (nav.errorText && !/ERR_ABORTED/.test(nav.errorText)) { off(); throw new Error('navigate: ' + nav.errorText); }
|
|
197
|
+
const how = await Promise.race([loaded, sleep(timeoutMs).then(() => 'timeout')]);
|
|
198
|
+
off();
|
|
199
|
+
await sleep(settleMs);
|
|
200
|
+
return how;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Evaluates a function (serialised) or an expression string in the page and returns its JSON value.
|
|
204
|
+
async eval(fnOrExpr, arg) {
|
|
205
|
+
const expression = typeof fnOrExpr === 'function'
|
|
206
|
+
? `(${fnOrExpr.toString()})(${arg === undefined ? '' : JSON.stringify(arg)})`
|
|
207
|
+
: fnOrExpr;
|
|
208
|
+
const r = await this.send('Runtime.evaluate', { expression, awaitPromise: true, returnByValue: true }, 30000);
|
|
209
|
+
if (r.exceptionDetails) {
|
|
210
|
+
throw new Error('page eval: ' + (r.exceptionDetails.exception?.description || r.exceptionDetails.text));
|
|
211
|
+
}
|
|
212
|
+
return r.result?.value;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async screenshot({ path, format = 'jpeg', quality = 72, fullPage = false, clip } = {}) {
|
|
216
|
+
const params = { format, captureBeyondViewport: !!fullPage };
|
|
217
|
+
if (format !== 'png') params.quality = quality;
|
|
218
|
+
if (fullPage) {
|
|
219
|
+
const m = await this.send('Page.getLayoutMetrics');
|
|
220
|
+
const s = m.cssContentSize || m.contentSize;
|
|
221
|
+
params.clip = { x: 0, y: 0, width: Math.ceil(s.width), height: Math.min(Math.ceil(s.height), 16000), scale: 1 };
|
|
222
|
+
} else if (clip) {
|
|
223
|
+
params.clip = { scale: 1, ...clip };
|
|
224
|
+
}
|
|
225
|
+
const { data } = await this.send('Page.captureScreenshot', params, 45000);
|
|
226
|
+
if (path) writeFileSync(path, Buffer.from(data, 'base64'));
|
|
227
|
+
return data;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Real wheel input, so smooth-scroll libraries and scroll-driven reveals behave as they do for a visitor.
|
|
231
|
+
async wheel(totalDeltaY, { step = 240, pauseMs = 45 } = {}) {
|
|
232
|
+
const { width, height } = this.viewport;
|
|
233
|
+
const dir = Math.sign(totalDeltaY) || 1;
|
|
234
|
+
let left = Math.abs(totalDeltaY);
|
|
235
|
+
while (left > 0) {
|
|
236
|
+
const d = Math.min(step, left);
|
|
237
|
+
await this.send('Input.dispatchMouseEvent', { type: 'mouseWheel', x: Math.round(width / 2), y: Math.round(height / 2), deltaX: 0, deltaY: d * dir }).catch(() => {});
|
|
238
|
+
left -= d;
|
|
239
|
+
await sleep(pauseMs);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async close() {
|
|
244
|
+
try { await this.browser.send('Target.closeTarget', { targetId: this.targetId }, undefined, 5000); } catch {}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export { sleep };
|