pw-repl 0.12.0 → 0.12.2

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
@@ -102,7 +102,10 @@ Commits follow [Conventional Commits](https://www.conventionalcommits.org/): `fe
102
102
  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.
103
103
 
104
104
  1. `npm test` passes and the working tree is clean.
105
- 2. `npm outdated` and `npm audit` show nothing that needs doing first.
105
+ 2. `npm outdated` and `npm audit` show nothing that needs doing first. The lockfile holds playwright-core
106
+ at 1.63: on 1.64, a click on a link to a 204 waits out its timeout instead of returning (the test
107
+ "waits for the page to load, counting a navigation that just began" fails). Update it once a version
108
+ passes `npm test`.
106
109
  3. `npm version <patch|minor|major> -m "chore: release %s"` sets the version in `package.json` and
107
110
  `package-lock.json`, commits it as `chore: release X.Y.Z`, and tags that commit `vX.Y.Z`.
108
111
  4. `git push --follow-tags` pushes the commits and the tag.
package/README.md CHANGED
@@ -8,6 +8,11 @@ in one it starts itself. You type commands at a `pw>` prompt. Agents send the sa
8
8
  with `pw-repl send`. Every command shows in the REPL, whoever sent it, so you always see what an agent
9
9
  is doing in your browser, and the agent can see what you did.
10
10
 
11
+ [![pw-repl demo, recorded by pw-repl](docs/pw-repl-demo.png)](docs/pw-repl-demo.webm)
12
+
13
+ A 90-second demo (click to play), recorded by pw-repl itself from one script of commands. The command
14
+ panel on the right is part of the demo page, showing what the agent sends.
15
+
11
16
  ```text
12
17
  pw> tab new http://localhost:3000/cart
13
18
  pw> snapshot # the page by role and name, with [ref=eN] labels
@@ -125,29 +130,30 @@ example the Docker bridge gateway, `ip route | awk '/default/ {print $3}'`).
125
130
 
126
131
  ## Common tasks
127
132
 
128
- | I want to… | Commands |
129
- | ------------------------------------ | ------------------------------------------------------------------------------------ |
130
- | see where I am | `tab` (it marks the tab you are looking at `(visible)`), `info` |
131
- | see what is on the page | `snapshot`, `screenshot` |
132
- | do something on it | `click`, `fill`, `type`, `press` |
133
- | run some JavaScript in the page | `eval document.title` |
134
- | wait for a page or element | `wait load`, `wait <selector>`, `wait <selector> --gone`, `wait text <text>` |
135
- | choose a file in a file input | `upload <selector> <file>` |
136
- | see it as a phone, or in dark mode | `emulate mobile`, `emulate dark` (also `emulate locale`, `emulate timezone`) |
137
- | see what the page requested | `requests`, then `body <#>` for what one got back |
138
- | see console messages and errors | `console` |
139
- | show an agent what I do | `watch on`, click around in the browser, then `watch` |
140
- | see each step as I click | `watch on --live` |
141
- | replay what I clicked through | `watch on`, click around, `watch save flow.txt`, then `pw-repl send --file flow.txt` |
142
- | record requests and console together | `capture on`, then `capture off` |
143
- | fake a backend response | `route <glob> <status> <json>`, `route <glob> abort` |
144
- | change or slow an API response | `route <glob> patch <json>`, `route <glob> delay <secs>` |
145
- | cut or slow the whole network | `network off`, `network slow`, then `network on` |
146
- | record a video of the page | `record on`, then `record off` (`help video` for a take with a pointer) |
147
- | show a pointer where the agent acts | `cursor on`, then `cursor off` (in screenshots and videos) |
148
- | say what is happening on screen | `toast <text>`, then `toast off` (`--duration=3s` hides it by itself) |
149
- | point something out on the page | `highlight <ref>` draws a box; `highlight off` hides it |
150
- | clean up | `modes off` |
133
+ | I want to… | Commands |
134
+ | ------------------------------------- | ------------------------------------------------------------------------------------ |
135
+ | see where I am | `tab` (it marks the tab you are looking at `(visible)`), `info` |
136
+ | see what is on the page | `snapshot`, `screenshot` |
137
+ | do something on it | `click`, `fill`, `type`, `press` |
138
+ | click what has no selector (a canvas) | `mouseclick <x> <y>` |
139
+ | run some JavaScript in the page | `eval document.title` |
140
+ | wait for a page or element | `wait load`, `wait <selector>`, `wait <selector> --gone`, `wait text <text>` |
141
+ | choose a file in a file input | `upload <selector> <file>` |
142
+ | see it as a phone, or in dark mode | `emulate mobile`, `emulate dark` (also `emulate locale`, `emulate timezone`) |
143
+ | see what the page requested | `requests`, then `body <#>` for what one got back |
144
+ | see console messages and errors | `console` |
145
+ | show an agent what I do | `watch on`, click around in the browser, then `watch` |
146
+ | see each step as I click | `watch on --live` |
147
+ | replay what I clicked through | `watch on`, click around, `watch save flow.txt`, then `pw-repl send --file flow.txt` |
148
+ | record requests and console together | `capture on`, then `capture off` |
149
+ | fake a backend response | `route <glob> <status> <json>`, `route <glob> abort` |
150
+ | change or slow an API response | `route <glob> patch <json>`, `route <glob> delay <secs>` |
151
+ | cut or slow the whole network | `network off`, `network slow`, then `network on` |
152
+ | record a video of the page | `record on`, then `record off` (`help video` for a take with a pointer) |
153
+ | show a pointer where the agent acts | `cursor on`, then `cursor off` (in screenshots and videos) |
154
+ | say what is happening on screen | `toast <text>`, then `toast off` (`--duration=3s` hides it by itself) |
155
+ | point something out on the page | `highlight <ref>` draws a box; `highlight off` hides it |
156
+ | clean up | `modes off` |
151
157
 
152
158
  `help <topic>` lists the rest, and `help <command>` has usage and caveats.
153
159
 
package/bin/pw-repl.js CHANGED
@@ -15,7 +15,8 @@ const USAGE = `Usage:
15
15
  see everything a background REPL does, and type commands to it; Ctrl-C leaves it running
16
16
 
17
17
  pw-repl stop [-e endpoint]
18
- stop a background REPL, and say what became of each recording it ended
18
+ stop a background REPL, and say what became of each recording it ended; its socket is removed, its
19
+ log stays
19
20
 
20
21
  pw-repl send [-e endpoint | -s session] [-c client] [-t seconds] [--no-vars] <command...> | --file <file>
21
22
  run one command in a running REPL and print its output; --file runs a file of them, one per line
package/lib/elements.js CHANGED
@@ -465,6 +465,27 @@ const commands = {
465
465
  out.log(`Moved the mouse to ${x}, ${y}`);
466
466
  },
467
467
 
468
+ // A click at a point, as Playwright's mouse.click: for what has no selector, such as a marker drawn on a canvas.
469
+ async mouseclick(line) {
470
+ const usage = 'mouseclick <x> <y> [left|right|middle] [--delay=<ms>]';
471
+ const held = /(?:^|\s)--delay(?:=|\s+)(\S*)(?=\s|$)/.exec(line || '');
472
+ if (held && !/^\d+$/.test(held[1])) throw new Error(`Usage: ${usage}, the ms between press and release, e.g. mouseclick 400 300 --delay=500`);
473
+ const words = (held ? line.replace(held[0], ' ') : line || '').trim().split(/\s+/).filter(Boolean);
474
+ const button = /^[a-z]+$/.test(words[words.length - 1] || '') ? mouseButton(words.pop(), 'mouseclick <x> <y>') : 'left';
475
+ const [x, y, extra] = numbers(words.join(' '), usage);
476
+ if (y === undefined || extra !== undefined) throw new Error(`Usage: ${usage}, in CSS pixels from the top left of the viewport`);
477
+ stepTarget(state.page, null, { x, y });
478
+ await glideTo(state.page, x, y);
479
+ await state.page.mouse.move(x, y);
480
+ tabState(state.page).mouseAt = { x, y };
481
+ // The ring shows at the press, as mousedown's does, while a --delay holds the button.
482
+ await state.page.mouse.down({ button });
483
+ await showClick(state.page);
484
+ if (held) await new Promise(resolve => setTimeout(resolve, Number(held[1])));
485
+ await state.page.mouse.up({ button });
486
+ out.log(`Clicked the ${button} button at ${x}, ${y}`);
487
+ },
488
+
468
489
  async mousedown(args) {
469
490
  const button = mouseButton(args, 'mousedown');
470
491
  await glideToMouse();
package/lib/help.js CHANGED
@@ -70,7 +70,7 @@ tab on its own lists them all.`,
70
70
  intro: `Selectors are Playwright selectors (CSS, text=..., role=...) or snapshot refs such as e5.
71
71
  Commands use the first match (click, dblclick and hover the first they can use: help click); see help
72
72
  fill for selectors with spaces. mousemove and the other mouse commands act at a point instead.`,
73
- commands: ['click', 'dblclick', 'hover', 'fill', 'type', 'press', 'select', 'check', 'uncheck', 'upload', 'mousemove', 'mousedown', 'mouseup', 'mousewheel'],
73
+ commands: ['click', 'dblclick', 'hover', 'fill', 'type', 'press', 'select', 'check', 'uncheck', 'upload', 'mousemove', 'mouseclick', 'mousedown', 'mouseup', 'mousewheel'],
74
74
  },
75
75
  inspect: {
76
76
  intro: `Output is capped; --all right after the command, or at the end, shows everything (text --all body).`,
@@ -219,8 +219,21 @@ events) and that the outcome is unknown; Playwright's full call log goes to the
219
219
  eval (r => [r.x + r.width / 2, r.y + r.height / 2])(document.querySelector("#map")
220
220
  .getBoundingClientRect())
221
221
 
222
- A click at a point: mousemove <x> <y>, mousedown, mouseup. A drag: mousedown, then mousemove to another
223
- point, then mouseup.`,
222
+ A click at a point: mouseclick <x> <y>. A drag: mousedown, then mousemove to another point, then mouseup.`,
223
+ },
224
+ mouseclick: {
225
+ usage: 'mouseclick <x> <y> [button]',
226
+ summary: 'click at a point in the viewport',
227
+ args: [
228
+ ['<x> <y>', 'CSS pixels from the top left of the tab\'s viewport, as for mousemove.'],
229
+ ['[button]', 'left, right or middle. Default: left.'],
230
+ ],
231
+ options: [
232
+ ['--delay=<ms>', 'holds the button down this long between press and release, as Playwright\'s mouse.click delay. Default: 0.'],
233
+ ],
234
+ detail: `For what has no selector, such as a marker drawn on a canvas; anything a selector finds is better
235
+ clicked with click, which waits for it. help mousemove shows how to find a point. The mouse stays at the point, for a mousewheel or mousedown after
236
+ it. The cursor (help cursor) moves there first and shows a ring at the press.`,
224
237
  },
225
238
  mousedown: { usage: 'mousedown [button]', summary: 'press a mouse button where the mouse is (left, right or middle)' },
226
239
  mouseup: { usage: 'mouseup [button]', summary: 'release a mouse button where the mouse is' },
@@ -460,7 +473,7 @@ circle). That changes the page, so put it back afterwards, as it was if it had a
460
473
  ['-d, --delay <secs>', 'counts down out loud first, up to 60, so someone can hold a hover or open a menu.'],
461
474
  ['--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.'],
462
475
  ],
463
- detail: `Where: $PW_SCREENSHOT_DIR; without it, next to the REPL's socket when it serves on one of its own, or
476
+ 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
464
477
  else in /tmp.
465
478
 
466
479
  It brings the tab to the front of its window first: Chrome draws only the tab in front. The image is
@@ -511,8 +524,8 @@ shows in screenshots and recordings. Selectors, snapshot and text don't see it,
511
524
  detail: `The REPL's clicks and typing don't move a pointer on screen, so a recording shows things happening
512
525
  with nothing pointing at them. With the cursor on, these commands move it smoothly to their element or
513
526
  point before they act: click, dblclick, hover, fill, type <selector>, press <selector>, select, check,
514
- uncheck, upload and mousemove. Each move takes 150 to 600ms by distance (300 to 1000ms while the tab
515
- records), and the command waits for it. Clicks (click, dblclick, check, uncheck, mousedown, and upload
527
+ uncheck, upload, mousemove and mouseclick. Each move takes 150 to 600ms by distance (300 to 1000ms while the tab
528
+ records), and the command waits for it. Clicks (click, dblclick, check, uncheck, mouseclick, mousedown, and upload
516
529
  when it clicks a button) show a ring where they land.
517
530
 
518
531
  Only the drawing moves: no mouse events are sent along the way, so the page gets the same input as with
@@ -905,6 +918,11 @@ Different on purpose, since the browser may be someone else's too:
905
918
  return Object.hasOwn(COMMANDS, topicOrCommand) ? `${text}\n\n${renderCommand(topicOrCommand)}` : text;
906
919
  }
907
920
  if (Object.hasOwn(COMMANDS, topicOrCommand)) return renderCommand(topicOrCommand);
921
+ // Not a topic: the mouse commands are in interact, with the others that act on the page.
922
+ if (topicOrCommand === 'mouse') {
923
+ const mice = TOPICS.interact.commands.filter(c => c.startsWith('mouse'));
924
+ return ['The mouse commands, which act at a point (part of help interact):', '', ...commandLines(mice)].join('\n');
925
+ }
908
926
  const full = /^(\S+)\s+--all$/.exec(topicOrCommand);
909
927
  if (full && Object.hasOwn(COMMANDS, full[1])) return renderCommand(full[1], true);
910
928
  // A playwright-cli name: what it runs here, with that command's help.
package/lib/record.js CHANGED
@@ -392,7 +392,7 @@ function videoTime(rec) {
392
392
  // What a step is, for its line: the command and what it acts on, never what it types or chooses, the
393
393
  // code it runs or the URL it goes to, which can hold a password or a token. A steps file goes where the
394
394
  // video goes, and the video shows a password as dots.
395
- const WHOLE = new Set(['click', 'dblclick', 'hover', 'check', 'uncheck', 'mousemove', 'mousedown', 'mouseup', 'mousewheel']);
395
+ const WHOLE = new Set(['click', 'dblclick', 'hover', 'check', 'uncheck', 'mousemove', 'mouseclick', 'mousedown', 'mouseup', 'mousewheel']);
396
396
  const TARGET_FIRST = new Set(['fill', 'type', 'select', 'press', 'upload']);
397
397
  function stepLabel(cmd, args) {
398
398
  if (WHOLE.has(cmd)) {
package/lib/start.js CHANGED
@@ -27,6 +27,14 @@ curl ${CDP_URL}/json/version checks that it answers; PW_CDP_URL points at a brow
27
27
  Or let pw-repl start a private one for this REPL: pw-repl run --launch (or serve --launch).`;
28
28
  }
29
29
 
30
+ // The command this REPL was started with, to start it again: a background one runs as serve, with
31
+ // --background left out of its arguments.
32
+ function startedAs() {
33
+ const args = process.argv.slice(2).map(a => (/[\s"'$]/.test(a) ? JSON.stringify(a) : a));
34
+ if (process.env.PW_REPL_PID_FILE && args[0] === 'serve') args.splice(1, 0, '--background');
35
+ return ['pw-repl', ...args].join(' ');
36
+ }
37
+
30
38
  async function start(options) {
31
39
  const START_URL = options.startUrl || process.env.PW_START_URL || null;
32
40
  // First, so a terminal or a log says which pw-repl ran; where names the skill to compare a saved one with.
@@ -60,7 +68,7 @@ async function start(options) {
60
68
  state.connectionLost = true;
61
69
  state.stopping = true;
62
70
  process.exitCode = 1;
63
- out.error('Chromium connection lost; queued commands will not run.');
71
+ out.error(`Chromium connection lost; queued commands will not run. Start the REPL again to carry on: ${startedAs()}`);
64
72
  void shutdown().then(beforeExit).then(() => process.exit(1));
65
73
  });
66
74
  out.log('Connected to Chromium via CDP');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pw-repl",
3
- "version": "0.12.0",
3
+ "version": "0.12.2",
4
4
  "description": "Text REPL for driving an existing Chromium through Playwright over CDP",
5
5
  "keywords": [
6
6
  "playwright",
package/skill/SKILL.md CHANGED
@@ -187,10 +187,9 @@ blindly retry a change), `64` usage or the REPL is not reachable. `pw-repl --hel
187
187
  ## Learn the commands
188
188
 
189
189
  Run `pw-repl send help`. It lists seven topics; `help <topic>` lists their commands, `help <command>`
190
- gives usage and caveats, and `help --all` prints everything at once. pw-repl answers it itself: it needs
191
- no running REPL, and never reaches one. The
192
- help is the command reference; this file does not repeat it. If you know playwright-cli, its command
193
- names work too (`help playwright-cli` lists them).
190
+ gives usage and caveats: read it before you use a command, since this file does not repeat them. `help
191
+ --all` prints everything at once. pw-repl answers help itself: it needs no running REPL, and never
192
+ reaches one. If you know playwright-cli, its command names work too (`help playwright-cli` lists them).
194
193
 
195
194
  ### Coming from playwright-cli
196
195
 
@@ -252,10 +251,10 @@ pw-repl send -c <name> close # your modes off; tab close
252
251
  - Playwright's own Chromium and ffmpeg are found where Playwright installs them:
253
252
  `PLAYWRIGHT_BROWSERS_PATH`, or `~/.cache/ms-playwright`.
254
253
  - `PW_CDP_URL` — CDP endpoint (default `http://localhost:9222`).
255
- - `PW_SCREENSHOT_DIR` — where the REPL saves screenshots, read when it starts, not by `send`. Without
256
- it, a REPL on a socket of its own saves them next to its socket, and any other in `/tmp`. `screenshot`
257
- prints each file's path; other REPLs may save theirs in the same place. Remove only your own, and keep
258
- those too if they are needed, e.g. as evidence or as something to hand over.
254
+ - `PW_SCREENSHOT_DIR` — where the REPL saves screenshots (a folder that exists), read when it starts,
255
+ not by `send`. Without it, a REPL on a socket of its own saves them next to its socket, and any other
256
+ in `/tmp`. `screenshot` prints each file's path; other REPLs may save theirs in the same place. Remove
257
+ only your own, and keep those too if they are needed, e.g. as evidence or as something to hand over.
259
258
  - `PW_ENDPOINT` / `PW_TMUX_SESSION` — defaults for `send -e` / `-s`. `PW_SOCKET` — the socket `serve`
260
259
  serves on and `send` looks for when neither is given (default `/tmp/pw-repl.sock`).
261
260