pw-repl 0.8.2 → 0.9.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/lib/help.js CHANGED
@@ -38,7 +38,7 @@ Common tasks:
38
38
  see each step as I click watch on --live
39
39
  watch a tab someone will open watch on --next-tab [url-part]
40
40
  requests and console together capture on, then capture off
41
- a video of the page record on, then record off
41
+ a video of what I do help video
42
42
  break the backend on purpose route <glob> <status> <json>, route <glob> abort, network off
43
43
  change or slow an API response route <glob> patch <json>, route <glob> delay <secs>
44
44
  slow the whole network network slow
@@ -52,13 +52,13 @@ Topics (help <topic>):
52
52
  tabs open, select, close, navigate network requests, bodies, console, fakes, network off
53
53
  interact click, fill, type, press, select devtools eval, CDP commands, cookie and storage names
54
54
  inspect snapshot, watch, text, screenshots session modes, markers, server, quitting
55
+ video record a take, with a pointer
55
56
 
56
- help <command> for usage and caveats (e.g. help route); help --all for everything.
57
+ help <command> for usage and caveats (e.g. help route), help <command> --all for all of it; help --all
58
+ for everything.
57
59
 
58
60
  Know playwright-cli? Its command names work here too (tab-list, go-back, set-color-scheme, route
59
- --status=...); help playwright-cli lists them.
60
-
61
- Recording video? help video says which way suits what you want.`);
61
+ --status=...); help playwright-cli lists them.`);
62
62
 
63
63
  const TOPICS = {
64
64
  tabs: {
@@ -74,7 +74,42 @@ fill for selectors with spaces. mousemove and the other mouse commands act at a
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).`,
77
- commands: ['snapshot', 'watch', 'text', 'html', 'attrs', 'listeners', 'count', 'visible', 'links', 'inputs', 'screenshot', 'highlight', 'cursor', 'record', 'viewport', 'emulate', 'wait', 'sleep'],
77
+ commands: ['snapshot', 'watch', 'text', 'html', 'attrs', 'listeners', 'count', 'visible', 'links', 'inputs', 'screenshot', 'highlight', 'viewport', 'emulate', 'wait', 'sleep'],
78
+ },
79
+ video: {
80
+ intro: `A video of what you do in a tab, for someone to watch. Record it from a file of commands sent with
81
+ pw-repl send --file, not command by command: the time between commands sent one by one is in the video.
82
+
83
+ 1. Set its size first: viewport 1280x720 (help viewport). Then try the flow without recording, and note
84
+ the selectors it needs (#id, text=..., role=...), not snapshot refs: the take starts again from a fresh
85
+ page, where the refs are stale.
86
+
87
+ 2. Write the take, one command per line, then run it: pw-repl send --file take.txt (pw-repl help, outside
88
+ the REPL, has the rest). Its first line puts the page back as it was before the rehearsal (goto <url>),
89
+ so the take does not start from the rehearsal's end.
90
+
91
+ goto https://shop.example/support/new
92
+ record on clip.webm
93
+ cursor on #subject
94
+ type #subject Package arrived damaged
95
+ select #area Shipping
96
+ click #send
97
+ wait text Ticket filed
98
+ record off
99
+
100
+ 3. While it records, pw-repl paces it for a viewer (help record): type types at a readable pace (use it,
101
+ not fill, for text the viewer should see typed), each action is followed by a pause, an element out of
102
+ view is scrolled to smoothly, and the video starts and ends on a still page. highlight <selector> draws a
103
+ box to point at something. After a step that loads a page, wait for text only the result shows (wait
104
+ text ...), not a heading the page always has, and not wait load: a single-page app loads nothing.
105
+
106
+ record on --steps also saves when and where each step ran, in the video's seconds and pixels, for
107
+ editing it afterwards.
108
+
109
+ The user's own screen, with their real pointer, takes a screen recorder they run (OBS, ffmpeg -f
110
+ x11grab, macOS screencapture -v): ask first. playwright-cli itself is a separate tool that records a
111
+ clean browser of its own (here, its video-start is record on).`,
112
+ commands: ['record', 'cursor'],
78
113
  },
79
114
  network: {
80
115
  intro: `requests and console record all the time; a capture records only while it runs.`,
@@ -114,27 +149,25 @@ const COMMANDS = {
114
149
  tab: {
115
150
  usage: 'tab [<index>|<url-part>|new [url]|close [url-part]]',
116
151
  summary: 'list tabs (* is selected); select, open or close one',
117
- detail: `tab <index> uses the numbers from your latest tab listing (each client has its own); they change
118
- when tabs open or close.
119
-
120
- tab <url-part> selects the one tab whose URL contains it, and refuses if none or several do.
121
-
122
- tab new opens a tab behind the one in front, so it does not take the window from whoever is using it,
123
- and selects it; tab shows who opened each tab opened this way. tab close closes the selected tab, or
124
- the one matching url-part.
125
-
126
- Closing the selected tab goes back to the previous tab if tab new opened it; otherwise no tab is
152
+ args: [
153
+ ['(none)', 'lists the tabs: * marks the selected one, and each says who opened it with tab new.'],
154
+ ['<index>', 'selects by the number in your latest listing (each client has its own); numbers change when tabs open or close.'],
155
+ ['<url-part>', 'selects the one tab whose URL contains it; refused if none or several do.'],
156
+ ['new [url]', 'opens a tab behind the one in front, so it does not take the window from whoever is using it, and selects it. The URL is read as goto reads it.'],
157
+ ['close [url-part]', 'closes the selected tab, or the one matching url-part.'],
158
+ ],
159
+ detail: `Closing the selected tab goes back to the previous tab if tab new opened it; otherwise no tab is
127
160
  selected, and commands that need one refuse until one is.`,
128
161
  },
129
162
  goto: {
130
163
  usage: 'goto <url>',
131
164
  summary: 'navigate the selected tab',
132
- detail: `Without a scheme, http:// is assumed for localhost, 127.x and [::1], and https:// otherwise, as in
133
- the address bar. tab new <url> reads its URL the same way.
134
-
135
- A page that does not load says why, as wait load does: a failed request, after which the tab shows
136
- Chrome's error page (info names the URL that failed), or an answer with no page (a 204), which
137
- leaves the tab where it was. tab new, reload, back and forward say it the same way.`,
165
+ args: [
166
+ ['<url>', 'without a scheme, http:// for localhost, 127.x and [::1], and https:// otherwise, as in the address bar.'],
167
+ ],
168
+ detail: `A page that does not load says why, as wait load does: a failed request, after which the tab shows
169
+ Chrome's error page (info names the URL that failed), or an answer with no page (a 204), which leaves
170
+ the tab where it was. tab new, reload, back and forward say it the same way.`,
138
171
  },
139
172
  back: {
140
173
  usage: 'back',
@@ -149,139 +182,163 @@ afresh.`,
149
182
  click: {
150
183
  usage: 'click <selector> [button]',
151
184
  summary: 'click the first match that can be clicked',
152
- detail: `After a ref or a quoted selector, a button (left, right, middle) may follow: click e5 right.
153
- --modifiers=<key>[,<key>] holds keys down meanwhile (Alt, Control, ControlOrMeta, Meta, Shift).
154
-
155
- When a selector matches several elements, hidden ones and ones covered by something else (a dialog's
156
- overlay) are passed over, and it says which match it used: Clicked: text=Buy (match 3 of 4; ...). If
157
- none can be clicked within 5s, it says why for each and does nothing. A ref, or a single match, is
158
- clicked as it is, waiting up to 5s for it to be ready.
159
-
160
- Timed out after it began acting, it says what stopped it (e.g. <div class="overlay"> intercepts
161
- pointer events) and that the outcome is unknown; Playwright's full call log goes to the REPL's pane
162
- or log.`,
185
+ args: [
186
+ ['<selector>', 'a Playwright selector or a snapshot ref (e5); quote one with spaces.'],
187
+ ['[button]', 'left, right or middle, after a ref or a quoted selector: click e5 right. Default: left.'],
188
+ ],
189
+ options: [
190
+ ['--modifiers=<keys>', 'keys held down meanwhile, comma-separated: Alt, Control, ControlOrMeta, Meta, Shift.'],
191
+ ],
192
+ detail: `Several matches: hidden ones and ones covered by something else (a dialog's overlay) are passed over,
193
+ and it says which it used: Clicked: text=Buy (match 3 of 4; ...). If none can be clicked within 5s, it
194
+ says why for each and does nothing. A ref, or a single match, is clicked as it is, waiting up to 5s for
195
+ it to be ready.
196
+
197
+ Timed out after it began acting, it says what stopped it (e.g. <div class="overlay"> intercepts pointer
198
+ events) and that the outcome is unknown; Playwright's full call log goes to the REPL's pane or log.`,
163
199
  },
164
200
  dblclick: { usage: 'dblclick <selector> [button]', summary: 'double-click, choosing the match as click does' },
165
201
  hover: { usage: 'hover <selector>', summary: 'hover, choosing the match as click does' },
166
202
  mousemove: {
167
203
  usage: 'mousemove <x> <y>',
168
204
  summary: 'move the mouse to a point in the viewport',
169
- detail: `x and y are CSS pixels from the top left of the tab's viewport (not the page), as mouse events
170
- have them. A snapshot has no coordinates; eval of getBoundingClientRect gives an element's centre:
205
+ args: [
206
+ ['<x> <y>', 'CSS pixels from the top left of the tab\'s viewport (not the page), as mouse events have them.'],
207
+ ],
208
+ detail: `A snapshot has no coordinates; eval of getBoundingClientRect gives an element's centre:
171
209
  eval (r => [r.x + r.width / 2, r.y + r.height / 2])(document.querySelector("#map")
172
210
  .getBoundingClientRect())
173
211
 
174
- A click at a point: mousemove <x> <y>, mousedown, mouseup. A drag: mousedown, then mousemove to
175
- another point, then mouseup.`,
212
+ A click at a point: mousemove <x> <y>, mousedown, mouseup. A drag: mousedown, then mousemove to another
213
+ point, then mouseup.`,
176
214
  },
177
215
  mousedown: { usage: 'mousedown [button]', summary: 'press a mouse button where the mouse is (left, right or middle)' },
178
216
  mouseup: { usage: 'mouseup [button]', summary: 'release a mouse button where the mouse is' },
179
217
  mousewheel: {
180
218
  usage: 'mousewheel <dx> <dy>',
181
219
  summary: 'turn the wheel where the mouse is',
182
- detail: `Wheel events at the mouse's point (mousemove puts it there), in CSS pixels: positive dy scrolls
183
- down. A map zooms around that point: mousemove 400 300, then mousewheel 0 -300 zooms in. It returns
184
- once the events are sent, not once the page has scrolled or redrawn.`,
220
+ args: [
221
+ ['<dx> <dy>', 'CSS pixels; positive dy scrolls down.'],
222
+ ],
223
+ detail: `The events go to the mouse's point (mousemove puts it there): a map zooms around it, so mousemove 400
224
+ 300, then mousewheel 0 -300 zooms in. It returns once the events are sent, not once the page has
225
+ scrolled or redrawn. While recording, it scrolls in eased steps over up to 1s (help record).`,
185
226
  },
186
227
  fill: {
187
228
  usage: 'fill <selector> <value> [--submit]',
188
229
  summary: 'clear an input and fill it',
189
- detail: `The selector is the first word; quote it if it has spaces. The rest of the line is the value, and
190
- quotes around all of it are removed, so fill #name "" clears the field. --submit at the end presses
191
- Enter in it afterwards.
192
-
193
- Examples: fill #name Ada Lovelace, fill "text=Your name" Ada, fill e7 Ada`,
230
+ args: [
231
+ ['<selector>', 'the first word; quote it if it has spaces: fill "text=Your name" Ada.'],
232
+ ['<value>', 'the rest of the line; quotes around all of it are removed, so fill #name "" clears it.'],
233
+ ],
234
+ options: [
235
+ ['--submit', 'at the end: presses Enter in it afterwards.'],
236
+ ],
237
+ detail: `Examples: fill #name Ada Lovelace, fill "text=Your name" Ada, fill e7 Ada. It sets the whole value at
238
+ once, even while recording: type shows it being typed.`,
194
239
  },
195
240
  type: {
196
241
  usage: 'type [<selector>] <text> [--submit]',
197
242
  summary: 'type key by key after what the field holds; text as for fill',
198
- detail: `With one word, or one quoted, it is typed into the element that has focus: type "garden hose".
199
- --submit at the end presses Enter afterwards.
200
-
201
- Keys are sent all at once. While the tab is recording (help record), they are 60ms apart, so the video
202
- shows the text being typed. type --delay=<ms> <selector> <text> sets the gap between keys yourself. The
203
- whole text takes its length times the gap, and send waits 20s by default: about 300 characters at 60ms
204
- need a longer send -t, or a shorter --delay. It is a gap between keys, not a wait before starting like
205
- screenshot's --delay. A page that reacts to each key, such as search as you type, may react to each one.`,
243
+ args: [
244
+ ['[<selector>]', 'as for fill. Without it (one word, or one quoted), into what has focus: type "garden hose".'],
245
+ ['<text>', 'the rest of the line, as fill\'s value.'],
246
+ ],
247
+ options: [
248
+ ['--delay=<ms>', 'right after type: the gap between keys. Default: none, or 60 while the tab records.'],
249
+ ['--submit', 'at the end: presses Enter afterwards.'],
250
+ ],
251
+ detail: `While the tab is recording (help record), keys go 60ms apart, so the video shows the text being typed.
252
+ The whole text takes its length times the gap, and send waits 20s by default: about 300 characters at
253
+ 60ms need a longer send -t, or a shorter --delay. --delay is a gap between keys, not a wait before
254
+ starting like screenshot's. A page that reacts to each key, such as search as you type, may react to
255
+ each one.`,
206
256
  },
207
257
  press: { usage: 'press <key> | press <selector> <key>', summary: 'press a key (Enter, Escape, Control+A; any case), maybe on an element' },
208
258
  select: {
209
259
  usage: 'select <selector> <value>',
210
260
  summary: 'choose an option in a select (the value is the rest of the line)',
211
- detail: `The value is an option's value or its label. With the cursor on (help cursor), select also draws the
212
- select's list opening and the cursor choosing the option, since a native select's list opens outside
213
- the page, where no screenshot or recording sees it; the page gets the same input either way.`,
261
+ args: [
262
+ ['<value>', 'an option\'s value or its label.'],
263
+ ],
264
+ detail: `With the cursor on (help cursor), it also draws the select's list opening and the cursor choosing the
265
+ option, since a native select's list opens outside the page, where no screenshot or recording sees it;
266
+ the page gets the same input either way.`,
214
267
  },
215
268
  check: { usage: 'check <selector>', summary: 'check a checkbox' },
216
269
  uncheck: { usage: 'uncheck <selector>', summary: 'uncheck a checkbox' },
217
270
  upload: {
218
271
  usage: 'upload <selector> <file>...',
219
272
  summary: 'choose files in a file input, as the file picker would',
220
- detail: `The selector is a file input (even a hidden one), its label, or a button that opens the file
221
- picker. The page then does what it does with a chosen file; often that is the upload itself.
222
-
223
- The REPL reads the files and hands the page their contents, so they need not be where the browser
224
- runs; up to 50MB in all. Relative paths are from the folder pw-repl send runs in, or the REPL's own
225
- for a command typed at its prompt. Quote a path with spaces.`,
273
+ args: [
274
+ ['<selector>', 'a file input (even a hidden one), its label, or a button that opens the file picker.'],
275
+ ['<file>...', 'one or more; quote one with spaces. Relative to the REPL\'s folder; through pw-repl send, the sender\'s.'],
276
+ ],
277
+ detail: `The page then does what it does with a chosen file; often that is the upload itself. The REPL reads
278
+ the files and hands the page their contents, so they need not be where the browser runs; up to 50MB in
279
+ all.`,
226
280
  },
227
281
  snapshot: {
228
282
  usage: 'snapshot [--full] [--grep <text> | <eN> | selector]',
229
283
  summary: 'outline by role and name, with [ref=eN] labels',
230
- detail: `Playwright's accessibility snapshot, with unnamed layout wrappers (generic) and cursor hints left
231
- out; --full shows it unchanged.
232
-
233
- --grep <text> prints only the lines containing text (role, name or flag such as [disabled], any
234
- case), each with the named elements around it. A hit with no named element around it prints without
235
- a path. A hit on text (a label) keeps the ref of the element holding it, so snapshot <that ref>
236
- shows what sits beside it, such as the value next to the label. --regex <pattern> is --grep by a
237
- regular expression: snapshot --regex "Total: \\d+". Refs belong to the tab whose snapshot gave them.
238
-
239
- An element with no role or name of its own (a card that is only a div) is left out, with its ref;
240
- snapshot --full keeps it.
241
-
242
- snapshot e3 outlines one element, and the refs of the last snapshot keep working. A ref (e3, or f1e3
243
- in newer Playwright) works as a selector in any command: click e3. Refs can change when the page
244
- changes (e102 may become f4e98), so take a new snapshot after it does. snapshot <selector> (not a
245
- ref) gives new refs, and those from before stop working.
284
+ args: [
285
+ ['(none)', 'the whole page: Playwright\'s accessibility snapshot, with unnamed layout wrappers (generic), cursor hints and elements with no role or name of their own (a card that is only a div) left out.'],
286
+ ['<eN>', 'outlines one element; the refs of the last snapshot keep working.'],
287
+ ['<selector>', 'outlines the first match, with new refs: those from before stop working.'],
288
+ ],
289
+ options: [
290
+ ['--full', 'Playwright\'s snapshot unchanged.'],
291
+ ['--grep <text>', 'only the lines containing text (role, name or a flag such as [disabled], any case), each with the named elements around it.'],
292
+ ['--regex <pattern>', '--grep by a regular expression: snapshot --regex "Total: \\d+".'],
293
+ ],
294
+ detail: `Refs: a ref (e3, or f1e3 in newer Playwright) works as a selector in any command: click e3. It belongs
295
+ to the tab whose snapshot gave it, and can change when the page changes (e102 may become f4e98): take a
296
+ new snapshot after it does.
297
+
298
+ --grep: a hit with no named element around it prints without a path. A hit on text (a label) keeps the
299
+ ref of the element holding it, so snapshot <that ref> shows what sits beside it, such as the value next
300
+ to the label.
246
301
 
247
302
  Output over 60 lines ends with a line count.`,
248
303
  },
249
304
  watch: {
250
305
  usage: 'watch on [--changes] [--live] | off | [n|new]',
251
306
  summary: 'record what happens in the tab; show the last n steps',
252
- detail: `Off until watch on; a watch on after watch off starts a new recording. Records clicks, typing (once
253
- it pauses), Enter and Escape, form changes, submits and navigations, each described by role and name
254
- like snapshot, with up to 5 of the requests it caused underneath, each with its status as in requests
255
- (body <#> for one), leaving out scripts and what requests hides. The REPL's own fill is recorded
256
- as type. A double-click shows as two clicks; hovering and moving the mouse are not recorded.
257
-
258
- From its first watch on, a tab keeps response bodies of up to 100 KB, so body <#> still shows one
307
+ args: [
308
+ ['on', 'starts recording the tab\'s steps; after watch off, a new recording.'],
309
+ ['off', 'stops it; anyone\'s watch off stops it, since a watch belongs to the tab.'],
310
+ ['(none) | <n>', 'says whether it is on, and shows the last 20 steps, or the last n.'],
311
+ ['new', 'only the steps not shown yet, and requests that have since arrived for the last one.'],
312
+ ],
313
+ optionsFor: 'watch on',
314
+ options: [
315
+ ['--changes', 'also what each step changed on the page once it settled, in up to 5 lines: + added, - removed, ~ changed.'],
316
+ ['--live', 'also prints each step in the REPL window once it settles (never in a command\'s answer), marked [watch], or [watch <url>] for a tab that is not the selected one.'],
317
+ ['--next-tab [part]', 'returns at once, then watches the next tab someone opens, or the next whose URL contains part, from its first page, and selects it for you. tab new, a client\'s own, does not count.'],
318
+ ],
319
+ detail: `What a step is: a click, typing (once it pauses), Enter or Escape, a form change, a submit or a
320
+ navigation, described by role and name as in snapshot, with up to 5 of the requests it caused under it,
321
+ each with its status as in requests (body <#> for one), leaving out scripts and what requests hides. The
322
+ REPL's own fill shows as type. A double-click shows as two clicks; hovering and moving the mouse are not
323
+ recorded.
324
+
325
+ Privacy: typed values are not recorded, and password fields not at all; --changes leaves out field
326
+ values, but what the page itself shows (e.g. "Hello <name>") is shown. Nothing is replayed.
327
+
328
+ Shared: every client sees the same steps, and watch new's place is shared.`,
329
+ moreAbout: 'bodies kept, --live timing, what --changes misses, and one --next-tab at a time',
330
+ more: `From its first watch on, a tab keeps response bodies of up to 100 KB, so body <#> still shows one
259
331
  after the tab navigates, even when the page navigates as soon as the request answers.
260
332
 
261
- watch on --next-tab [url-part] returns at once, then waits for the next tab someone opens (tab new,
262
- a client's own, does not count), or the next whose URL contains url-part, and watches it from its
263
- first page and selects it for the client that asked: for a tab someone is about to open. It takes
264
- --changes and --live too; watch off stops waiting, and modes lists it. One tab is waited for at a time:
265
- while one client waits, another's watch on --next-tab is refused, and its watch off leaves the wait on.
333
+ --live: a step cut short by the next one, or by watch off, prints at once; changes it made then show
334
+ under the next step.
266
335
 
267
- watch on its own says whether it is on and shows the last 20 steps; watch <n> shows the last n.
268
- watch new shows only the steps it has not shown yet, and requests that have since arrived for the
269
- last one.
336
+ --changes: each element once, with the first names inside it. Not for navigations: what a page changes
337
+ on its own after a step settles (data arriving later) shows under the next step.
270
338
 
271
- --live also prints each step in the REPL window (never in the server's answer to a command) once it
272
- settles, with its requests and changes, marked [watch], or [watch <url>] for a tab that is not the
273
- selected one. A step cut short by the next one, or by watch off, prints at once; changes it made
274
- then show under the next step.
275
-
276
- --changes also shows what each step changed on the page once it settles, in up to 5 lines: + added,
277
- - removed, ~ changed, each element once with the first names inside it. Not for navigations: what a
278
- page changes on its own after a step settles (data arriving later) shows under the next step.
279
-
280
- A watch belongs to the tab, not to a client: every client sees the same steps, watch new's place is
281
- shared, and anyone's watch off stops it.
282
-
283
- Typed values are not recorded, and password fields not at all; --changes leaves out field values,
284
- but what the page itself shows (e.g. "Hello <name>") is shown. Nothing is replayed.`,
339
+ --next-tab: it takes --changes and --live too; watch off stops waiting, and modes lists it. One tab is
340
+ waited for at a time: while one client waits, another's watch on --next-tab is refused, and its watch
341
+ off leaves the wait on.`,
285
342
  },
286
343
  text: { usage: 'text [--all] <selector>', summary: 'visible text of the first match' },
287
344
  html: { usage: 'html [--all] <selector>', summary: 'outer HTML of the first match' },
@@ -289,11 +346,13 @@ but what the page itself shows (e.g. "Hello <name>") is shown. Nothing is replay
289
346
  listeners: {
290
347
  usage: 'listeners <selector>|document|window',
291
348
  summary: 'the page\'s event listeners on an element',
292
- detail: `For the first match, one line each: the event, how it was added (capture, once, passive), the
293
- start of the handler, where ↵ marks a line break, and its line in its script. --all shows each handler
294
- whole, as written. Listeners added on a parent (e.g. document, for delegation) are not the element's
295
- own: check listeners document too. Those Playwright adds to window as it acts are left out, and it
296
- says how many.`,
349
+ options: [
350
+ ['--all', 'each handler whole, as written.'],
351
+ ],
352
+ detail: `For the first match, one line each: the event, how it was added (capture, once, passive), the start
353
+ of the handler, where ↵ marks a line break, and its line in its script. Listeners added on a parent
354
+ (e.g. document, for delegation) are not the element's own: check listeners document too. Those
355
+ Playwright adds to window as it acts are left out, and it says how many.`,
297
356
  },
298
357
  count: { usage: 'count <selector>', summary: 'number of matches' },
299
358
  visible: { usage: 'visible <selector>', summary: 'whether the first match is visible' },
@@ -301,290 +360,364 @@ says how many.`,
301
360
  inputs: { usage: 'inputs [--all]', summary: 'form controls on the page; values are omitted' },
302
361
  highlight: {
303
362
  usage: 'highlight [<selector> [--style=<css>] | off]',
304
- summary: 'draw a box over elements, for a person or a screenshot',
305
- detail: `highlight <selector> draws Playwright's highlight over every match, as playwright-cli's does: a
306
- tinted box, labelled with its locator, that follows the element as the page scrolls or changes, and is
307
- in screenshots. --style=<css> adds CSS to the box, quoted if it has spaces; this circles e5 in blue:
308
- highlight e5 --style="outline: 3px solid blue; border-radius: 50%"
309
-
310
- highlight on its own lists the selected tab's highlights. highlight --hide <selector> hides one, and
311
- highlight --hide or highlight off hides them all. A highlight is a mode of its tab: modes shows it,
312
- modes off hides it, and the REPL hides every one as it stops. Loading a new page drops them; a page that
313
- changes its route without loading keeps them.
363
+ summary: 'draw a box over elements, for the user or a screenshot',
364
+ args: [
365
+ ['<selector>', 'draws Playwright\'s highlight over every match, as playwright-cli\'s does: a tinted box labelled with its locator, that follows the element as the page scrolls or changes, and is in screenshots.'],
366
+ ['(none)', 'lists the selected tab\'s highlights.'],
367
+ ['off', 'hides them all, as highlight --hide does.'],
368
+ ],
369
+ options: [
370
+ ['--style=<css>', 'CSS added to the box, quoted if it has spaces: highlight e5 --style="outline: 3px solid blue; border-radius: 50%" circles e5 in blue.'],
371
+ ['--hide <selector>', 'hides one.'],
372
+ ],
373
+ detail: `A highlight is a mode of its tab: modes shows it, modes off hides it, and the REPL hides every one as
374
+ it stops. Loading a new page drops them; a page that changes its route without loading keeps them.
314
375
 
315
376
  The box is drawn over the page, in an element of Playwright's own added to it (x-pw-glass); the page's
316
377
  own elements are not changed, and clicks and typing reach them as before. Playwright always labels the
317
- box, and the label can cover what is just below it. For a screenshot without one, eval can outline
318
- the element itself (with border-radius: 50% for a circle). That changes the page, so put it back
319
- afterwards, as it was if it had a style of its own:
378
+ box, and the label can cover what is just below it.`,
379
+ moreAbout: 'marking a screenshot without the label',
380
+ more: `For a screenshot without the label, eval can outline the element itself (with border-radius: 50% for
381
+ a circle). That changes the page, so put it back afterwards, as it was if it had a style of its own:
320
382
  eval document.querySelector("#rate-3").style.outline = "3px solid blue"
321
383
  eval document.querySelector("#rate-3").style.outline = ""`,
322
384
  },
323
385
  screenshot: {
324
386
  usage: 'screenshot [<ref>] [--full] [-d <secs>] [name]',
325
387
  summary: 'save a PNG of the viewport, --full page or an element',
326
- detail: `Saved as screenshot-<name or timestamp>.png in $PW_SCREENSHOT_DIR; without it, next to the REPL's
327
- socket when it serves on one of its own, or else in /tmp. --filename=<file> saves it there instead (a
328
- JPEG if it ends in .jpg), relative to the folder pw-repl send runs in; it never replaces a file.
329
-
330
- screenshot e5 saves only that element, from a snapshot ref. --full-page is --full.
331
-
332
- It brings the tab to the front of its window first: Chrome draws only the tab in front. The image
333
- is at CSS pixel size, one image pixel per CSS pixel. While emulate mobile is on, it is what the
334
- phone shows: a page laid out wider than the phone (no viewport meta tag) comes out shrunk to fit.
335
-
336
- --delay counts down out loud first (maximum 60s), so someone can hold a hover or open a menu.`,
388
+ args: [
389
+ ['[<ref>]', 'only that element, from a snapshot ref: screenshot e5.'],
390
+ ['[name]', 'saved as screenshot-<name>.png; letters, digits, underscores and hyphens. Default: a timestamp.'],
391
+ ],
392
+ options: [
393
+ ['--full', 'the whole page, not only the viewport; --full-page too.'],
394
+ ['-d, --delay <secs>', 'counts down out loud first, up to 60, so someone can hold a hover or open a menu.'],
395
+ ['--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.'],
396
+ ],
397
+ detail: `Where: $PW_SCREENSHOT_DIR; without it, next to the REPL's socket when it serves on one of its own, or
398
+ else in /tmp.
399
+
400
+ It brings the tab to the front of its window first: Chrome draws only the tab in front. The image is
401
+ at CSS pixel size, one image pixel per CSS pixel. While emulate mobile is on, it is what the phone
402
+ shows: a page laid out wider than the phone (no viewport meta tag) comes out shrunk to fit.`,
337
403
  },
338
404
  cursor: {
339
405
  usage: 'cursor [on [<selector> | <x> <y>] | off]',
340
406
  summary: 'draw a pointer where the REPL acts, for videos',
341
- detail: `The REPL's clicks and typing don't move a pointer on screen, so a recording shows things
342
- happening with nothing pointing at them. cursor on draws a pointer in the selected tab. After that, these
343
- commands move it smoothly to their element or point before they act: click, dblclick, hover, fill, type
344
- <selector>, press <selector>, select, check, uncheck, upload and mousemove. Each move takes 150 to 600ms,
345
- depending on the distance (300 to 1000ms while the tab is recording), and the command waits for it. Clicks (click, dblclick, check, uncheck,
346
- mousedown, and upload when it clicks a button) show a ring where they land.
347
-
348
- It shows only the REPL's input, from every client, never the user's own mouse. Only the drawing moves: no
349
- mouse events are sent along the way, so the page gets the same input as with the cursor off. fill, select
350
- and the like move the drawing to their element but leave the real mouse where it was; mousedown, mouseup
351
- and mousewheel act at the real mouse, so the pointer moves back there first.
352
-
353
- cursor on fades it in where the mouse last went (or the middle), cursor on <selector> or <x> <y> fades it in
354
- there instead (a video's first element, say), cursor off fades it out, and cursor alone says whether it
355
- is on and where. It is a
356
- mode of its tab: modes lists it and modes off turns it off. It stays on across page loads, at the same
357
- spot, and shows in screenshots and recordings. Selectors, snapshot and text don't see it, it takes no
358
- clicks, and it points at elements inside frames too.`,
407
+ args: [
408
+ ['on', 'fades it in where the mouse last went, or the middle.'],
409
+ ['on <selector>', 'fades it in on the element (a video\'s first, say), scrolled into view; when it is on already, glides it there.'],
410
+ ['on <x> <y>', 'the same, at a point in the viewport.'],
411
+ ['off', 'fades it out.'],
412
+ ['(none)', 'says whether it is on, and where.'],
413
+ ],
414
+ detail: `The REPL's clicks and typing don't move a pointer on screen, so a recording shows things happening
415
+ with nothing pointing at them. With the cursor on, these commands move it smoothly to their element or
416
+ point before they act: click, dblclick, hover, fill, type <selector>, press <selector>, select, check,
417
+ uncheck, upload and mousemove. Each move takes 150 to 600ms by distance (300 to 1000ms while the tab
418
+ records), and the command waits for it. Clicks (click, dblclick, check, uncheck, mousedown, and upload
419
+ when it clicks a button) show a ring where they land.
420
+
421
+ Only the drawing moves: no mouse events are sent along the way, so the page gets the same input as with
422
+ the cursor off. It shows only the REPL's input, from every client, never the user's own mouse. fill,
423
+ select and the like move the drawing but leave the real mouse where it was; mousedown, mouseup and
424
+ mousewheel act at the real mouse, so the pointer moves back there first.
425
+
426
+ It is a mode of its tab: modes lists it and modes off turns it off. It stays on across page loads, at
427
+ the same spot, and shows in screenshots and recordings. Selectors, snapshot and text don't see it, it
428
+ takes no clicks, and it points at elements inside frames too.`,
359
429
  },
360
430
  record: {
361
- usage: 'record [on [file] [seconds] | off [file]]',
431
+ usage: 'record [on [file] [seconds] [options] | off [file]]',
362
432
  summary: 'record the selected tab\'s page to a video',
363
- detail: `record on starts recording the selected tab, and record off stops it and saves the file: its
364
- path, length, size and bytes. record on its own, or record off when nothing is recording, says whether the
365
- selected tab is recording, and how your last recording ended (record off, its seconds, its tab closing
366
- or the REPL stopping) with what record off would have said. tab close says what became of a recording it ended. A REPL stopped with pw-repl stop cannot be asked: stop prints what became of each recording it ended, and of one that ended by itself that record has not shown since.
367
-
368
- record off <file> saves it under that name instead. The format is set by record on: a name that does not end as its file does, or cannot be used, is refused, and it goes on recording. --filename=<file> is file too, as playwright-cli
369
- writes it.
370
-
371
- The file's ending picks the format: record on clip.mp4 records an MP4, and record on clip.webm, or record on with no file, a WebM (there is no --mp4 or --webm). The file is absolute or relative to the folder pw-repl send runs in, quoted if it has spaces, and
372
- owner-only, as a screenshot is; it never replaces a file. Without one, it is recording-<timestamp>.webm where screenshot saves. With seconds,
373
- it stops by itself after them (at most 3600, which is also the default); commands run meanwhile. One recording per tab. Another client with the tab selected can stop it too, as it can any mode; both are then told whose it was.
374
-
375
- It brings the tab to the front of its window first, as screenshot does. It records the page only,
376
- from inside the browser, at the viewport's size in CSS pixels, scrollbars included, made even (an
377
- odd width or height loses a pixel), a phone's too under emulate mobile: a Pixel 7's is 412 wide, less
378
- sharp than the phone draws it. It has no pointer (cursor on draws one) and no browser toolbars,
379
- and a tab behind another, hidden or headless keeps recording. Set the size first with viewport
380
- (1280x720 fills a 16:9 player); a page that changes size while recording is fitted into the first size.
381
- Chrome sends a frame as the page draws, and a still page keeps its real length. Under viewport or
382
- emulate, a page the tab loads while recording holds the last frame until the new page's HTML is in,
383
- and is then recorded at the viewport's size.
384
-
385
- While a tab records, type sends keys at a pace the video can show (help type), and mousewheel scrolls in
386
- small eased steps over up to 1s rather than at once; fill still sets the whole
387
- value at once, so use type for text the video should show being typed. record off also saves the steps
388
- next to the video, as clip.steps.txt for clip.webm: one line for each command run on the tab while it
389
- recorded, with when it began and ended in seconds into the video, the element or point it acted on in the
390
- video's pixels, and the command with what it acts on, never what it types or chooses, the code it runs or
391
- the URL it goes to. A (page) line marks each new page, loaded or routed to by a single-page app. They
392
- are for editing the video afterwards, such as zooming in on a step. After the page changes size, the steps leave out where they acted.
393
-
394
- It needs ffmpeg: $PW_FFMPEG, and only it when it is set (the REPL's, not send's); otherwise Playwright's own (npx playwright-core install ffmpeg, in $PLAYWRIGHT_BROWSERS_PATH or ~/.cache/ms-playwright), then one on the PATH. Playwright's writes only WebM; an .mp4 needs a system ffmpeg, with H.264. record on fails at once, recording nothing, without an ffmpeg that can write its format. If ffmpeg exits while recording, the recording ends then, the pane says what was saved, and the next record off fails with it. ffmpeg writes in blocks, so what it has not written yet is lost: the last seconds, or at first all of it. WebM plays in browsers,
395
- GitHub, GitLab and Slack; MP4 plays everywhere. It takes about 15 MB a minute.
396
-
397
- For a video with the pointer, or of the person's screen, help video compares the other ways.`,
433
+ args: [
434
+ ['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.'],
435
+ ['[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.'],
436
+ ['[seconds]', 'stops by itself after them, 1 to 3600. Default: 3600.'],
437
+ ],
438
+ optionsFor: 'record on',
439
+ options: [
440
+ ['--steps', 'also save clip.steps.txt next to clip.webm: when and where each command ran (Steps, below). Default: off.'],
441
+ ['--pause=<ms>', 'a pause after each action, so the viewer sees what it did. Default: 750; 0 none.'],
442
+ ['--lead=<ms>', 'still page recorded before record on returns. Default: 1000; counts toward the seconds.'],
443
+ ['--tail=<ms>', 'still page recorded after record off, before it stops. Default: 1000; none when it stops at its seconds.'],
444
+ ['--filename=<file>', 'the file, as playwright-cli writes it.'],
445
+ ],
446
+ detail: `What it records: the page only, from inside the browser, at the viewport's size in CSS pixels (set
447
+ it first: viewport 1280x720 fills a 16:9 player), with no pointer (cursor on draws one) and no toolbars. A hidden, background
448
+ or headless tab keeps recording. It brings the tab to the front first, as screenshot does. The file is
449
+ owner-only, as a screenshot is, and never replaces one. One recording per tab; another client with the
450
+ tab selected can stop it, and both are told whose it was.
451
+
452
+ While recording: type types at a pace the video can show (help type), mousewheel scrolls in eased steps,
453
+ an element out of view is scrolled to smoothly, each action is followed by --pause, and with the cursor
454
+ on it rests 300ms on the element before acting. fill still sets a value at once: use type for text the
455
+ viewer should see typed. Send the take as a file of commands (pw-repl send --file take.txt), or the time
456
+ between commands is in the video. help video has the whole way through.
457
+
458
+ Steps (--steps): one line per command run on the tab while it recorded: when it began and ended, in
459
+ seconds into the video, and the element or point it acted on, in the video's pixels. Use it to find a
460
+ moment in the video ("at 0:12 it clicked Save"), to add captions or cuts in an editor, or to check what
461
+ ran. It names the command and what it acted on, never what it typed or chose, the code it ran or a URL.
462
+ A (page) line marks each new page, loaded or routed to by a single-page app. It is renamed with the
463
+ video.
464
+
465
+ ffmpeg: $PW_FFMPEG, and only it when it is set (the REPL's, not send's); otherwise Playwright's own (npx
466
+ playwright-core install ffmpeg, in $PLAYWRIGHT_BROWSERS_PATH or ~/.cache/ms-playwright), then one on the
467
+ PATH. Playwright's writes only WebM; an MP4 needs a system ffmpeg with H.264. Without one that can write
468
+ the format, record on fails at once, recording nothing. WebM plays in browsers, GitHub, GitLab and Slack;
469
+ MP4 everywhere. About 15 MB a minute.`,
470
+ moreAbout: 'an ffmpeg that exits mid-recording, a page that changes size, phones under emulate, and how a recording ended',
471
+ more: `An ffmpeg that exits mid-recording ends the recording then; the pane says what was saved, and the
472
+ next record off fails with it. ffmpeg writes in blocks, so what it has not written yet is lost: the last
473
+ seconds, or at first all of it.
474
+
475
+ A page that changes size while recording is fitted into the first size, and the steps after it leave
476
+ out where they acted. Under viewport or emulate, a page the tab loads while recording holds the last
477
+ frame until the new page's HTML is in, and is then recorded at the viewport's size. Chrome sends a
478
+ frame as the page draws, and a still page keeps its real length. The size is made even (an odd width or
479
+ height loses a pixel), scrollbars included; a phone's under emulate mobile is its CSS size: a Pixel 7's
480
+ is 412 wide, less sharp than the phone draws it.
481
+
482
+ How it ended: record on its own, or record off when nothing is recording, says how your last recording
483
+ ended (record off, its seconds, its tab closing or the REPL stopping) with what record off would have
484
+ said. tab close says what became of a recording it ended. A REPL stopped with pw-repl stop cannot be
485
+ asked: stop prints what became of each recording it ended, and of one that ended by itself that record
486
+ has not shown since.
487
+
488
+ A name given to record off that does not end as its file does, or cannot be used, is refused, and it
489
+ goes on recording. There is no --mp4 or --webm: the file's ending picks the format.`,
398
490
  },
399
491
  viewport: {
400
492
  usage: 'viewport [WxH | off]',
401
493
  summary: 'show, set or unset the viewport size',
402
- detail: `viewport 1280x720 sets the selected tab's page to that size, until viewport off, modes off or the
403
- REPL exits. The window is not resized. viewport on its own shows the size, and whether it was set or is
404
- the window's. emulate mobile sets a phone's size instead, and each refuses while the other is on.`,
494
+ args: [
495
+ ['<W>x<H>', 'sets the selected tab\'s page to that size, until viewport off, modes off or the REPL exits. The window is not resized.'],
496
+ ['off', 'back to the window\'s size.'],
497
+ ['(none)', 'shows the size, and whether it was set or is the window\'s.'],
498
+ ],
499
+ detail: `emulate mobile sets a phone's size instead, and each refuses while the other is on.`,
405
500
  },
406
501
  emulate: {
407
502
  usage: 'emulate [<what> [off] | off]',
408
503
  summary: 'emulate a phone, dark mode, a locale or a timezone',
409
- detail: `emulate mobile [device] a phone's screen, touch and user agent: a Pixel 7, or a device named as in
410
- Playwright's list, e.g. emulate mobile iPhone 13; a name that is not
411
- exact lists the devices it matches (emulate mobile galaxy)
412
- emulate dark | light the color scheme the page's CSS and matchMedia see
413
- emulate locale <tag> the language (Accept-Language, navigator.language) and date and number
414
- formats, e.g. fr-FR
415
- emulate timezone <zone> an IANA timezone, e.g. Asia/Tokyo
416
- emulate <what> off stop one (dark or light off stops the color scheme); emulate off stops all
417
-
418
- Per tab, until turned off or the REPL exits; emulate mobile <device> while another is on switches to it.
419
- emulate on its own shows what is on.
420
-
421
- The page sees the user agent, touch and navigator.languages from its next load: reload after
422
- turning mobile or locale on or off. Touch means the page sees a touch screen; click still clicks
423
- with the mouse, and sends no touch events. Dark, light and timezone apply at once, but what the page
424
- already drew with them (times, colors set by its script) changes only when it reloads.
425
-
426
- While mobile is on, viewport refuses: the device sets the size.`,
504
+ args: [
505
+ ['mobile [device]', 'a phone\'s screen, touch and user agent: a Pixel 7, or a device named as in Playwright\'s list, e.g. emulate mobile iPhone 13. A name that is not exact lists the devices it matches (emulate mobile galaxy).'],
506
+ ['dark | light', 'the color scheme the page\'s CSS and matchMedia see.'],
507
+ ['locale <tag>', 'the language (Accept-Language, navigator.language) and date and number formats, e.g. fr-FR.'],
508
+ ['timezone <zone>', 'an IANA timezone, e.g. Asia/Tokyo.'],
509
+ ['<what> off', 'stops one (dark or light off stops the color scheme).'],
510
+ ['off', 'stops all.'],
511
+ ['(none)', 'shows what is on.'],
512
+ ],
513
+ detail: `Per tab, until turned off or the REPL exits; emulate mobile <device> while another is on switches to
514
+ it. While mobile is on, viewport refuses: the device sets the size.
515
+
516
+ The page sees the user agent, touch and navigator.languages from its next load: reload after turning
517
+ mobile or locale on or off. Touch means the page sees a touch screen; click still clicks with the
518
+ mouse, and sends no touch events. Dark, light and timezone apply at once, but what the page already
519
+ drew with them (times, colors set by its script) changes only when it reloads.`,
427
520
  },
428
521
  wait: {
429
522
  usage: 'wait [text|request] <what> [--gone] [secs]',
430
523
  summary: 'wait for an element, text, a response or a load (10s)',
431
- detail: `wait <selector> waits for a matching element to be visible, and wait text <text> for text to be;
432
- wait request <url-part|glob> waits for a matching response, counting one that finished since the
433
- previous command began, or is still under way (so click, then wait request, does not miss it).
434
-
435
- wait load waits for the page to finish loading (its load event). A navigation that began since the
436
- previous command began counts, so click a link, then wait load, waits for the new page. A page
437
- that changes its own URL without loading (an app's own routing) is not a load: wait for something on
438
- the new view instead.
439
-
440
- wait text matches any part of an element's text, in any case.
441
-
442
- --gone waits instead until nothing matching is visible (removed or hidden): wait .spinner --gone.
443
- It is done at once if nothing matches yet, so wait for it to appear first if it may not have.
444
-
445
- Up to 120s. A wait that times out is an error; the REPL carries on.`,
524
+ args: [
525
+ ['<selector>', 'for a matching element to be visible.'],
526
+ ['text <text>', 'for text to be visible: any part of an element\'s text, any case.'],
527
+ ['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.'],
528
+ ['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.'],
529
+ ['[secs]', 'how long, up to 120. Default: 10.'],
530
+ ],
531
+ options: [
532
+ ['--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.'],
533
+ ],
534
+ detail: `A page that changes its own URL without loading (an app's own routing) is not a load: wait for
535
+ something on the new view instead. A wait that times out is an error; the REPL carries on.`,
536
+ },
537
+ sleep: {
538
+ usage: 'sleep <ms>',
539
+ summary: 'wait a fixed time, in milliseconds (maximum 3600000)',
540
+ detail: `While recording, each action is already followed by a pause (help record): a sleep adds to it.`,
446
541
  },
447
- sleep: { usage: 'sleep <ms>', summary: 'wait a fixed time, in milliseconds (maximum 3600000)' },
448
542
  requests: {
449
543
  usage: 'requests [--all] [n] [url-filter]',
450
544
  summary: 'the selected tab\'s last n requests (default 20)',
451
- detail: `Recording starts when the REPL connects, and a tab opened since is recorded from its first page (one a
452
- link opens included); the last 200 per tab are kept, of every kind the page
453
- requests (not the browser's own, such as the favicon). Each line: #number, time, method, status (HTTP
454
- code, pending, no response, failed: <reason>, <code>, then failed: <reason> when its body was cut
455
- off, or <code> faked), duration, kind (document, fetch, xhr, script, image, ...), URL. A request that
456
- finished a moment ago can still show pending.
457
-
458
- n is how many of the latest to show (default 20). When more match, a first line says so: (last 20
459
- of 112 kept; requests 112 shows them all). A page often makes more than 20 in one load. A line
460
- "--- the page loads <url>" marks where each page load starts, even when a filter hides that request.
461
-
462
- body <#> shows what a request got back.
463
-
464
- Images, fonts, stylesheets, media and extension requests are hidden unless --all is given.
465
-
466
- url-filter is a substring: requests 20 /api/ shows API calls only, e.g. when dev-server scripts
467
- crowd the list. --regex <pattern>, at the end, matches URLs by a regular expression instead.`,
545
+ args: [
546
+ ['[n]', 'how many of the latest to show. Default: 20. When more match, a first line says so: (last 20 of 112 kept; requests 112 shows them all).'],
547
+ ['[url-filter]', 'a part of the URL: requests 20 /api/ shows API calls only.'],
548
+ ],
549
+ options: [
550
+ ['--all', 'also images, fonts, stylesheets, media and extension requests, hidden otherwise.'],
551
+ ['--regex <pattern>', 'at the end: matches URLs by a regular expression instead.'],
552
+ ],
553
+ detail: `Each line: #number, time, method, status (HTTP code, pending, no response, failed: <reason>, <code>
554
+ then failed: <reason> when its body was cut off, or <code> faked), duration, kind (document, fetch, xhr,
555
+ script, image, ...), URL. A line "--- the page loads <url>" marks where each page load starts, even when
556
+ a filter hides that request. body <#> shows what a request got back.
557
+
558
+ Kept: from when the REPL connects, and a tab opened since from its first page (one a link opens
559
+ included); the last 200 per tab, of every kind the page requests (not the browser's own, such as the
560
+ favicon). A page often makes more than 20 in one load. One that finished a moment ago can still show
561
+ pending.`,
468
562
  },
469
563
  body: {
470
564
  usage: 'body [--all] <#>|<url-part>',
471
565
  summary: 'the response body of request <#>, or of the latest one matching',
472
- detail: `body <url-part> shows the latest finished request whose URL contains url-part (body slots.json).
473
-
474
- Read from the browser on demand: JSON is pretty-printed, binary is not shown, output is capped. The
475
- browser may drop a body (e.g. after the tab navigates); then body reports not available, unless a
476
- watch kept it (help watch).
477
-
478
- Bodies can contain sensitive data.`,
566
+ args: [
567
+ ['<#>', 'a request\'s number, from requests.'],
568
+ ['<url-part>', 'the latest finished request whose URL contains it: body slots.json.'],
569
+ ],
570
+ options: [
571
+ ['--all', 'the whole body, not capped.'],
572
+ ],
573
+ detail: `Read from the browser on demand: JSON is pretty-printed, binary is not shown. The browser may drop a
574
+ body (e.g. after the tab navigates); then body reports not available, unless a watch kept it (help
575
+ watch). Bodies can contain sensitive data.`,
479
576
  },
480
577
  console: {
481
578
  usage: 'console [--all] [n] [level|filter]',
482
579
  summary: 'the last n console messages and page errors',
483
- detail: `Recording starts when the REPL connects; the last 200 per tab are kept. Each line: time, [type],
484
- text. Types are console levels (log, warning, error, ...) and pageerror for uncaught exceptions. A
485
- "--- the page loaded <url>" line marks each load after the first message shown.
486
-
487
- A level (error, warning, info, debug) shows that level and those above it: console warning shows
488
- warnings and errors, page errors among them. info, which leaves out debug, is what playwright-cli
489
- shows by default. Anything else is a filter, matching part of the type or the text.
490
-
491
- n is how many of the latest to show (default 20); when more match, a first line says so.
492
-
493
- A page brought back from the back/forward cache (back, forward) reports its earlier messages again,
494
- at the time it comes back, though nothing loaded again.`,
580
+ args: [
581
+ ['[n]', 'how many of the latest to show. Default: 20; when more match, a first line says so.'],
582
+ ['[level]', 'error, warning, info or debug: that level and those above it. console warning shows warnings and errors, page errors among them. info, which leaves out debug, is playwright-cli\'s default.'],
583
+ ['[filter]', 'anything else: matches part of the type or the text.'],
584
+ ],
585
+ options: [
586
+ ['--all', 'each message whole, not capped.'],
587
+ ],
588
+ detail: `Each line: time, [type], text. Types are console levels (log, warning, error, ...) and pageerror for
589
+ uncaught exceptions. A "--- the page loaded <url>" line marks each load after the first message shown.
590
+ Kept from when the REPL connects; the last 200 per tab.
591
+
592
+ A page brought back from the back/forward cache (back, forward) reports its earlier messages again, at
593
+ the time it comes back, though nothing loaded again.`,
495
594
  },
496
595
  capture: {
497
596
  usage: 'capture on [requests|console] [secs] | off',
498
597
  summary: 'record requests and console together, in time order',
499
- detail: `capture on records both until capture off, which prints them; requests or console records only one.
500
- With secs (1-3600 seconds) it records that long, then prints. Other commands wait until a timed
501
- capture ends, so it suits recording what someone does in the browser; around your own commands, use
502
- capture on and capture off.
503
-
504
- One capture runs at a time, on the tab selected when it started. Unlike requests and console it
505
- keeps more than the last 200 and lists both together.
506
-
507
- capture on its own says whether one is running, or shows the last one.`,
598
+ args: [
599
+ ['on', 'records both until capture off, which prints them.'],
600
+ ['requests | console', 'records only one.'],
601
+ ['[secs]', 'records that long (1 to 3600), then prints. Other commands wait until it ends.'],
602
+ ['(none)', 'says whether one is running, or shows the last one.'],
603
+ ],
604
+ detail: `A timed capture suits recording what someone does in the browser; around your own commands, use
605
+ capture on and capture off. One runs at a time, on the tab selected when it started. Unlike requests
606
+ and console it keeps more than the last 200, and lists both together.`,
508
607
  },
509
608
  route: {
510
609
  usage: 'route <glob> <how> | off <glob>|--all',
511
610
  summary: 'fake, patch, delay or fail the selected tab\'s matching requests',
512
- detail: `route <glob> <status> <json> answer with this status (200-599) and JSON, or no body; it never
513
- reaches the network, so it still answers while the network is off.
514
- The body is the rest of the line. Another kind needs its type:
515
- route <glob> 200 --content-type=text/plain hello
516
- route <glob> patch <json> let it through, then change its JSON response: a JSON Merge Patch, where
517
- objects merge, null removes a key and anything else replaces; a
518
- patch that is not an object (an array) replaces the whole body
519
- route <glob> delay <secs> hold it for up to 120 seconds, then let it through
520
- route <glob> abort fail it as if the connection broke
521
-
522
- Each matching request prints a line (Faked:, Patched:, Delayed:, Aborted:) in the REPL window, and in
523
- the answer to a command running then if that command's client has the tab selected, with its number as in requests (one the page makes after reload or
524
- a click has answered shows only in the window: wait request <glob> waits for it); requests shows a fake
525
- as <status> faked and a patch as <status> patched. If a route fails it prints "Route failed" and aborts
526
- the request.
527
-
528
- Routes belong to the tab and last until route off <glob> (or route off --all) or the REPL exits.
529
- Routing the same glob again replaces it. route on its own lists the selected tab's routes. A glob may
530
- be quoted.
531
-
532
- Example: route **/api/cart patch {"total": 0}`,
611
+ args: [
612
+ ['<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.'],
613
+ ['<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.'],
614
+ ['<glob> delay <secs>', 'holds it for up to 120 seconds, then lets it through.'],
615
+ ['<glob> abort', 'fails it as if the connection broke.'],
616
+ ['off <glob> | --all', 'removes one route, or all.'],
617
+ ['(none)', 'lists the selected tab\'s routes.'],
618
+ ],
619
+ options: [
620
+ ['--content-type=<t>', 'the type of a body that is not JSON: route <glob> 200 --content-type=text/plain hello.'],
621
+ ],
622
+ detail: `Each matching request prints a line (Faked:, Patched:, Delayed:, Aborted:) with its number as in
623
+ requests: in the REPL window, and in the answer to a command running then if that command's client has
624
+ the tab selected. One the page makes after reload or a click has answered shows only in the window
625
+ (wait request <glob> waits for it). requests shows a fake as <status> faked and a patch as <status>
626
+ patched. A route that fails prints "Route failed" and aborts the request.
627
+
628
+ Routes belong to the tab and last until route off or the REPL exits. Routing the same glob again
629
+ replaces it. A glob may be quoted. Example: route **/api/cart patch {"total": 0}`,
533
630
  },
534
631
  network: {
535
632
  usage: 'network [on|off|slow [<ms> [<kbps>]]]',
536
633
  summary: 'cut, slow or restore the tab\'s network',
537
- detail: `network off cuts it, like dropped wifi. Stopping a service is not the same: a dev proxy in front of
538
- it usually holds the request open, so the page spins instead of failing.
539
-
540
- network slow makes each request take at least <ms> (Chrome's latency is a minimum, not added to a
541
- slow server's own time) and limits its speed: by default as DevTools' Slow 4G (563ms, 1440 kbps
542
- down, 675 up); network slow <ms> [<kbps>] sets them, kbps both ways (left out, it stays Slow
543
- 4G's). To slow one API, use route <glob> delay <secs>.
544
-
545
- Per tab; lasts until network on or the REPL exits. Routes still answer while it is off or slow.`,
634
+ args: [
635
+ ['off', 'cuts it, like dropped wifi.'],
636
+ ['slow [<ms> [<kbps>]]', 'each request takes at least <ms> (a minimum, not added to a slow server\'s own time), at <kbps> both ways. Default: DevTools\' Slow 4G: 563ms, 1440 kbps down, 675 up.'],
637
+ ['on', 'restores it.'],
638
+ ],
639
+ detail: `Per tab; lasts until network on or the REPL exits. Routes still answer while it is off or slow. To slow
640
+ one API, use route <glob> delay <secs>. Stopping a service is not the same as network off: a dev proxy
641
+ in front of it usually holds the request open, so the page spins instead of failing.`,
546
642
  },
547
643
  eval: {
548
644
  usage: 'eval [--all] <JavaScript>',
549
645
  summary: 'evaluate JavaScript in the selected tab and print the result',
550
- detail: `Promises are awaited, await works at the top level, and a function (() => document.title) is
551
- called. eval <function> <ref> calls it with that element: eval "el => el.textContent" e5.
552
-
553
- eval fetch(...) sends a new request, which can change state on the server; it cannot read one that
646
+ args: [
647
+ ['<JavaScript>', 'the rest of the line. Promises are awaited, await works at the top level, and a function (() => document.title) is called.'],
648
+ ['<function> <ref>', 'calls it with that element: eval "el => el.textContent" e5.'],
649
+ ],
650
+ options: [
651
+ ['--all', 'the whole result, not capped.'],
652
+ ],
653
+ detail: `eval fetch(...) sends a new request, which can change state on the server; it cannot read one that
554
654
  already happened (requests lists those).`,
555
655
  },
556
656
  cdp: {
557
657
  usage: 'cdp [--all] <method> <JSON object>',
558
658
  summary: 'send one CDP command through a temporary session',
559
- detail: `The session is detached afterwards, so subscriptions and settings do not persist.
560
-
561
- Browser.close and Target.closeTarget are refused, and so is Page.handleJavaScriptDialog: dialog answers one.`,
659
+ args: [
660
+ ['<method>', 'a CDP method: Network.getCookies.'],
661
+ ['<JSON object>', 'its parameters: {} for none.'],
662
+ ],
663
+ options: [
664
+ ['--all', 'the whole result, not capped.'],
665
+ ],
666
+ detail: `The session is detached afterwards, so subscriptions and settings do not persist. Browser.close and
667
+ Target.closeTarget are refused, and so is Page.handleJavaScriptDialog: dialog answers one.`,
668
+ },
669
+ cookies: {
670
+ usage: 'cookies [--all]',
671
+ summary: 'cookie names, domains and flags; values are omitted',
672
+ detail: `--all lists every cookie, not capped. cdp Network.getCookies {} shows the values.`,
673
+ },
674
+ storage: {
675
+ usage: 'storage [--all]',
676
+ summary: 'localStorage keys; values are omitted',
677
+ detail: `--all lists every key, not capped. eval localStorage.getItem("<key>") shows a value.`,
562
678
  },
563
- cookies: { usage: 'cookies [--all]', summary: 'cookie names, domains and flags; values are omitted' },
564
- storage: { usage: 'storage [--all]', summary: 'localStorage keys; values are omitted' },
565
679
  modes: {
566
680
  usage: 'modes [off [--mine]]',
567
681
  summary: 'the modes on in every tab; modes off turns them all off',
568
- detail: `The modes are watch, network off or slow, route, capture, record, emulate, viewport, highlight and cursor.
569
- Each is turned on and off with its own command: watch on|off, network off|slow|on, route <glob> ... |
570
- route off <glob>|--all, capture on|off, record on|off, emulate ... | emulate off, viewport <WxH> |
571
- viewport off, highlight <selector> | highlight off, cursor on|off.
572
-
573
- modes off turns off every one in every tab; a capture it stops is kept for capture to show, and a
574
- recording it stops is saved, as record off would. With
575
- clients (help session), modes says which client turned each on, and modes off --mine turns off only
576
- the sender's own.`,
682
+ args: [
683
+ ['(none)', 'lists the modes on in every tab, and with clients (help session), which client turned each on.'],
684
+ ['off', 'turns off every one in every tab; a capture it stops is kept for capture to show, and a recording it stops is saved, as record off would.'],
685
+ ],
686
+ options: [
687
+ ['--mine', 'after off: only the sender\'s own.'],
688
+ ],
689
+ detail: `The modes, each turned on and off with its own command: watch on|off, network off|slow|on, route <glob>
690
+ ... | route off <glob>|--all, capture on|off, record on|off, emulate ... | emulate off, viewport <WxH> |
691
+ viewport off, highlight <selector> | highlight off, cursor on|off.`,
577
692
  },
578
693
  dialog: {
579
694
  usage: 'dialog [accept [text] | dismiss]',
580
695
  summary: 'show, accept or dismiss an open alert, confirm or prompt',
581
- detail: `While a dialog is open, its page and the commands that read it wait, a click that opened it included, until it is answered. Commands run one at a time, so every client's commands behind that one wait too, in any tab: in a shared REPL, answer a dialog you open. dialog runs at once, ahead
582
- of them; it answers the selected tab's dialog (marked *), or the only one open. accept text answers a prompt, and accept alone keeps its default, as OK in the browser does.
583
-
584
- An agent can answer any dialog, one it opened included; one the person answers in the browser is gone
585
- from dialog then. cdp cannot answer one: Chrome lets only a session that saw it open do so. With nobody at the browser (e.g. headless), it is the only way to answer one.`,
696
+ args: [
697
+ ['(none)', 'lists the open dialogs; * marks the selected tab\'s.'],
698
+ ['accept [text]', 'answers OK: text answers a prompt, and accept alone keeps its default, as OK in the browser does.'],
699
+ ['dismiss', 'answers Cancel.'],
700
+ ],
701
+ detail: `It answers the selected tab's dialog, or the only one open, and runs at once, ahead of other commands.
702
+ While a dialog is open, its page and the commands that read it wait, a click that opened it included,
703
+ until it is answered. Commands run one at a time, so every client's commands behind that one wait too,
704
+ in any tab: in a shared REPL, answer a dialog you open.
705
+
706
+ An agent can answer any dialog, one it opened included; one the user answers in the browser is gone
707
+ from dialog then. cdp cannot answer one: Chrome lets only a session that saw it open do so. With nobody
708
+ at the browser (e.g. headless), it is the only way to answer one.`,
709
+ },
710
+ help: {
711
+ usage: 'help [topic | command | --all]',
712
+ summary: 'this help; --all prints every topic and command in full',
713
+ args: [
714
+ ['(none)', 'what this is, and the topics.'],
715
+ ['<topic>', 'the topic, and one line per command in it.'],
716
+ ['<command>', 'its usage, arguments, options and what most need to know.'],
717
+ ['<command> --all', 'all of it, with what only goes wrong now and then.'],
718
+ ['--all', 'every topic and command in full.'],
719
+ ],
586
720
  },
587
- help: { usage: 'help [topic | command | --all]', summary: 'this help; --all prints every topic and command in full' },
588
721
  quit: {
589
722
  usage: 'quit',
590
723
  summary: 'disconnect, leaving Chromium running (one --launch started stops too)',
@@ -593,24 +726,52 @@ from dialog then. cdp cannot answer one: Chrome lets only a session that saw it
593
726
  };
594
727
 
595
728
  for (const topic of Object.values(TOPICS)) topic.intro = wrap(topic.intro);
596
- for (const entry of Object.values(COMMANDS)) if (entry.detail) entry.detail = wrap(entry.detail);
729
+ for (const entry of Object.values(COMMANDS)) {
730
+ if (entry.detail) entry.detail = wrap(entry.detail);
731
+ if (entry.more) entry.more = wrap(entry.more);
732
+ }
733
+
734
+ // An Arguments or Options list: each name in a column of its own, its text wrapped beside it.
735
+ const NAME_WIDTH = 18;
736
+ function list(items, width = WIDTH) {
737
+ return items.flatMap(([name, text]) => {
738
+ const lines = wrap(text, width - NAME_WIDTH - 4).split('\n');
739
+ const first = name.length <= NAME_WIDTH ? ` ${name.padEnd(NAME_WIDTH)} ${lines.shift()}` : ` ${name}`;
740
+ return [first, ...lines.map(l => `${' '.repeat(NAME_WIDTH + 4)}${l}`)];
741
+ }).join('\n');
742
+ }
597
743
 
598
744
  function commandLines(names) {
599
745
  const width = Math.max(...names.map(n => COMMANDS[n].usage.length));
600
746
  return names.map(n => ` ${COMMANDS[n].usage.padEnd(width)} ${COMMANDS[n].summary}`);
601
747
  }
602
748
 
603
- function renderCommand(name) {
749
+ // A command's help: usage and summary, its arguments and options, what most need to know, and, with
750
+ // full, the rest (what goes wrong rarely); without it, a line says where the rest is.
751
+ function commandBody(name, full) {
604
752
  const entry = COMMANDS[name];
605
- return [`${entry.usage} — ${entry.summary}`, ...(entry.detail ? ['', entry.detail] : [])].join('\n');
753
+ const parts = [];
754
+ const lists = [];
755
+ if (entry.args) lists.push(`Arguments:\n${list(entry.args)}`);
756
+ if (entry.options) lists.push(`Options${entry.optionsFor ? ` (${entry.optionsFor})` : ''}:\n${list(entry.options)}`);
757
+ if (lists.length) parts.push(lists.join('\n'));
758
+ if (entry.detail) parts.push(entry.detail);
759
+ if (entry.more) parts.push(full ? entry.more : wrap(`More, on ${entry.moreAbout}: help ${name} --all.`));
760
+ return parts.join('\n\n');
761
+ }
762
+
763
+ function renderCommand(name, full = false) {
764
+ const entry = COMMANDS[name];
765
+ const body = commandBody(name, full);
766
+ return [`${entry.usage} — ${entry.summary}`, ...(body ? ['', body] : [])].join('\n');
606
767
  }
607
768
 
608
769
  // Every topic's command list, with each command's details indented under it.
609
770
  function renderAll() {
610
771
  const sections = Object.entries(TOPICS).map(([name, topic]) => {
611
772
  const lines = commandLines(topic.commands).flatMap((line, i) => {
612
- const detail = COMMANDS[topic.commands[i]].detail;
613
- return detail ? [line, ...detail.split('\n').map(d => (d ? ` ${d}` : '')), ''] : [line];
773
+ const body = commandBody(topic.commands[i], true);
774
+ return body ? [line, ...body.split('\n').map(d => (d ? ` ${d}` : '')), ''] : [line];
614
775
  });
615
776
  if (lines[lines.length - 1] === '') lines.pop();
616
777
  return [`== ${name} ==`, topic.intro, '', ...lines].join('\n');
@@ -618,23 +779,6 @@ function renderAll() {
618
779
  return [OVERVIEW, ...sections].join('\n\n');
619
780
  }
620
781
 
621
- // Not a command: the ways to record a video, since none is right for every use.
622
- const VIDEO = `There are three ways to record a video of a page, and each gives something up.
623
-
624
- record on (help record) records the selected tab's page from inside the browser: headless, hidden and
625
- background tabs too, with nothing to set up but ffmpeg. It has no pointer and no browser toolbars:
626
- cursor on draws a pointer that shows where the REPL clicks. For quick clips, and for an agent recording
627
- its own work.
628
-
629
- A screen recorder on the person's machine (OBS, ffmpeg -f x11grab, macOS screencapture -v) records their
630
- screen as they see it, pointer included, smooth and in real time. It records the screen or a window, not
631
- a tab: size the window, and crop off the toolbars, for just the page. Ask before starting one, or have
632
- them start it. It cannot see a headless browser: for a browser of the REPL's own, --launch --headed on
633
- a screen. For a polished video of a person's browser.
634
-
635
- Microsoft's playwright-cli (the npm package @playwright/cli; video-start and video-stop) records a
636
- browser of its own, not this one, which starts logged out. For a scripted flow in a clean browser.`;
637
-
638
782
  function render(topicOrCommand) {
639
783
  if (!topicOrCommand) return OVERVIEW;
640
784
  if (topicOrCommand === 'playwright-cli') {
@@ -651,11 +795,10 @@ Its other options are refused, never read as part of a selector or a value.
651
795
  Different on purpose, since the browser may be someone else's too:
652
796
  routes, network and emulation per tab, not the whole browser, so other people's tabs are untouched
653
797
  close turns off your modes; tabs stay open (tab close <url-part> closes one)
654
- upload <selector> <file> names the input: file pickers are not caught, as a person's would be
655
- no snapshot after each command a person reads the pane too; snapshot when you need one
798
+ upload <selector> <file> names the input: file pickers are not caught, as the user's would be
799
+ no snapshot after each command the user reads the pane too; snapshot when you need one
656
800
  -s=<session> -c <name> is the nearest: a client with a selected tab of its own`);
657
801
  }
658
- if (topicOrCommand === 'video') return wrap(VIDEO);
659
802
  if (topicOrCommand === '--all') return renderAll();
660
803
  if (Object.hasOwn(TOPICS, topicOrCommand)) {
661
804
  const topic = TOPICS[topicOrCommand];
@@ -664,6 +807,8 @@ Different on purpose, since the browser may be someone else's too:
664
807
  return Object.hasOwn(COMMANDS, topicOrCommand) ? `${text}\n\n${renderCommand(topicOrCommand)}` : text;
665
808
  }
666
809
  if (Object.hasOwn(COMMANDS, topicOrCommand)) return renderCommand(topicOrCommand);
810
+ const full = /^(\S+)\s+--all$/.exec(topicOrCommand);
811
+ if (full && Object.hasOwn(COMMANDS, full[1])) return renderCommand(full[1], true);
667
812
  // A playwright-cli name: what it runs here, with that command's help.
668
813
  const cli = require('./cli-names').describe(topicOrCommand);
669
814
  if (cli?.nearest) return wrap(`${topicOrCommand} is playwright-cli's; ${cli.nearest}`);