pw-repl 0.12.8 → 0.13.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/AGENTS.md +10 -6
- package/README.md +2 -2
- package/bin/pw-repl.js +12 -10
- package/lib/background.js +2 -2
- package/lib/capture.js +11 -12
- package/lib/cli-names.js +1 -1
- package/lib/commands.js +3 -0
- package/lib/help.js +53 -27
- package/lib/inspect.js +82 -37
- package/lib/output.js +10 -3
- package/lib/profile.js +16 -14
- package/lib/record.js +17 -13
- package/lib/routes.js +8 -9
- package/lib/runner.js +28 -9
- package/lib/send.js +16 -6
- package/lib/server.js +4 -1
- package/lib/state.js +26 -9
- package/lib/toast.js +1 -6
- package/lib/util.js +25 -1
- package/lib/watch.js +4 -2
- package/lib/written.js +67 -0
- package/package.json +1 -1
- package/skill/SKILL.md +12 -4
package/lib/send.js
CHANGED
|
@@ -93,6 +93,15 @@ function doubleQuote(word) {
|
|
|
93
93
|
return `"${word.replace(/[\\"]/g, '\\$&').replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/\t/g, '\\t')}"`;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
// Where eval's --file is, after its --timeout if it has one; -1 when it has none.
|
|
97
|
+
function evalFileAt(words) {
|
|
98
|
+
if (words[0] !== 'eval') return -1;
|
|
99
|
+
let at = 1;
|
|
100
|
+
if (/^--timeout=/.test(words[at] || '')) at += 1;
|
|
101
|
+
else if (words[at] === '--timeout') at += 2;
|
|
102
|
+
return /^--file(?:=|$)/.test(words[at] || '') ? at : -1;
|
|
103
|
+
}
|
|
104
|
+
|
|
96
105
|
// A command's words as send was given them, made into the line the REPL reads: quoted again where the
|
|
97
106
|
// shell had kept a word whole, and with the files it reads or writes made absolute from the sender's
|
|
98
107
|
// folder, since the REPL's may be another.
|
|
@@ -104,7 +113,7 @@ function commandLine(words) {
|
|
|
104
113
|
const { SELECTOR_FIRST } = require('./syntax');
|
|
105
114
|
// So does playwright-cli's storage key (localstorage-set "my key" v).
|
|
106
115
|
// And record's file, which may have spaces in it, and wait's text, whose last word would otherwise be
|
|
107
|
-
// taken as the
|
|
116
|
+
// taken as the time when it is a number (wait text 'Showing 1').
|
|
108
117
|
const requote = words.length > 1 && (SELECTOR_FIRST.has(words[0]) || words[0] === 'drag' || /^(?:local|session)storage-/.test(words[0]) || words[0] === 'record' || words[0] === 'wait' || /^video-(?:start|stop)$/.test(words[0]));
|
|
109
118
|
// An empty word (fill #name "") is the empty value.
|
|
110
119
|
// upload's files are read by the REPL, whose folder may not be this one.
|
|
@@ -123,10 +132,11 @@ function commandLine(words) {
|
|
|
123
132
|
if (words[0] === 'profile' && words[1] === 'save' && words.length > 2) resolved = ['profile', 'save', resolve(words.slice(2).join(' '))];
|
|
124
133
|
if (words[0] === 'watch' && words[1] === 'save' && words.length > 2) resolved = ['watch', 'save', resolve(words.slice(2).join(' '))];
|
|
125
134
|
// So does the file eval --file reads.
|
|
126
|
-
|
|
135
|
+
const at = evalFileAt(words);
|
|
136
|
+
if (at > 0) {
|
|
127
137
|
// As one word, --file=<path>, which is quoted below when the path has spaces.
|
|
128
|
-
const [file, rest] = words[
|
|
129
|
-
if (file) resolved = [
|
|
138
|
+
const [file, rest] = words[at] === '--file' ? [words[at + 1], words.slice(at + 2)] : [words[at].slice(7), words.slice(at + 1)];
|
|
139
|
+
if (file) resolved = [...words.slice(0, at), `--file=${resolve(file)}`, ...rest];
|
|
130
140
|
}
|
|
131
141
|
if (words[0] === 'screenshot') {
|
|
132
142
|
resolved = words.map((w, n) => (w.startsWith('--filename=') && w.length > 11 ? `--filename=${resolve(w.slice(11))}` : words[n - 1] === '--filename' ? resolve(w) : w));
|
|
@@ -167,7 +177,7 @@ async function sendFile(options) {
|
|
|
167
177
|
// Read as the prompt reads it; a file it reads or writes is the sender's, as on send's command line.
|
|
168
178
|
const words = shellWords(line);
|
|
169
179
|
// eval's own line keeps its quotes; only eval --file names a file.
|
|
170
|
-
const command = PATHS.test(words[0]) || (words
|
|
180
|
+
const command = PATHS.test(words[0]) || evalFileAt(words) > 0 ? commandLine(words) : line;
|
|
171
181
|
const found = options.noVars ? null : variablesFor(command);
|
|
172
182
|
if (found?.error) { console.error(`pw-repl: stopped at line ${i + 1} of ${options.file} (${line}): ${found.error}`); return 1; }
|
|
173
183
|
const status = await send({ ...options, file: null, command, vars: found?.vars || null });
|
|
@@ -194,7 +204,7 @@ async function send(options) {
|
|
|
194
204
|
if (found?.error) return fail(found.error);
|
|
195
205
|
options = { ...options, vars: found?.vars || null };
|
|
196
206
|
}
|
|
197
|
-
const timeoutMs = options.timeout
|
|
207
|
+
const timeoutMs = options.timeout;
|
|
198
208
|
const way = route(options);
|
|
199
209
|
const name = options.client || process.env.PW_CLIENT || null;
|
|
200
210
|
if (name && !/^[A-Za-z0-9][A-Za-z0-9._-]{0,39}$/.test(name)) return fail('a client name is 1-40 letters, digits, dots, dashes and underscores');
|
package/lib/server.js
CHANGED
|
@@ -56,7 +56,10 @@ function handle(req, res) {
|
|
|
56
56
|
}
|
|
57
57
|
// Bound before the browser is reached, so where and send find it while it starts.
|
|
58
58
|
if (!state.serving) return send(res, 503, { status: 'error', starting: true, output: `The REPL is starting (${state.startingStep}); retry shortly` });
|
|
59
|
-
|
|
59
|
+
// The sender gave up (send -t) once its connection closes before the answer is written.
|
|
60
|
+
const gone = new AbortController();
|
|
61
|
+
res.once('close', () => { if (!res.writableEnded) gone.abort(); });
|
|
62
|
+
const result = await runner.submit(command.trim(), client || null, vars || null, gone.signal);
|
|
60
63
|
if (!result.uncertain) return send(res, 200, { status: result.status, output: result.output });
|
|
61
64
|
// unconfirmed: the command may or may not have done what it was sent to do.
|
|
62
65
|
send(res, 200, { status: result.status, output: result.output, unconfirmed: true });
|
package/lib/state.js
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
// Session state shared by the entry point, the runner, and the commands, plus
|
|
2
2
|
// the shutdown that ends the session.
|
|
3
|
+
const { AsyncLocalStorage } = require('async_hooks');
|
|
3
4
|
const out = require('./output');
|
|
4
5
|
|
|
5
6
|
const SHUTDOWN_TIMEOUT = 5000;
|
|
6
7
|
|
|
7
8
|
const state = {
|
|
8
9
|
browser: null,
|
|
9
|
-
// The client whose command runs now: null for the prompt and senders without
|
|
10
|
-
// a name, who share one selected tab, as before clients had names.
|
|
11
|
-
client: null,
|
|
12
10
|
rl: null,
|
|
13
11
|
promptBase: 'pw> ',
|
|
14
12
|
// The browser the REPL is connected to, and whether --launch started it.
|
|
@@ -36,18 +34,31 @@ function clientRecord(name = state.client) {
|
|
|
36
34
|
return clients.get(name);
|
|
37
35
|
}
|
|
38
36
|
|
|
37
|
+
// The command running in this async context: its client, and whether its sender gave up on it.
|
|
38
|
+
// Code outside any command (a browser event, quit and dialog, which skip the queue) goes by the
|
|
39
|
+
// queued command running now.
|
|
40
|
+
const running = new AsyncLocalStorage();
|
|
41
|
+
let queued = null;
|
|
42
|
+
|
|
43
|
+
// The client whose command runs now: null for the prompt and senders without a name, who share one
|
|
44
|
+
// selected tab, as before clients had names. Each command keeps its own for its whole life, so one
|
|
45
|
+
// its sender gave up on, still running, never acts as the command after it.
|
|
46
|
+
Object.defineProperty(state, 'client', { enumerable: true, get: () => (running.getStore() ?? queued)?.client ?? null });
|
|
47
|
+
|
|
48
|
+
out.onCancelled(() => running.getStore()?.cancelled === true);
|
|
49
|
+
|
|
39
50
|
for (const key of Object.keys(PER_CLIENT)) {
|
|
40
51
|
Object.defineProperty(state, key, { enumerable: true, get: () => clientRecord()[key], set: value => { clientRecord()[key] = value; } });
|
|
41
52
|
}
|
|
42
53
|
|
|
43
|
-
// Runs fn as the named client.
|
|
44
|
-
async function asClient(name, fn) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
try { return await fn(); } finally { state.client = previous; }
|
|
54
|
+
// Runs fn as the named client. run is the command's record, whose cancelled the runner sets.
|
|
55
|
+
async function asClient(name, fn, run = {}) {
|
|
56
|
+
queued = Object.assign(run, { client: name || null });
|
|
57
|
+
try { return await running.run(run, fn); } finally { if (queued === run) queued = null; }
|
|
48
58
|
}
|
|
49
59
|
|
|
50
60
|
const cleanups = [];
|
|
61
|
+
const endings = [];
|
|
51
62
|
const exitWaits = [];
|
|
52
63
|
let shutdownPromise = null;
|
|
53
64
|
|
|
@@ -65,6 +76,11 @@ function onShutdown(fn) {
|
|
|
65
76
|
cleanups.push(fn);
|
|
66
77
|
}
|
|
67
78
|
|
|
79
|
+
// Run once every cleanup has finished, before the browser is closed.
|
|
80
|
+
function onShutdownEnd(fn) {
|
|
81
|
+
endings.push(fn);
|
|
82
|
+
}
|
|
83
|
+
|
|
68
84
|
// Things to finish before the process exits, e.g. answers still being sent.
|
|
69
85
|
function onBeforeExit(fn) {
|
|
70
86
|
exitWaits.push(fn);
|
|
@@ -80,6 +96,7 @@ async function shutdown() {
|
|
|
80
96
|
shutdownPromise = (async () => {
|
|
81
97
|
// Some undo what the REPL changed in the browser, so they finish first.
|
|
82
98
|
await withTimeout(Promise.all(cleanups.map(async fn => fn())), 'Cleanup').catch(() => {});
|
|
99
|
+
for (const fn of endings) try { await fn(); } catch {}
|
|
83
100
|
if (state.browser) {
|
|
84
101
|
try { await withTimeout(state.browser.close(), 'Chromium shutdown'); }
|
|
85
102
|
catch (error) { state.shutdownFailed = true; process.exitCode = 1; out.error(`Could not confirm Chromium shutdown: ${error.message}`); }
|
|
@@ -88,4 +105,4 @@ async function shutdown() {
|
|
|
88
105
|
return shutdownPromise;
|
|
89
106
|
}
|
|
90
107
|
|
|
91
|
-
module.exports = { state, clientRecord, asClient, withTimeout, onShutdown, onBeforeExit, beforeExit, shutdown, SHUTDOWN_TIMEOUT };
|
|
108
|
+
module.exports = { state, clientRecord, asClient, withTimeout, onShutdown, onShutdownEnd, onBeforeExit, beforeExit, shutdown, SHUTDOWN_TIMEOUT };
|
package/lib/toast.js
CHANGED
|
@@ -7,6 +7,7 @@ const { unquote } = require('./syntax');
|
|
|
7
7
|
const { takeOptions } = require('./cli-names');
|
|
8
8
|
const { inOverlay, keepAcrossPages, forget } = require('./overlay');
|
|
9
9
|
const { recordingOf, beforeRecordingEnds } = require('./record');
|
|
10
|
+
const { parseDuration } = require('./util');
|
|
10
11
|
|
|
11
12
|
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
12
13
|
|
|
@@ -81,12 +82,6 @@ async function toastOff(p, fade = true) {
|
|
|
81
82
|
beforeRecordingEnds(p => (p && !p.isClosed() ? waitToRead(p) : null));
|
|
82
83
|
|
|
83
84
|
// 3s, 1.5s, 800ms, or a number of milliseconds.
|
|
84
|
-
function parseDuration(value) {
|
|
85
|
-
const match = /^(\d+(?:\.\d+)?)(ms|s)?$/.exec(value || '');
|
|
86
|
-
if (!match) return null;
|
|
87
|
-
return Math.round(Number(match[1]) * (match[2] === 's' ? 1000 : 1));
|
|
88
|
-
}
|
|
89
|
-
|
|
90
85
|
onShutdown(async () => {
|
|
91
86
|
if (!state.browser || state.connectionLost) return;
|
|
92
87
|
const pages = state.browser.contexts().flatMap(c => c.pages()).filter(p => !p.isClosed());
|
package/lib/util.js
CHANGED
|
@@ -20,6 +20,28 @@ function hints(pairs) {
|
|
|
20
20
|
return `\n${pairs.map(([command, what]) => ` ${command.padEnd(width)} ${what}`).join('\n')}`;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
// A time: milliseconds, as Playwright and JavaScript count it, or seconds with an s (1500, 1.5s, 1500ms).
|
|
24
|
+
const TIME = /^(\d+(?:\.\d+)?)(ms|s)?$/;
|
|
25
|
+
|
|
26
|
+
function parseDuration(value) {
|
|
27
|
+
const match = TIME.exec(value || '');
|
|
28
|
+
if (!match) return null;
|
|
29
|
+
return Math.round(Number(match[1]) * (match[2] === 's' ? 1000 : 1));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// How a time reads back: 1.5s, or 250ms under a second.
|
|
33
|
+
function durationText(ms) {
|
|
34
|
+
return ms >= 1000 ? `${Number((ms / 1000).toFixed(3))}s` : `${ms}ms`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// A time that is a second or more, where a smaller one is most likely seconds written as before.
|
|
38
|
+
function longDuration(value, what) {
|
|
39
|
+
const ms = parseDuration(value);
|
|
40
|
+
if (ms === null) throw new Error(`${what} is a time, in milliseconds or with s for seconds: 30000 or 30s (got ${value})`);
|
|
41
|
+
if (ms < 1000) throw new Error(`${what} is in milliseconds, 1000 or more: ${value} is ${durationText(ms)}; ${value}s for seconds`);
|
|
42
|
+
return ms;
|
|
43
|
+
}
|
|
44
|
+
|
|
23
45
|
function clipText(text) {
|
|
24
46
|
return text.length > MAX_EVENT_TEXT ? `${text.slice(0, MAX_EVENT_TEXT)}…` : text;
|
|
25
47
|
}
|
|
@@ -56,6 +78,8 @@ function clock(t) {
|
|
|
56
78
|
// by itself unseen: stop prints those lines.
|
|
57
79
|
const STOPPED_RECORDING = 'The REPL stopped, which ended a recording';
|
|
58
80
|
const ENDED_UNSEEN = 'A recording ended before the REPL stopped, unseen since';
|
|
81
|
+
// Each line stop prints of the files a client left.
|
|
82
|
+
const FILES_LEFT = 'Files left';
|
|
59
83
|
|
|
60
84
|
// Where Playwright installs its browsers and ffmpeg, worked out as playwright-core does it: a cache of
|
|
61
85
|
// each platform's own (~/Library/Caches on macOS), unless PLAYWRIGHT_BROWSERS_PATH says otherwise.
|
|
@@ -69,4 +93,4 @@ function playwrightBrowsersDir() {
|
|
|
69
93
|
return path.join(cache, 'ms-playwright');
|
|
70
94
|
}
|
|
71
95
|
|
|
72
|
-
module.exports = { BUSY_PAGE, STOPPED_RECORDING, ENDED_UNSEEN, COMMAND_TIMEOUT, MAX_EVENT_TEXT, NO_TAB, hints, clipText, numbers, regexOf, patternText, clock, playwrightBrowsersDir };
|
|
96
|
+
module.exports = { TIME, parseDuration, durationText, longDuration, BUSY_PAGE, STOPPED_RECORDING, ENDED_UNSEEN, FILES_LEFT, COMMAND_TIMEOUT, MAX_EVENT_TEXT, NO_TAB, hints, clipText, numbers, regexOf, patternText, clock, playwrightBrowsersDir };
|
package/lib/watch.js
CHANGED
|
@@ -4,6 +4,7 @@ const out = require('./output');
|
|
|
4
4
|
const { printOutput } = out;
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
|
+
const { noteWritten } = require('./written');
|
|
7
8
|
const { clipText, clock, hints, NO_TAB } = require('./util');
|
|
8
9
|
const { tabState } = require('./tabstate');
|
|
9
10
|
const { keptSession, keepDrawing } = require('./cdp');
|
|
@@ -22,8 +23,8 @@ const PW_SETTINGS = ['PW_CDP_URL', 'PW_CHROME', 'PW_CLIENT', 'PW_ENDPOINT', 'PW_
|
|
|
22
23
|
'PW_SOCKET', 'PW_START_URL', 'PW_TEST_CHROME', 'PW_TMUX_SESSION'];
|
|
23
24
|
const PAGE_ACTIONS = new Set(['click', 'check', 'uncheck', 'select', 'fill', 'type', 'press', 'submit']);
|
|
24
25
|
const WATCH_BINDING = '__pwReplWatch';
|
|
25
|
-
//
|
|
26
|
-
const ROUTE_WAIT =
|
|
26
|
+
// How long a saved step waits for its element after the app's own routing (watch save).
|
|
27
|
+
const ROUTE_WAIT = '30s';
|
|
27
28
|
|
|
28
29
|
// Runs in the page. Describes elements the way snapshot does: role and name.
|
|
29
30
|
const WATCH_SCRIPT = `(() => {
|
|
@@ -811,6 +812,7 @@ const commands = {
|
|
|
811
812
|
if (variables.length) header.push(`# It needs ${variables.join(', ')} set in the environment: what was typed into ${where}, not saved here`);
|
|
812
813
|
// It holds what was typed, so it is the user's own to read.
|
|
813
814
|
fs.writeFileSync(file, `${[...header, ...lines].join('\n')}\n`, { mode: 0o600, flag: 'wx' });
|
|
815
|
+
noteWritten(file, 'watch file');
|
|
814
816
|
out.log(`Saved ${watch.events.length} step${watch.events.length === 1 ? '' : 's'} to ${file}${skipped ? `; ${skipped} could not be written as commands, and are comments there saying why` : ''}.`);
|
|
815
817
|
if (variables.length) out.log(`What was typed into ${where} is a variable there, filled in from the environment: ${variables.map(name => `{{ ${name} }}`).join(', ')}.`);
|
|
816
818
|
out.log(`${variables.map(name => `${name}=... `).join('')}pw-repl send --file ${file} runs them.`);
|
package/lib/written.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// The files the REPL wrote this session, and for whom, so close and stop can say which are still
|
|
2
|
+
// there to clean up. Kept in memory only, and nothing is removed.
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { state, onShutdownEnd } = require('./state');
|
|
6
|
+
const out = require('./output');
|
|
7
|
+
const { FILES_LEFT } = require('./util');
|
|
8
|
+
|
|
9
|
+
const written = new Map();
|
|
10
|
+
|
|
11
|
+
// kind: what it is, as the summary counts it (screenshot, video, steps file, profile, watch file).
|
|
12
|
+
function noteWritten(file, kind, client = state.client) {
|
|
13
|
+
written.set(path.resolve(file), { kind, client: client ?? null });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// A file renamed (record off <file>) is still the same one.
|
|
17
|
+
function noteMoved(from, to) {
|
|
18
|
+
const entry = written.get(path.resolve(from));
|
|
19
|
+
if (!entry) return;
|
|
20
|
+
written.delete(path.resolve(from));
|
|
21
|
+
written.set(path.resolve(to), entry);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function remaining(client) {
|
|
25
|
+
const files = [];
|
|
26
|
+
for (const [file, entry] of written) {
|
|
27
|
+
if (entry.client !== (client ?? null)) continue;
|
|
28
|
+
try { files.push({ file, kind: entry.kind, size: fs.statSync(file).size }); } catch {}
|
|
29
|
+
}
|
|
30
|
+
return files;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function sizeText(bytes) {
|
|
34
|
+
if (bytes < 1024 * 1024) return `${Math.max(1, Math.round(bytes / 1024))} KB`;
|
|
35
|
+
return `${Number((bytes / 1024 / 1024).toFixed(1))} MB`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// For close: the client's own files, in full.
|
|
39
|
+
function ownFilesText(client) {
|
|
40
|
+
const files = remaining(client);
|
|
41
|
+
if (!files.length) return null;
|
|
42
|
+
return [`Files you wrote that are still there; remove what is no longer needed:`, ...files.map(f => ` ${f.file} (${sizeText(f.size)})`)].join('\n');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const plural = (n, kind) => `${n} ${kind}${n === 1 ? '' : 's'}`;
|
|
46
|
+
|
|
47
|
+
// For stop: one line for each client that left files, each with what and where.
|
|
48
|
+
function summaryLines() {
|
|
49
|
+
const clients = [...new Set([...written.values()].map(e => e.client))];
|
|
50
|
+
const lines = [];
|
|
51
|
+
for (const client of clients) {
|
|
52
|
+
const files = remaining(client);
|
|
53
|
+
if (!files.length) continue;
|
|
54
|
+
const kinds = new Map();
|
|
55
|
+
for (const f of files) kinds.set(f.kind, (kinds.get(f.kind) || 0) + 1);
|
|
56
|
+
const folders = [...new Set(files.map(f => `${path.dirname(f.file)}/`))];
|
|
57
|
+
const bytes = files.reduce((sum, f) => sum + f.size, 0);
|
|
58
|
+
const who = client ? `by ${client}` : 'without a client name';
|
|
59
|
+
lines.push(`${FILES_LEFT} ${who}: ${plural(files.length, 'file')}, ${sizeText(bytes)} (${[...kinds].map(([k, n]) => plural(n, k)).join(', ')}) in ${folders.join(' and ')}`);
|
|
60
|
+
}
|
|
61
|
+
return lines;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// After every recording has been saved, so its file is whole.
|
|
65
|
+
onShutdownEnd(() => { for (const line of summaryLines()) out.log(line); });
|
|
66
|
+
|
|
67
|
+
module.exports = { noteWritten, noteMoved, ownFilesText, summaryLines };
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pw-repl
|
|
3
|
-
description: Inspect and drive a Chromium browser, possibly one the user is using, through the pw-repl REPL - tabs, page snapshots, clicks and typing, the mouse at a point (moves, buttons, wheel), requests and their bodies, console messages, waiting for pages and elements, choosing files, faking, patching or delaying responses, cutting or slowing the network, emulating a phone, dark mode, a locale or a timezone, recording what the user does, profiling what makes a page slow (long tasks, forced layouts, input delay, dropped frames), and recording a video of a tab, with a pointer and on-screen captions. Use when asked to look at, debug or test something in a web page, in an existing Chromium with remote debugging or in one it starts itself.
|
|
3
|
+
description: Inspect and drive a Chromium browser, possibly one the user is using, through the pw-repl REPL - tabs, page snapshots, clicks and typing, the mouse at a point (moves, buttons, wheel), requests and their bodies, console messages, what a page copied to the clipboard, waiting for pages and elements, choosing files, faking, patching or delaying responses, cutting or slowing the network, emulating a phone, dark mode, a locale or a timezone, recording what the user does, profiling what makes a page slow (long tasks, forced layouts, input delay, dropped frames), and recording a video of a tab, with a pointer and on-screen captions. Use when asked to look at, debug or test something in a web page, in an existing Chromium with remote debugging or in one it starts itself.
|
|
4
4
|
allowed-tools: Bash(pw-repl:*) Bash(npx pw-repl@latest:*)
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -93,7 +93,7 @@ client of your own: `PW_CLIENT=<name>` (or `send -c <name>`), with a name that s
|
|
|
93
93
|
as `[server:<name>]` lines.
|
|
94
94
|
- `modes` and `tab` say which client turned each mode on and opened each tab. `modes off --mine` turns
|
|
95
95
|
off only what you turned on; `tab close <url-part>` closes a tab you opened.
|
|
96
|
-
- Commands run one at a time, from every client: a long one (`capture on
|
|
96
|
+
- Commands run one at a time, from every client: a long one (`capture on 60s`, `sleep`) holds the rest.
|
|
97
97
|
|
|
98
98
|
A REPL of your own, on a socket of its own, is for a browser of your own (`--launch`), or when you are
|
|
99
99
|
asked for one. Several REPLs on one browser each see every tab but list only their own modes, and can
|
|
@@ -105,16 +105,19 @@ A REPL in a terminal stops at its prompt (`quit`, or Ctrl-C); `send quit` is ref
|
|
|
105
105
|
|
|
106
106
|
```bash
|
|
107
107
|
pw-repl send tab
|
|
108
|
-
pw-repl send -t
|
|
108
|
+
pw-repl send -t 90s 'screenshot -d 60s' # wait longer than the 20s default
|
|
109
109
|
```
|
|
110
110
|
|
|
111
|
+
Times, in send's `-t` and in every command, are in milliseconds, as in Playwright, or in seconds with an
|
|
112
|
+
`s`: `-t 90000` or `-t 90s`.
|
|
113
|
+
|
|
111
114
|
Always send commands with `pw-repl send`; don't type into the pane yourself, except `quit` at the prompt
|
|
112
115
|
of a REPL of your own, which `send` refuses. The words after `send` are the command. For `fill`, `type`,
|
|
113
116
|
`select`, `press`, `upload` and `wait`, a word quoted in your shell stays one word
|
|
114
117
|
(`pw-repl send fill "text=Your name" Ada`, `pw-repl send wait text "Showing 1"`); other commands get the words as they are, joined by
|
|
115
118
|
spaces (`pw-repl send eval "document.title + ' x'"`). A command that takes only a selector takes the
|
|
116
119
|
whole line, spaces and all: `pw-repl send click "text=Your name"`. `send`'s own options (`-t`, `-c`, `-e`,
|
|
117
|
-
`-s`, `--no-vars`) go before the command or after it (`pw-repl send open <url> -t
|
|
120
|
+
`-s`, `--no-vars`) go before the command or after it (`pw-repl send open <url> -t 30s`); after `--`, every
|
|
118
121
|
word is the command's. It works however the REPL was started:
|
|
119
122
|
|
|
120
123
|
- `run` (in tmux): `send` types the command into the tmux pane and reads the result back off the screen.
|
|
@@ -227,6 +230,9 @@ pw-repl send -c <name> close # your modes off; tab close
|
|
|
227
230
|
lists what is on in every tab (the prompt shows the selected tab's, e.g. `(watch routes:1) pw>`);
|
|
228
231
|
`modes off` turns off every mode in every tab.
|
|
229
232
|
- Someone may be attached to the REPL's tmux session; killing the session ends it for them too.
|
|
233
|
+
- What a page copied (a Copy button): click it, then `clipboard` prints it. In a headless browser
|
|
234
|
+
(`--launch`) that is the browser's own clipboard. In one with a window it is the user's, which may
|
|
235
|
+
hold a password, so `clipboard` refuses there without `--shared-clipboard`: ask the user first.
|
|
230
236
|
- To point something out, to the user or in a screenshot, `highlight <ref|selector>` draws a box over it
|
|
231
237
|
without changing the page (`--style=<css>` restyles it; `highlight off` hides them), without
|
|
232
238
|
Playwright's locator label unless `highlight --labels on`.
|
|
@@ -249,6 +255,8 @@ pw-repl send -c <name> close # your modes off; tab close
|
|
|
249
255
|
not by `send`. Without it, a REPL on a socket of its own saves them next to its socket, and any other
|
|
250
256
|
in `/tmp`. `screenshot` prints each file's path; other REPLs may save theirs in the same place. Remove
|
|
251
257
|
only your own, and keep those too if they are needed, e.g. as evidence or as something to hand over.
|
|
258
|
+
`close` (as a client, `-c <name>`) lists every file you wrote that is still there: screenshots,
|
|
259
|
+
videos, saved profiles and watches.
|
|
252
260
|
- `PW_ENDPOINT` / `PW_TMUX_SESSION` — defaults for `send -e` / `-s`. `PW_SOCKET` — the socket `serve`
|
|
253
261
|
serves on and `send` looks for when neither is given (default `/tmp/pw-repl.sock`).
|
|
254
262
|
|