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 CHANGED
@@ -23,7 +23,7 @@ If you hit a limitation or write a workaround, consider adding the capability to
23
23
  `lib/routes.js`, `lib/network.js`, `lib/emulation.js` (and viewport), `lib/capture.js`, `lib/profile.js` (performance traces;
24
24
  `lib/trace.js` reads and sums them up), `lib/record.js`
25
25
  (video, through ffmpeg, and the steps file saved with it), `lib/dialogs.js` and `lib/modes.js`. Beneath them, `lib/tabstate.js` is the one record kept per tab, `lib/cdp.js` the CDP
26
- session kept per tab, and `lib/util.js` small shared helpers. No facility requires `lib/modes.js` or
26
+ session kept per tab, `lib/util.js` small shared helpers, and `lib/written.js` the files written and for whom. No facility requires `lib/modes.js` or
27
27
  `lib/commands.js`, and a unit test fails on a require cycle, which CommonJS would not report.
28
28
  - `bin/pw-repl.js` is the command line; `lib/start.js` connects and runs the prompt; `lib/runner.js`
29
29
  runs commands one at a time (`quit` and `dialog` skip its queue); `lib/server.js` is the command
@@ -39,7 +39,7 @@ If you hit a limitation or write a workaround, consider adding the capability to
39
39
  accepts theirs too (`lib/cli-names.js`, `help playwright-cli`). Deviate only where it is necessary, as
40
40
  where sharing a browser with a user needs it, and say why in the help.
41
41
  - Comment the *why* when it isn't obvious from the code.
42
- - `npm test` runs the suite (under two minutes): a private headless Chromium, a local test site
42
+ - `npm test` runs the suite (about two and a half minutes): a private headless Chromium, a local test site
43
43
  (`test/harness.js`), and the real REPL with its server. Nothing is mocked, and the shared browser is
44
44
  never touched. It finds Chromium through `PW_TEST_CHROME`, or where `--launch` looks (Playwright's
45
45
  browsers, then the `PATH`), and skips the browser tests if there is none. Add a test with each new
@@ -105,10 +105,14 @@ Commits follow [Conventional Commits](https://www.conventionalcommits.org/): `fe
105
105
  1.0, a change that breaks how the REPL is used is a minor bump (0.2.0 to 0.3.0), anything else a patch.
106
106
 
107
107
  1. `npm test` passes and the working tree is clean.
108
- 2. `npm outdated` and `npm audit` show nothing that needs doing first. The lockfile holds playwright-core
109
- at 1.63: on 1.64 (and 1.64.1), a click on a link to a 204 waits out its timeout instead of returning (the test
110
- "waits for the page to load, counting a navigation that just began" fails). Update it once a version
111
- passes `npm test`.
108
+ 2. `npm outdated` and `npm audit` show nothing that needs doing first. Dependencies are always the newest
109
+ stable release, never pinned back (user ruling). When the newest breaks a test, fix it in our code;
110
+ only a regression of the dependency's own, with the fix already in its next release, keeps the version
111
+ before it, until that release ships. playwright-core stays at 1.63 for that reason: on 1.64.1 a click
112
+ on a link to a 204 hangs at "waiting for scheduled navigations to finish" until its timeout, in plain
113
+ Playwright too (chromium.launch, a page with a link to a 204, page.click on it); 1.65's alphas fix it.
114
+ The test "waits for the page to load, counting a navigation that just began" shows it: move up once it
115
+ passes on the newest release.
112
116
  3. `npm version <patch|minor|major> -m "chore: release %s"` sets the version in `package.json` and
113
117
  `package-lock.json`, commits it as `chore: release X.Y.Z`, and tags that commit `vX.Y.Z`.
114
118
  4. `git push --follow-tags` pushes the commits and the tag.
package/README.md CHANGED
@@ -83,7 +83,7 @@ With `npx`, use `npx pw-repl@latest` the same way every time, so every command r
83
83
  whole tab's network.
84
84
  - **Emulate:** a phone, dark mode, a locale, a timezone, a viewport size.
85
85
  - **Run code:** `eval` any JavaScript in the page, a line or a file of it, or send a raw CDP command with
86
- `cdp`.
86
+ `cdp`; read what a Copy button put on the clipboard (`clipboard`).
87
87
  - **Wait:** for an element, some text, a response or a page load.
88
88
  - **Record:** what someone does in a tab (`watch`), a timeline of requests and console (`capture`), or a
89
89
  video of the tab (`record`), with a virtual pointer (`cursor`), captions (`toast`) and highlights
@@ -154,7 +154,7 @@ example the Docker bridge gateway, `ip route | awk '/default/ {print $3}'`).
154
154
  | record requests and console together | `capture on`, then `capture off` |
155
155
  | find what makes the page slow | `profile on`, then `profile off`; `profile reload` for a load |
156
156
  | fake a backend response | `route <glob> <status> <json>`, `route <glob> abort` |
157
- | change or slow an API response | `route <glob> patch <json>`, `route <glob> delay <secs>` |
157
+ | change or slow an API response | `route <glob> patch <json>`, `route <glob> delay <time>` |
158
158
  | cut or slow the whole network | `network off`, `network slow`, then `network on` |
159
159
  | record a video of the page | `record on`, then `record off` (`help video` for a take) |
160
160
  | show a pointer where the agent acts | `cursor on`, then `cursor off` |
package/bin/pw-repl.js CHANGED
@@ -2,6 +2,7 @@
2
2
  // The one command: pw-repl run | serve | attach | stop | send | where | help | skill.
3
3
 
4
4
  const { looksLikeEndpoint } = require('../lib/client');
5
+ const { longDuration } = require('../lib/util');
5
6
 
6
7
  const USAGE = `Usage:
7
8
  pw-repl run [--launch [--headed]] [start-url] [-- <chromium flags>]
@@ -19,7 +20,7 @@ const USAGE = `Usage:
19
20
  stop a background REPL, and say what became of each recording it ended; its socket is removed, its
20
21
  log stays
21
22
 
22
- pw-repl send [-e endpoint | -s session] [-c client] [-t seconds] [--no-vars]
23
+ pw-repl send [-e endpoint | -s session] [-c client] [-t time] [--no-vars]
23
24
  <command...> | --file <file>
24
25
  run one command in a running REPL and print its output; --file runs a file of them, one per line
25
26
 
@@ -56,12 +57,14 @@ serves on that same one: $PW_ENDPOINT, then $PW_SOCKET, then the default.
56
57
  send -c <client> (or $PW_CLIENT) sends as a client of the REPL's with a selected tab of its own, so
57
58
  several agents can share one REPL; pw-repl help session has the rest.
58
59
 
59
- send's options can also follow the command, as in pw-repl send open <url> -t 30; after a -- before the
60
- command, every word is the command's, -t 30 included.
60
+ send's options can also follow the command, as in pw-repl send open <url> -t 30s; after a -- before the
61
+ command, every word is the command's, -t 30s included.
61
62
 
62
63
  Exit status: 0 ok, 1 command error, 2 completion not confirmed, 64 usage or unreachable. -t is how long
63
- send waits for the answer (default 20s); it does not lengthen a command's own limits, such as the 5s an
64
- action waits for its element.
64
+ send waits for the answer, in milliseconds or with s for seconds (default 20s: 20000); it does not
65
+ lengthen a command's own limits, such as the 5s an action waits for its element. Over the server, giving
66
+ up cancels the command: one still queued is never run, and one running is no longer waited for, though
67
+ what it began in the page may go on.
65
68
 
66
69
  --file <file> runs each line of a file as a command, in order, as if typed at the REPL's prompt: quoted
67
70
  as the prompt reads it (no shell), with a file it records, uploads or saves relative to this folder, as
@@ -105,10 +108,10 @@ function shellLine(words) {
105
108
  }
106
109
 
107
110
  // -e, -s, -c and -t, then the command words (unquoted words are one command), and send's options again
108
- // at the end, as an agent types them (send open <url> -t 30); after --, every word is the command.
111
+ // at the end, as an agent types them (send open <url> -t 30s); after --, every word is the command.
109
112
  function parseSendArgs(args, subcommand) {
110
113
  const allowCommand = subcommand === 'send';
111
- const options = { endpoint: null, session: null, client: null, timeout: 20, command: '', file: null };
114
+ const options = { endpoint: null, session: null, client: null, timeout: 20000, command: '', file: null };
112
115
  const given = new Set();
113
116
  const take = (arg, value, after) => {
114
117
  if (value === undefined) usage(`${arg} needs a value`, subcommand);
@@ -120,8 +123,7 @@ function parseSendArgs(args, subcommand) {
120
123
  if (arg === '-s') options.session = value;
121
124
  if (arg === '-c') options.client = value;
122
125
  if (arg === '-t') {
123
- if (!/^\d+$/.test(value) || Number(value) < 1) usage(`-t takes whole seconds, 1 or more (got ${value})`, subcommand);
124
- options.timeout = Number(value);
126
+ try { options.timeout = longDuration(value, '-t'); } catch (error) { usage(error.message, subcommand); }
125
127
  }
126
128
  };
127
129
  let i = 0;
@@ -296,7 +298,7 @@ async function main() {
296
298
  case '--help':
297
299
  return console.log(`${USAGE}\n\n${REPL_HELP}`);
298
300
  default: {
299
- // An option before the subcommand (pw-repl -t 30 send ...): say where it goes, with the line fixed.
301
+ // An option before the subcommand (pw-repl -t 30s send ...): say where it goes, with the line fixed.
300
302
  if (subcommand.startsWith('-')) {
301
303
  const all = [subcommand, ...args];
302
304
  const named = all.find(word => SUBCOMMANDS.includes(word));
package/lib/background.js CHANGED
@@ -5,7 +5,7 @@ const path = require('path');
5
5
  const readline = require('readline');
6
6
  const { spawn } = require('child_process');
7
7
  const client = require('./client');
8
- const { STOPPED_RECORDING, ENDED_UNSEEN } = require('./util');
8
+ const { STOPPED_RECORDING, ENDED_UNSEEN, FILES_LEFT } = require('./util');
9
9
 
10
10
  const BIN = path.join(__dirname, '..', 'bin', 'pw-repl.js');
11
11
  const START_TIMEOUT = 20000;
@@ -162,7 +162,7 @@ async function stop(options) {
162
162
  fs.closeSync(fd);
163
163
  logged = buffer.toString('utf8');
164
164
  } catch {}
165
- for (const line of logged.split('\n')) if (line.startsWith(STOPPED_RECORDING) || line.startsWith(ENDED_UNSEEN)) console.log(line);
165
+ for (const line of logged.split('\n')) if (line.startsWith(STOPPED_RECORDING) || line.startsWith(ENDED_UNSEEN) || line.startsWith(FILES_LEFT)) console.log(line);
166
166
  return 0;
167
167
  }
168
168
 
package/lib/capture.js CHANGED
@@ -2,7 +2,7 @@
2
2
  const { state, onShutdown } = require('./state');
3
3
  const out = require('./output');
4
4
  const { printOutput } = out;
5
- const { numbers, MAX_EVENT_TEXT, NO_TAB, clock, hints } = require('./util');
5
+ const { numbers, MAX_EVENT_TEXT, NO_TAB, clock, hints, TIME, longDuration, durationText } = require('./util');
6
6
  const { consoleText, requestEntries } = require('./requestlog');
7
7
 
8
8
  const MAX_CAPTURE_EVENTS = 10000;
@@ -17,16 +17,15 @@ function currentCapture() {
17
17
  }
18
18
 
19
19
  // Records the selected tab's requests and console messages together, in time
20
- // order, until capture off, or for a set number of seconds.
20
+ // order, until capture off, or for a set time.
21
21
  async function startCapture(tokens) {
22
22
  if (cap) { out.log('Already capturing; capture off stops it.'); return; }
23
23
  if (!state.page || state.page.isClosed()) throw new Error(NO_TAB);
24
- const usage = 'Usage: capture on [requests|console] [seconds] (seconds from 1 to 3600)';
24
+ const usage = 'Usage: capture on [requests|console] [time] (ms, or 30s)';
25
25
  const kinds = tokens.filter(t => t === 'requests' || t === 'console');
26
- const numbers = tokens.filter(t => /^\d+$/.test(t));
27
- if (kinds.length > 1 || numbers.length > 1 || kinds.length + numbers.length !== tokens.length) throw new Error(usage);
28
- const seconds = numbers.length ? Number(numbers[0]) : null;
29
- if (seconds !== null && (seconds < 1 || seconds > 3600)) throw new Error(usage);
26
+ const times = tokens.filter(t => TIME.test(t));
27
+ if (kinds.length > 1 || times.length > 1 || kinds.length + times.length !== tokens.length) throw new Error(usage);
28
+ const ms = times.length ? longDuration(times[0], 'capture on\'s time') : null;
30
29
  const label = kinds[0] || 'requests and console';
31
30
  const startedAt = Date.now();
32
31
  const events = [];
@@ -61,11 +60,11 @@ async function startCapture(tokens) {
61
60
  get shortened() { return shortened; },
62
61
  };
63
62
  cap = capture;
64
- if (seconds) {
65
- out.log(`Capturing ${label} for ${seconds}s...`);
63
+ if (ms) {
64
+ out.log(`Capturing ${label} for ${durationText(ms)}...`);
66
65
  // Also ends early if the capture does, e.g. because its tab closed.
67
66
  await new Promise(resolve => {
68
- const timer = setTimeout(resolve, seconds * 1000);
67
+ const timer = setTimeout(resolve, ms);
69
68
  capture.wake = () => { clearTimeout(timer); resolve(); };
70
69
  });
71
70
  if (cap === capture) stopCapture();
@@ -120,7 +119,7 @@ const commands = {
120
119
  const [sub, ...rest] = (args || '').trim().split(/\s+/).filter(Boolean);
121
120
  if (sub === 'on') return startCapture(rest);
122
121
  if (sub === 'off' && !rest.length) return stopCapture(all);
123
- if (sub) throw new Error('Usage: capture on [requests|console] [seconds] | capture off');
122
+ if (sub) throw new Error('Usage: capture on [requests|console] [time] | capture off');
124
123
  if (cap) {
125
124
  out.log(`Capturing ${cap.label} on ${cap.page.url()} since ${clock(cap.startedAt)} (${cap.events.length} so far)`);
126
125
  out.log(hints([['capture off', 'stop and print what it recorded']]));
@@ -132,7 +131,7 @@ const commands = {
132
131
  } else {
133
132
  out.log('Not capturing.');
134
133
  }
135
- out.log(hints([['capture on [requests|console] [seconds]', 'record the selected tab\'s requests and console messages in time order']]));
134
+ out.log(hints([['capture on [requests|console] [time]', 'record the selected tab\'s requests and console messages in time order']]));
136
135
  },
137
136
  };
138
137
 
package/lib/cli-names.js CHANGED
@@ -15,7 +15,7 @@ const NAMES = {
15
15
  'tab-close': [a => (a ? null : 'tab close'), 'tab close [url-part], or tab <index> then tab close'],
16
16
  open: [a => `tab new ${a}`.trim(), 'tab new [url] (goto <url> navigates); --mobile, --device=<name>: emulate mobile'],
17
17
  // It ends playwright-cli's own browser; this one is shared, so tabs stay open.
18
- close: [() => 'close', 'modes off --mine, and tabs stay open'],
18
+ close: [() => 'close', 'modes off --mine; tabs stay open; lists the files you wrote'],
19
19
  'go-back': [() => 'back', 'back'],
20
20
  'go-forward': [() => 'forward', 'forward'],
21
21
  find: [a => `snapshot --grep ${a}`, 'snapshot --grep <text>'],
package/lib/commands.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // The REPL's commands, gathered from each facility's module, and what the prompt and the runner
2
2
  // need besides: playwright-cli's own commands, the hooks every page gets, and tab completion.
3
3
  const { state, shutdown } = require('./state');
4
+ const { ownFilesText } = require('./written');
4
5
  const out = require('./output');
5
6
  const HELP = require('./help');
6
7
  const { tabState } = require('./tabstate');
@@ -80,6 +81,8 @@ const cliCommands = {
80
81
  await allModesOff(true);
81
82
  const own = state.browser.contexts().flatMap(c => c.pages()).filter(p => tabState(p).owner === state.client);
82
83
  if (own.length) out.log(`Tabs you opened are still open: ${own.map(p => p.url()).join(', ')}; tab close <url-part> closes one, or select it and tab close.`);
84
+ const files = ownFilesText(state.client);
85
+ if (files) out.log(files);
83
86
  },
84
87
  };
85
88
 
package/lib/help.js CHANGED
@@ -41,7 +41,7 @@ const TASKS = [
41
41
  ['record requests and console together', 'capture on, then capture off'],
42
42
  ['find what makes the page slow', 'profile on, then profile off; profile reload for a load'],
43
43
  ['fake a backend response', 'route <glob> <status> <json>, route <glob> abort'],
44
- ['change or slow an API response', 'route <glob> patch <json>, route <glob> delay <secs>'],
44
+ ['change or slow an API response', 'route <glob> patch <json>, route <glob> delay <time>'],
45
45
  ['cut or slow the whole network', 'network off, network slow, then network on'],
46
46
  ['record a video of the page', 'record on, then record off (help video for a take)'],
47
47
  ['show a pointer where the agent acts', 'cursor on, then cursor off'],
@@ -62,6 +62,8 @@ ${TASKS.map(([task, commands]) => ` ${task.padEnd(38)} ${commands}`).join('\n'
62
62
  Modes (${MODES}) stay on until turned off; the prompt shows the selected tab's: (watch routes:1) pw>.
63
63
  Each of them on its own, and modes, shows its state and what you can run next.
64
64
 
65
+ Times are in milliseconds, as in Playwright, or in seconds with an s: wait .done 30000, or wait .done 30s.
66
+
65
67
  Topics (help <topic>):
66
68
  tabs open, select, close, navigate network requests, bodies, console, fakes, network off
67
69
  interact click, fill, type, press, select devtools eval, CDP commands, cookie and storage names
@@ -120,7 +122,7 @@ box to point at something, and toast <text> a line of text saying what is happen
120
122
  the viewer needs to read it. After a step that loads a page, wait for text only the result shows (wait
121
123
  text ...), not a heading the page always has, and not wait load: a single-page app loads nothing.
122
124
 
123
- record on --steps also saves when and where each step ran, in the video's seconds and pixels, for
125
+ record on --steps also saves when and where each step ran, in seconds into the video and its pixels, for
124
126
  editing it afterwards.
125
127
 
126
128
  The user's own screen, with their real pointer, takes a screen recorder they run (OBS, ffmpeg -f
@@ -135,7 +137,7 @@ clean browser of its own (here, its video-start is record on).`,
135
137
  devtools: {
136
138
  intro: `Cookie and storage listings omit values. eval, cdp, html, screenshots, recordings and URLs can
137
139
  still show sensitive data (cdp Network.getCookies shows cookie values).`,
138
- commands: ['eval', 'cdp', 'cookies', 'storage'],
140
+ commands: ['eval', 'cdp', 'clipboard', 'cookies', 'storage'],
139
141
  },
140
142
  session: {
141
143
  intro: `Server: pw-repl serve also takes commands on /tmp/pw-repl.sock; they show here as [server] lines.
@@ -372,7 +374,7 @@ all.`,
372
374
  ['--grep <text>', 'only the lines containing text (role, name or a flag such as [disabled], any case), each with the named elements around it.'],
373
375
  ['--regex <pattern>', '--grep by a regular expression: snapshot --regex "Total: \\d+".'],
374
376
  ],
375
- detail: `Refs: a ref (e3, or f1e3 in newer Playwright) works as a selector in any command: click e3. It belongs
377
+ detail: `Refs: a ref (e3; f1e3, f2e3 once the page has loaded again, when the old refs stop working) works as a selector in any command: click e3. It belongs
376
378
  to the tab whose snapshot gave it, and can change when the page changes (e102 may become f4e98): take a
377
379
  new snapshot after it does.
378
380
 
@@ -527,7 +529,7 @@ circle). That changes the page, so put it back afterwards, as it was if it had a
527
529
  eval document.querySelector("#rate-3").style.outline = ""`,
528
530
  },
529
531
  screenshot: {
530
- usage: 'screenshot [<ref>] [--full] [-d <secs>] [name]',
532
+ usage: 'screenshot [<ref>] [--full] [-d <time>] [name]',
531
533
  summary: 'save a PNG of the viewport, --full page or an element',
532
534
  args: [
533
535
  ['[<ref>]', 'only that element, from a snapshot ref: screenshot e5.'],
@@ -535,7 +537,7 @@ circle). That changes the page, so put it back afterwards, as it was if it had a
535
537
  ],
536
538
  options: [
537
539
  ['--full', 'the whole page, not only the viewport; --full-page too.'],
538
- ['-d, --delay <secs>', 'counts down out loud first, up to 60, so someone can hold a hover or open a menu.'],
540
+ ['-d, --delay <time>', 'counts down out loud first, in seconds, up to 60s, so someone can hold a hover or open a menu.'],
539
541
  ['--filename=<file>', 'saves it there instead (a JPEG if it ends in .jpg). Relative to the REPL\'s folder; through pw-repl send, the sender\'s. It never replaces a file.'],
540
542
  ],
541
543
  detail: `Where: $PW_SCREENSHOT_DIR (a folder that exists); without it, next to the REPL's socket when it serves on one of its own, or
@@ -603,19 +605,19 @@ the same spot, and shows in screenshots and recordings. Selectors, snapshot and
603
605
  takes no clicks, and it points at elements inside frames too.`,
604
606
  },
605
607
  record: {
606
- usage: 'record [on [file] [seconds] [options] | off [file]]',
608
+ usage: 'record [on [file] [time] [options] | off [file]]',
607
609
  summary: 'record the selected tab\'s page to a video',
608
610
  args: [
609
611
  ['on | off', 'on starts recording the selected tab; off stops it, saves the file and says its path, length, size and bytes. Neither: says whether it is recording, and how your last recording ended.'],
610
612
  ['[file]', 'clip.webm or clip.mp4; its ending picks the format. Relative to the REPL\'s folder; through pw-repl send, the sender\'s. Default: recording-<timestamp>.webm where screenshot saves. On record off: saves it under that name instead.'],
611
- ['[seconds]', 'stops by itself after them, 1 to 3600. Default: 3600.'],
613
+ ['[time]', 'stops by itself after it, 1s or more. Default: 1 hour (3600s).'],
612
614
  ],
613
615
  optionsFor: 'record on',
614
616
  options: [
615
617
  ['--steps', 'also save clip.steps.txt next to clip.webm: when and where each command ran (Steps, below). Default: off.'],
616
618
  ['--pause=<ms>', 'a pause after each action, and each highlight drawn, so the viewer sees it. Default: 750; 0 none.'],
617
- ['--lead=<ms>', 'still page recorded before record on returns. Default: 1000; counts toward the seconds.'],
618
- ['--tail=<ms>', 'still page recorded after record off, before it stops. Default: 1000; none when it stops at its seconds.'],
619
+ ['--lead=<ms>', 'still page recorded before record on returns. Default: 1000; counts toward its time.'],
620
+ ['--tail=<ms>', 'still page recorded after record off, before it stops. Default: 1000; none when it stops at its time.'],
619
621
  ['--filename=<file>', 'the file, as playwright-cli writes it.'],
620
622
  ],
621
623
  detail: `What it records: the page only, from inside the browser, at the viewport's size in CSS pixels (set
@@ -656,7 +658,7 @@ height loses a pixel), scrollbars included; a phone's under emulate mobile is it
656
658
  is 412 wide, less sharp than the phone draws it.
657
659
 
658
660
  How it ended: record on its own, or record off when nothing is recording, says how your last recording
659
- ended (record off, its seconds, its tab closing or the REPL stopping) with what record off would have
661
+ ended (record off, its time, its tab closing or the REPL stopping) with what record off would have
660
662
  said. tab close says what became of a recording it ended. A REPL stopped with pw-repl stop cannot be
661
663
  asked: stop prints what became of each recording it ended, and of one that ended by itself that record
662
664
  has not shown since.
@@ -702,14 +704,14 @@ mouse, and sends no touch events. Dark, light and timezone apply at once, but wh
702
704
  drew with them (times, colors set by its script) changes only when it reloads.`,
703
705
  },
704
706
  wait: {
705
- usage: 'wait [text|request] <what> [--gone] [secs]',
707
+ usage: 'wait [text|request] <what> [--gone] [time]',
706
708
  summary: 'wait for an element, text, a response or a load (10s)',
707
709
  args: [
708
710
  ['<selector>', 'for a matching element to be visible.'],
709
- ['text <text>', 'for text to be visible: any part of an element\'s text, any case. Quote text that ends in a number, or the number is taken as the seconds: wait text "Showing 1".'],
711
+ ['text <text>', 'for text to be visible: any part of an element\'s text, any case. Quote text that ends in a number, or the number is taken as the time: wait text "Showing 1".'],
710
712
  ['request <part>', 'for a matching response (a URL part or a glob), counting one that finished since the previous command began, or is still under way: click, then wait request, does not miss it.'],
711
713
  ['load', 'for the page to finish loading (its load event), counting a navigation that began since the previous command began: click a link, then wait load.'],
712
- ['[secs]', 'how long, up to 120. Default: 10.'],
714
+ ['[time]', 'how long, with no limit (give send time too: send -t). Default: 10s.'],
713
715
  ],
714
716
  options: [
715
717
  ['--gone', 'until nothing matching is visible (removed or hidden): wait .spinner --gone. Done at once if nothing matches yet, so wait for it to appear first if it may not have.'],
@@ -718,8 +720,8 @@ drew with them (times, colors set by its script) changes only when it reloads.`,
718
720
  something on the new view instead. A wait that times out is an error; the REPL carries on.`,
719
721
  },
720
722
  sleep: {
721
- usage: 'sleep <ms>',
722
- summary: 'wait a fixed time, in milliseconds (maximum 3600000)',
723
+ usage: 'sleep <time>',
724
+ summary: 'wait a fixed time: sleep 1500, or sleep 1.5s',
723
725
  detail: `While recording, each action is already followed by a pause (help record): a sleep adds to it.`,
724
726
  },
725
727
  requests: {
@@ -776,11 +778,11 @@ A page brought back from the back/forward cache (back, forward) reports its earl
776
778
  the time it comes back, though nothing loaded again.`,
777
779
  },
778
780
  profile: {
779
- usage: 'profile on [secs] | off | reload | save <file>',
781
+ usage: 'profile on [time] | off | reload | save <file>',
780
782
  summary: 'what slows the tab: long tasks, layouts, input, frames',
781
783
  args: [
782
- ['on', 'starts a performance trace of the selected tab, until profile off (at most 120 seconds).'],
783
- ['[secs]', 'records that long (1 to 120), then prints. Other commands wait until it ends. Over 20, give send time too: pw-repl send -t 70 profile on 60.'],
784
+ ['on', 'starts a performance trace of the selected tab, until profile off (at most 120s; give it a time for longer).'],
785
+ ['[time]', 'records that long (1s or more), then prints. Other commands wait until it ends. Over 20s, give send time too: pw-repl send -t 70s profile on 60s.'],
784
786
  ['off', 'stops it and prints the summary.'],
785
787
  ['reload', 'reloads the tab and profiles its load, until its network is idle (at most 10s after load) and half a second more. For work that starts later (a timer): profile on, reload, sleep, profile off.'],
786
788
  ['save <file>', 'writes the last profile\'s trace, the tab\'s events only, as Chrome trace JSON for DevTools\' Performance panel (Load profile) or ui.perfetto.dev. It refuses a file that exists.'],
@@ -813,7 +815,7 @@ may time out, but the profile records it all; profile off once the page answers
813
815
 
814
816
  Size: a busy page fills Chrome's trace buffer in under a minute (one that restyles many elements as it
815
817
  draws, sooner), which stops the profile there, and the summary says so. Stopping a long one takes a
816
- while, as Chrome writes out its trace: give send time, pw-repl send -t 60 profile off. If send gives up
818
+ while, as Chrome writes out its trace: give send time, pw-repl send -t 60s profile off. If send gives up
817
819
  first, the profile still ends, and profile shows it.
818
820
 
819
821
  One profile runs at a time, in a browser: Chrome traces one at a time, the whole browser's, so another
@@ -844,12 +846,12 @@ the trace even when its page did nothing. The trace is kept in a temporary folde
844
846
  profile, or until the REPL stops.`,
845
847
  },
846
848
  capture: {
847
- usage: 'capture on [requests|console] [secs] | off',
849
+ usage: 'capture on [requests|console] [time] | off',
848
850
  summary: 'record requests and console together, in time order',
849
851
  args: [
850
852
  ['on', 'records both until capture off, which prints them.'],
851
853
  ['requests | console', 'records only one.'],
852
- ['[secs]', 'records that long (1 to 3600), then prints. Other commands wait until it ends.'],
854
+ ['[time]', 'records that long (1s or more), then prints. Other commands wait until it ends.'],
853
855
  ['(none)', 'says whether one is running, or shows the last one.'],
854
856
  ],
855
857
  detail: `A timed capture suits recording what someone does in the browser; around your own commands, use
@@ -862,7 +864,7 @@ and console it keeps more than the last 200, and lists both together.`,
862
864
  args: [
863
865
  ['<glob> <status> <json>', 'answers with this status (200-599) and JSON (the rest of the line), or no body. It never reaches the network, so it still answers while the network is off.'],
864
866
  ['<glob> patch <json>', 'lets it through, then changes its JSON response: a JSON Merge Patch, where objects merge, null removes a key and anything else replaces; one that is not an object (an array) replaces the whole body.'],
865
- ['<glob> delay <secs>', 'holds it for up to 120 seconds, then lets it through.'],
867
+ ['<glob> delay <time>', 'holds it that long, then lets it through.'],
866
868
  ['<glob> abort', 'fails it as if the connection broke.'],
867
869
  ['off <glob> | --all', 'removes one route, or all.'],
868
870
  ['(none)', 'lists the selected tab\'s routes.'],
@@ -888,11 +890,11 @@ replaces it. A glob may be quoted. Example: route **/api/cart patch {"total": 0}
888
890
  ['on', 'restores it.'],
889
891
  ],
890
892
  detail: `Per tab; lasts until network on or the REPL exits. Routes still answer while it is off or slow. To slow
891
- one API, use route <glob> delay <secs>. Stopping a service is not the same as network off: a dev proxy
893
+ one API, use route <glob> delay <time>. Stopping a service is not the same as network off: a dev proxy
892
894
  in front of it usually holds the request open, so the page spins instead of failing.`,
893
895
  },
894
896
  eval: {
895
- usage: 'eval [--all] <JavaScript>',
897
+ usage: 'eval [--all] [--timeout=<time>] <JavaScript>',
896
898
  summary: 'evaluate JavaScript in the selected tab and print the result',
897
899
  args: [
898
900
  ['<JavaScript>', 'the rest of the line. Promises are awaited, await works at the top level, and a function (() => document.title) is called.'],
@@ -900,10 +902,16 @@ in front of it usually holds the request open, so the page spins instead of fail
900
902
  ],
901
903
  options: [
902
904
  ['--file=<path>', 'the JavaScript is the file\'s text: anything the line takes, over many lines and with // comments. With a ref or a selector after the path, it is a function called with that element (the first match). Over send, a relative path is from your shell\'s current folder.'],
905
+ ['--timeout=<time>', 'how long the page has to answer, before --file. Default: 15s. Past 20s, give send time too, or it gives up first and cancels the eval: pw-repl send -t 70s eval --timeout=60s ...'],
903
906
  ['--all', 'the whole result, not capped.'],
904
907
  ],
905
908
  detail: `eval fetch(...) sends a new request, which can change state on the server; it cannot read one that
906
- already happened (requests lists those).`,
909
+ already happened (requests lists those).
910
+
911
+ Long work (--timeout): every other command waits while an eval runs, the other clients' too. For work of
912
+ minutes, or in a browser others are using, start it without awaiting it, keep its result on window, and
913
+ read it back: eval "window.__done = null; run().then(r => window.__done = r); 'started'", then eval
914
+ window.__done until it is set.`,
907
915
  },
908
916
  cdp: {
909
917
  usage: 'cdp [--all] <method> <JSON object>',
@@ -917,6 +925,20 @@ already happened (requests lists those).`,
917
925
  ],
918
926
  detail: `The session is detached afterwards, so subscriptions and settings do not persist. Browser.close and
919
927
  Target.closeTarget are refused, and so is Page.handleJavaScriptDialog: dialog answers one.`,
928
+ },
929
+ clipboard: {
930
+ usage: 'clipboard [--all] [--shared-clipboard]',
931
+ summary: 'print the text on the clipboard, as a Copy button left it',
932
+ options: [
933
+ ['--shared-clipboard', 'reads it in a browser with a window too, whose clipboard is the computer\'s (below).'],
934
+ ['--all', 'the whole text, not capped.'],
935
+ ],
936
+ detail: `It reads through the selected tab, which must be a secure page (https, or http on localhost), granting
937
+ it clipboard-read only while it reads. Check a Copy button: click it, then clipboard.
938
+
939
+ A headless browser, as --launch starts, has a clipboard of its own, holding only what its pages copied.
940
+ One with a window reads the computer's: whatever its user last copied, a password too. There it refuses
941
+ without --shared-clipboard; ask the user before giving it.`,
920
942
  },
921
943
  cookies: {
922
944
  usage: 'cookies [--all]',
@@ -973,7 +995,11 @@ at the browser (e.g. headless), it is the only way to answer one.`,
973
995
  quit: {
974
996
  usage: 'quit',
975
997
  summary: 'disconnect, leaving Chromium running (one --launch started stops too)',
976
- detail: `Runs immediately, even while another command is waiting. Only available at the prompt.`,
998
+ detail: `Runs immediately, even while another command is waiting. Only available at the prompt.
999
+
1000
+ As it stops (quit, or pw-repl stop), it says, for each client, how many of the files it wrote are still
1001
+ there, how large, and in which folders: Files left by cart-bug: 3 files, 48 MB (2 screenshots, 1 video)
1002
+ in /tmp/. pw-repl send -c <name> close lists a client's own in full.`,
977
1003
  },
978
1004
  };
979
1005