pw-repl 0.12.6 → 0.12.8
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 +4 -2
- package/README.md +76 -74
- package/bin/pw-repl.js +152 -65
- package/lib/cli-names.js +3 -3
- package/lib/elements.js +115 -26
- package/lib/emulation.js +5 -3
- package/lib/help.js +118 -47
- package/lib/inspect.js +28 -6
- package/lib/launch.js +3 -1
- package/lib/record.js +1 -1
- package/lib/runner.js +11 -0
- package/lib/send.js +16 -4
- package/lib/start.js +9 -3
- package/lib/syntax.js +11 -5
- package/package.json +1 -1
- package/skill/SKILL.md +30 -42
package/lib/elements.js
CHANGED
|
@@ -4,7 +4,7 @@ const fs = require('fs');
|
|
|
4
4
|
const { state, onShutdown } = require('./state');
|
|
5
5
|
const out = require('./output');
|
|
6
6
|
const { takeOptions } = require('./cli-names');
|
|
7
|
-
const { unquote, toSelector, keyName, splitSelector, REF } = require('./syntax');
|
|
7
|
+
const { unquote, unescapeDouble, toSelector, keyName, splitSelector, REF } = require('./syntax');
|
|
8
8
|
const { numbers, hints, BUSY_PAGE } = require('./util');
|
|
9
9
|
const { tabState } = require('./tabstate');
|
|
10
10
|
const { cursorOf, aiming, aimAt, glideTo, showClick, showSelect } = require('./cursor');
|
|
@@ -35,18 +35,37 @@ const MODIFIERS = ['Alt', 'Control', 'ControlOrMeta', 'Meta', 'Shift'];
|
|
|
35
35
|
|
|
36
36
|
// click <selector> [button] [--modifiers=<keys>], as playwright-cli's click
|
|
37
37
|
// takes them. The button is read only after a ref or a quoted selector, so
|
|
38
|
-
// click text=Turn right is still one selector.
|
|
39
|
-
|
|
38
|
+
// click text=Turn right is still one selector; --button=right, as Playwright's option, after any.
|
|
39
|
+
// click takes --click-count too; dblclick is click's count of 2.
|
|
40
|
+
function clickArgs(args, usage, counts = false) {
|
|
40
41
|
// Found outside quotes only, so a quoted selector keeps whatever is in it.
|
|
41
|
-
const { found, rest: text } = takeOptions((args || '').trim(), { modifiers: 'value' });
|
|
42
|
-
|
|
42
|
+
const { found, rest: text } = takeOptions((args || '').trim(), { modifiers: 'value', button: 'value', 'click-count': 'value' });
|
|
43
|
+
if (!counts && found.some(o => o.name === 'click-count')) throw new Error('dblclick is a click count of 2; for another count, click --click-count=<n>');
|
|
44
|
+
const clickCount = clickCountOf(found, usage);
|
|
45
|
+
const modifiers = found.filter(o => o.name === 'modifiers').flatMap(o => (o.value || '').split(',').filter(Boolean).map(keyName));
|
|
43
46
|
const unknown = modifiers.find(m => !MODIFIERS.includes(m));
|
|
44
47
|
if (unknown) throw new Error(`Not a modifier: ${unknown}; they are ${MODIFIERS.join(', ')}`);
|
|
45
48
|
const parsed = splitSelector(text);
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
|
|
49
|
+
const positional = parsed && /^(?:left|right|middle)$/.test(parsed.rest || '') && (REF.test(parsed.word) || /^["']/.test(text)) ? parsed.rest : null;
|
|
50
|
+
const button = chooseButton(found, positional, usage);
|
|
51
|
+
const selector = positional ? parsed.selector : soleSelector(text, usage);
|
|
52
|
+
const options = { ...(button ? { button } : {}), ...(modifiers.length ? { modifiers } : {}), ...(clickCount > 1 ? { clickCount } : {}) };
|
|
53
|
+
// The selector as written, then the kind of click, so an option never reads as part of the selector.
|
|
54
|
+
const kind = [
|
|
55
|
+
...(button && button !== 'left' ? [`${button} button`] : []),
|
|
56
|
+
...(clickCount > 1 ? [`${clickCount} times`] : []),
|
|
57
|
+
...(modifiers.length ? [`with ${modifiers.join('+')}`] : []),
|
|
58
|
+
];
|
|
59
|
+
return { selector, shown: [positional ? parsed.word : unquote(text), ...kind].join(', '), options };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// --click-count=<n>, as Playwright's clickCount: 3 selects a line or a field's text. Past 3 a browser
|
|
63
|
+
// does nothing more.
|
|
64
|
+
function clickCountOf(found, usage) {
|
|
65
|
+
const given = found.filter(o => o.name === 'click-count');
|
|
66
|
+
if (!given.length) return 1;
|
|
67
|
+
if (given.length > 1 || !/^[1-3]$/.test(given[0].value || '')) throw new Error(`Usage: ${usage}; --click-count is 1, 2 or 3, once (2 is a double-click, 3 a triple-click)`);
|
|
68
|
+
return Number(given[0].value);
|
|
50
69
|
}
|
|
51
70
|
|
|
52
71
|
// A trailing --submit, as playwright-cli's fill and type take: Enter afterwards.
|
|
@@ -177,9 +196,30 @@ async function chooseMatch(selector, timeout) {
|
|
|
177
196
|
throw error;
|
|
178
197
|
}
|
|
179
198
|
|
|
199
|
+
// The button, from a word (click e5 right) or --button=right, as Playwright's option is named; not both.
|
|
200
|
+
function chooseButton(found, positional, usage) {
|
|
201
|
+
const named = found.filter(o => o.name === 'button');
|
|
202
|
+
if (named.length > 1 || (named.length && !/^(?:left|right|middle)$/.test(named[0].value || ''))) throw new Error(`Usage: ${usage}; --button is left, right or middle, once`);
|
|
203
|
+
if (positional && named.length) throw new Error(`Usage: ${usage}; the button goes once, as a word or as --button, not both`);
|
|
204
|
+
return positional || named[0]?.value || null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// A mouse command's words, its button (a last word, or --button) and its options. Any other --option is
|
|
208
|
+
// refused before a mouse event is sent.
|
|
209
|
+
function mouseArgs(line, usage, spec = {}) {
|
|
210
|
+
const { found, rest } = takeOptions((line || '').trim(), { button: 'value', ...spec });
|
|
211
|
+
const words = rest.split(/\s+/).filter(Boolean);
|
|
212
|
+
const unknown = words.find(w => w.startsWith('--'));
|
|
213
|
+
if (unknown) throw new Error(`Usage: ${usage}; ${unknown.split('=')[0]} is not one of its options`);
|
|
214
|
+
const last = /^[a-z]+$/.test(words[words.length - 1] || '') ? words.pop() : null;
|
|
215
|
+
if (last && !/^(?:left|right|middle)$/.test(last)) throw new Error(`Usage: ${usage}`);
|
|
216
|
+
return { words, found, button: chooseButton(found, last, usage) || 'left' };
|
|
217
|
+
}
|
|
218
|
+
|
|
180
219
|
function mouseButton(args, name) {
|
|
181
|
-
const
|
|
182
|
-
|
|
220
|
+
const usage = `${name} [left|right|middle]`;
|
|
221
|
+
const { words, button } = mouseArgs(args, usage);
|
|
222
|
+
if (words.length) throw new Error(`Usage: ${usage}`);
|
|
183
223
|
return button;
|
|
184
224
|
}
|
|
185
225
|
|
|
@@ -386,17 +426,48 @@ function dateInputHolds(kind, value, what, selector) {
|
|
|
386
426
|
|
|
387
427
|
const commands = {
|
|
388
428
|
async click(args) {
|
|
389
|
-
const { selector, shown, options } = clickArgs(args, 'click <selector> [left|right|middle] [--modifiers=<key>[,<key>]]');
|
|
390
|
-
const which = await pointerAction(selector, (on, sel, o) => (sel ? on.click(sel, o) : on.click(o)), options, 1);
|
|
429
|
+
const { selector, shown, options } = clickArgs(args, 'click <selector> [left|right|middle] [--button=<button>] [--click-count=<n>] [--modifiers=<key>[,<key>]]', true);
|
|
430
|
+
const which = await pointerAction(selector, (on, sel, o) => (sel ? on.click(sel, o) : on.click(o)), options, options.clickCount || 1);
|
|
391
431
|
out.log(`Clicked: ${shown}${which}`);
|
|
392
432
|
},
|
|
393
433
|
|
|
394
434
|
async dblclick(args) {
|
|
395
|
-
const { selector, shown, options } = clickArgs(args, 'dblclick <selector> [left|right|middle] [--modifiers=<key>[,<key>]]');
|
|
435
|
+
const { selector, shown, options } = clickArgs(args, 'dblclick <selector> [left|right|middle] [--button=<button>] [--modifiers=<key>[,<key>]]');
|
|
396
436
|
const which = await pointerAction(selector, (on, sel, o) => (sel ? on.dblclick(sel, o) : on.dblclick(o)), options, 2);
|
|
397
437
|
out.log(`Double-clicked: ${shown}${which}`);
|
|
398
438
|
},
|
|
399
439
|
|
|
440
|
+
// drag <from> <to>, as playwright-cli's drag: Playwright's locator.dragTo, in its steps (hover the
|
|
441
|
+
// source, press, hover the target, release), so the cursor can glide to each in turn. Chromium turns
|
|
442
|
+
// the moves into an HTML5 drag when the source is draggable.
|
|
443
|
+
async drag(args) {
|
|
444
|
+
const usage = 'Usage: drag <from> <to>, each a selector or a ref, quoted if it has spaces: drag e5 e9, drag "text=Item 1" "#list li"';
|
|
445
|
+
const from = splitSelector(args);
|
|
446
|
+
const to = from?.rest !== undefined ? splitSelector(from.rest) : null;
|
|
447
|
+
if (!to || to.rest !== undefined) throw new Error(usage);
|
|
448
|
+
const TIMEOUT = 5000;
|
|
449
|
+
const deadline = Date.now() + TIMEOUT;
|
|
450
|
+
const left = () => Math.max(1000, deadline - Date.now());
|
|
451
|
+
const source = state.page.locator(from.selector).first();
|
|
452
|
+
const target = state.page.locator(to.selector).first();
|
|
453
|
+
await onElement(from.selector, () => aimAt(state.page, source, left()));
|
|
454
|
+
await onElement(from.selector, () => source.hover({ timeout: left() }));
|
|
455
|
+
await state.page.mouse.down();
|
|
456
|
+
// Pressed from here on: a failure would leave the button down, which the page would take as a drag still going.
|
|
457
|
+
try {
|
|
458
|
+
await showClick(state.page);
|
|
459
|
+
await onElement(to.selector, () => aimAt(state.page, target, left()));
|
|
460
|
+
await onElement(to.selector, () => target.hover({ timeout: left() }));
|
|
461
|
+
} catch (error) {
|
|
462
|
+
await state.page.mouse.up().catch(() => {});
|
|
463
|
+
throw error;
|
|
464
|
+
}
|
|
465
|
+
await state.page.mouse.up();
|
|
466
|
+
const box = await target.boundingBox({ timeout: 1000 }).catch(() => null);
|
|
467
|
+
tabState(state.page).mouseAt = box ? { x: Math.round(box.x + box.width / 2), y: Math.round(box.y + box.height / 2) } : null;
|
|
468
|
+
out.log(`Dragged ${from.word} to ${to.word}`);
|
|
469
|
+
},
|
|
470
|
+
|
|
400
471
|
async hover(args) {
|
|
401
472
|
const selector = soleSelector(args, 'hover <selector>');
|
|
402
473
|
const which = await pointerAction(selector, (on, sel, o) => (sel ? on.hover(sel, o) : on.hover(o)));
|
|
@@ -472,23 +543,41 @@ const commands = {
|
|
|
472
543
|
|
|
473
544
|
// A click at a point, as Playwright's mouse.click: for what has no selector, such as a marker drawn on a canvas.
|
|
474
545
|
async mouseclick(line) {
|
|
475
|
-
const usage = 'mouseclick <x> <y> [left|right|middle] [--delay=<ms>]';
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
const
|
|
479
|
-
|
|
546
|
+
const usage = 'mouseclick <x> <y> [left|right|middle] [--button=<button>] [--click-count=<n>] [--delay=<ms>]';
|
|
547
|
+
const { words, found, button } = mouseArgs(line, usage, { 'click-count': 'value', delay: 'value' });
|
|
548
|
+
const clickCount = clickCountOf(found, usage);
|
|
549
|
+
const delay = found.find(o => o.name === 'delay');
|
|
550
|
+
if (delay && !/^\d+$/.test(delay.value || '')) throw new Error(`Usage: ${usage}, the ms between press and release, e.g. mouseclick 400 300 --delay=500`);
|
|
551
|
+
const held = delay ? Number(delay.value) : 0;
|
|
480
552
|
const [x, y, extra] = numbers(words.join(' '), usage);
|
|
481
553
|
if (y === undefined || extra !== undefined) throw new Error(`Usage: ${usage}, in CSS pixels from the top left of the viewport`);
|
|
482
554
|
stepTarget(state.page, null, { x, y });
|
|
483
555
|
await glideTo(state.page, x, y);
|
|
484
556
|
await state.page.mouse.move(x, y);
|
|
485
557
|
tabState(state.page).mouseAt = { x, y };
|
|
486
|
-
//
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
558
|
+
// Each press counts up, as Playwright's mouse.click does it. The ring shows at the first press, as
|
|
559
|
+
// mousedown's does, while a --delay holds the button.
|
|
560
|
+
for (let count = 1; count <= clickCount; count += 1) {
|
|
561
|
+
await state.page.mouse.down({ button, clickCount: count });
|
|
562
|
+
if (count === 1) await showClick(state.page, clickCount);
|
|
563
|
+
if (held) await new Promise(resolve => setTimeout(resolve, held));
|
|
564
|
+
await state.page.mouse.up({ button, clickCount: count });
|
|
565
|
+
}
|
|
566
|
+
out.log(`Clicked the ${button} button at ${x}, ${y}${clickCount > 1 ? ` ${clickCount} times` : ''}`);
|
|
567
|
+
},
|
|
568
|
+
|
|
569
|
+
// Two mouseclicks are two single clicks (each event's detail is 1), so no dblclick fires: this is one.
|
|
570
|
+
async mousedblclick(line) {
|
|
571
|
+
const usage = 'mousedblclick <x> <y> [left|right|middle] [--button=<button>]';
|
|
572
|
+
const { words, button } = mouseArgs(line, usage);
|
|
573
|
+
const [x, y, extra] = numbers(words.join(' '), usage);
|
|
574
|
+
if (y === undefined || extra !== undefined) throw new Error(`Usage: ${usage}, in CSS pixels from the top left of the viewport`);
|
|
575
|
+
stepTarget(state.page, null, { x, y });
|
|
576
|
+
await glideTo(state.page, x, y);
|
|
577
|
+
await state.page.mouse.dblclick(x, y, { button });
|
|
578
|
+
tabState(state.page).mouseAt = { x, y };
|
|
579
|
+
await showClick(state.page, 2);
|
|
580
|
+
out.log(`Double-clicked the ${button} button at ${x}, ${y}`);
|
|
492
581
|
},
|
|
493
582
|
|
|
494
583
|
async mousedown(args) {
|
|
@@ -633,7 +722,7 @@ const commands = {
|
|
|
633
722
|
if (!parsed || parsed.rest === undefined) throw new Error(usage);
|
|
634
723
|
// A path, not a selector; // starts an XPath.
|
|
635
724
|
if (/^(?:\.{1,2}\/|\/(?!\/)|~)/.test(parsed.word) || (!REF.test(parsed.word) && fs.statSync(path.resolve(parsed.word), { throwIfNoEntry: false })?.isFile())) throw new Error(`${parsed.word} is a file, not the input: ${usage.slice(7)}`);
|
|
636
|
-
const files = [...parsed.rest.matchAll(/"((?:[^"\\]|\\.)*)"|'([^']*)'|(\S+)/g)].map(m => path.resolve(m[1] !== undefined ? m[1]
|
|
725
|
+
const files = [...parsed.rest.matchAll(/"((?:[^"\\]|\\.)*)"|'([^']*)'|(\S+)/g)].map(m => path.resolve(m[1] !== undefined ? unescapeDouble(m[1]) : m[2] ?? m[3]));
|
|
637
726
|
const payloads = files.map(file => {
|
|
638
727
|
let buffer;
|
|
639
728
|
try { buffer = fs.readFileSync(file); } catch (error) { throw new Error(`Cannot read ${file}: ${error.code === 'ENOENT' ? 'no such file' : error.code === 'EISDIR' ? 'it is a folder' : error.message}`); }
|
package/lib/emulation.js
CHANGED
|
@@ -20,7 +20,8 @@ async function viewportText() {
|
|
|
20
20
|
const set = tabState(state.page).viewport;
|
|
21
21
|
if (set) return `${set.width}x${set.height} (set with viewport)`;
|
|
22
22
|
const size = await state.page.evaluate(() => `${innerWidth}x${innerHeight}`).catch(() => null);
|
|
23
|
-
|
|
23
|
+
// The window less its frame (headless Chromium counts one too: a 1280x800 window has a 1280x657 page).
|
|
24
|
+
return size ? `${size} (the window's page area)` : 'unknown';
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
const DEFAULT_DEVICE = 'Pixel 7';
|
|
@@ -175,9 +176,10 @@ const commands = {
|
|
|
175
176
|
out.log(`Viewport off: ${await viewportText()}`);
|
|
176
177
|
return;
|
|
177
178
|
}
|
|
178
|
-
const match = /^(\d+)x(\d+)
|
|
179
|
+
const match = /^(\d+)x(\d+)$/i.exec(arg);
|
|
179
180
|
const [w, h] = match ? [Number(match[1]), Number(match[2])] : [];
|
|
180
|
-
if (!
|
|
181
|
+
if (!match) throw new Error(`A size is <width>x<height>, e.g. viewport 1280x900 (got ${arg}). Usage: viewport [<width>x<height> | off]`);
|
|
182
|
+
if (!w || !h) throw new Error(`A width and a height are 1 or more (got ${arg})`);
|
|
181
183
|
// Both set the screen size; whichever came last would win without saying so.
|
|
182
184
|
if (tabState(state.page).emulation?.device) throw new Error('emulate mobile sets the size of the selected tab; emulate mobile off first');
|
|
183
185
|
await setViewport(state.page, { width: w, height: h });
|
package/lib/help.js
CHANGED
|
@@ -22,36 +22,50 @@ function wrap(text, width = WIDTH) {
|
|
|
22
22
|
}).join('\n\n');
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
// The common tasks, as the README's table lists them too (a unit test keeps the two the same).
|
|
26
|
+
const TASKS = [
|
|
27
|
+
['see where I am', 'tab, info'],
|
|
28
|
+
['see what is on the page', 'snapshot, screenshot'],
|
|
29
|
+
['do something on it', 'click, fill, type, press, drag'],
|
|
30
|
+
['click what has no selector (a canvas)', 'mouseclick <x> <y>, mousedblclick <x> <y>'],
|
|
31
|
+
['run some JavaScript in the page', 'eval document.title; eval --file=check.js for a long one'],
|
|
32
|
+
['wait for a page or element', 'wait load, wait <selector> [--gone], wait text <text>'],
|
|
33
|
+
['choose a file in a file input', 'upload <selector> <file>'],
|
|
34
|
+
['see it as a phone, or in dark mode', 'emulate mobile, emulate dark'],
|
|
35
|
+
['see what the page requested', 'requests, then body <#> for what one got back'],
|
|
36
|
+
['see console messages and errors', 'console'],
|
|
37
|
+
['show an agent what I do', 'watch on, click around, then watch'],
|
|
38
|
+
['see each step as I click', 'watch on --live'],
|
|
39
|
+
['watch a tab someone will open', 'watch on --next-tab [url-part]'],
|
|
40
|
+
['replay what I clicked through', 'watch save flow.txt, then pw-repl send --file flow.txt'],
|
|
41
|
+
['record requests and console together', 'capture on, then capture off'],
|
|
42
|
+
['find what makes the page slow', 'profile on, then profile off; profile reload for a load'],
|
|
43
|
+
['fake a backend response', 'route <glob> <status> <json>, route <glob> abort'],
|
|
44
|
+
['change or slow an API response', 'route <glob> patch <json>, route <glob> delay <secs>'],
|
|
45
|
+
['cut or slow the whole network', 'network off, network slow, then network on'],
|
|
46
|
+
['record a video of the page', 'record on, then record off (help video for a take)'],
|
|
47
|
+
['show a pointer where the agent acts', 'cursor on, then cursor off'],
|
|
48
|
+
['say what is happening on screen', 'toast <text>, then toast off'],
|
|
49
|
+
['point something out on the page', 'highlight <ref>, then highlight off'],
|
|
50
|
+
['clean up', 'modes off'],
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
// The commands that turn on a mode, as the README lists them too.
|
|
54
|
+
const MODES = 'watch, capture, profile, record, route, network off or slow, emulate, viewport, highlight, cursor and toast';
|
|
55
|
+
|
|
25
56
|
const OVERVIEW = wrap(`pw-repl drives Chromium over CDP. Commands act on the selected tab (\`tab\` shows which).
|
|
26
57
|
The browser may be a shared session, with other users' tabs open in it.
|
|
27
58
|
|
|
28
59
|
Common tasks:
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
choose a file in a file input upload <selector> <file>
|
|
34
|
-
see it as a phone, or dark emulate mobile, emulate dark
|
|
35
|
-
what did the page request? requests, then body <#> for what one got back
|
|
36
|
-
console messages and errors console
|
|
37
|
-
show an agent what I do watch on, click around, then watch
|
|
38
|
-
see each step as I click watch on --live
|
|
39
|
-
watch a tab someone will open watch on --next-tab [url-part]
|
|
40
|
-
requests and console together capture on, then capture off
|
|
41
|
-
a video of what I do help video
|
|
42
|
-
break the backend on purpose route <glob> <status> <json>, route <glob> abort, network off
|
|
43
|
-
change or slow an API response route <glob> patch <json>, route <glob> delay <secs>
|
|
44
|
-
slow the whole network network slow
|
|
45
|
-
clean up modes off
|
|
46
|
-
|
|
47
|
-
Modes (watch, capture, record, route, network off or slow, emulate, viewport) stay on until turned off;
|
|
48
|
-
the prompt shows the selected tab's: (watch routes:1) pw>. tab, watch, capture, record, route, network,
|
|
49
|
-
emulate and modes on their own show their state and what you can run next.
|
|
60
|
+
${TASKS.map(([task, commands]) => ` ${task.padEnd(38)} ${commands}`).join('\n')}
|
|
61
|
+
|
|
62
|
+
Modes (${MODES}) stay on until turned off; the prompt shows the selected tab's: (watch routes:1) pw>.
|
|
63
|
+
Each of them on its own, and modes, shows its state and what you can run next.
|
|
50
64
|
|
|
51
65
|
Topics (help <topic>):
|
|
52
66
|
tabs open, select, close, navigate network requests, bodies, console, fakes, network off
|
|
53
67
|
interact click, fill, type, press, select devtools eval, CDP commands, cookie and storage names
|
|
54
|
-
inspect snapshot, watch,
|
|
68
|
+
inspect snapshot, watch, wait, screenshots session modes, server, clients, quitting
|
|
55
69
|
video record a take, with a pointer
|
|
56
70
|
|
|
57
71
|
help <command> for usage and caveats (e.g. help route), help <command> --all for all of it; help --all
|
|
@@ -71,7 +85,7 @@ tab on its own lists them all.`,
|
|
|
71
85
|
Commands use the first match (click, dblclick and hover the first they can use: help click); see help
|
|
72
86
|
fill for selectors with spaces. mousemove and the other mouse commands act at a point instead. An action
|
|
73
87
|
scrolls its element into view first, as Playwright does; profile counts that scroll in the action's step.`,
|
|
74
|
-
commands: ['click', 'dblclick', 'hover', 'fill', 'type', 'press', 'select', 'check', 'uncheck', 'upload', 'mousemove', 'mouseclick', 'mousedown', 'mouseup', 'mousewheel'],
|
|
88
|
+
commands: ['click', 'dblclick', 'hover', 'drag', 'fill', 'type', 'press', 'select', 'check', 'uncheck', 'upload', 'mousemove', 'mouseclick', 'mousedblclick', 'mousedown', 'mouseup', 'mousewheel'],
|
|
75
89
|
},
|
|
76
90
|
inspect: {
|
|
77
91
|
intro: `Output is capped; --all right after the command, or at the end, shows everything (text --all body).`,
|
|
@@ -124,12 +138,7 @@ still show sensitive data (cdp Network.getCookies shows cookie values).`,
|
|
|
124
138
|
commands: ['eval', 'cdp', 'cookies', 'storage'],
|
|
125
139
|
},
|
|
126
140
|
session: {
|
|
127
|
-
intro: `
|
|
128
|
-
|
|
129
|
-
Unknown outcome: a command that timed out after it began acting on the page may or may not have done
|
|
130
|
-
it. It says so (pw-repl send exits 2), and the REPL carries on.
|
|
131
|
-
|
|
132
|
-
Server: pw-repl serve also takes commands on /tmp/pw-repl.sock; they show here as [server] lines.
|
|
141
|
+
intro: `Server: pw-repl serve also takes commands on /tmp/pw-repl.sock; they show here as [server] lines.
|
|
133
142
|
|
|
134
143
|
Clients: pw-repl send -c <name> (or $PW_CLIENT) sends as a client of its own. It has a selected tab of
|
|
135
144
|
its own, so clients do not move each other's; its commands show as [server:<name>] lines. modes and tab
|
|
@@ -143,7 +152,12 @@ selected tab, if any: (watch network:off routes:2 capture) pw>. modes lists them
|
|
|
143
152
|
|
|
144
153
|
Times (requests, console, watch) are local, with their UTC offset: 18:16:15.721-06:00.
|
|
145
154
|
|
|
146
|
-
Dialogs are reported and never answered on their own; dialog accept or dismiss answers one, from any client, whoever opened it
|
|
155
|
+
Dialogs are reported and never answered on their own; dialog accept or dismiss answers one, from any client, whoever opened it.
|
|
156
|
+
|
|
157
|
+
Unknown outcome: a command that timed out after it began acting on the page may or may not have done
|
|
158
|
+
it. It says so (pw-repl send exits 2), and the REPL carries on.
|
|
159
|
+
|
|
160
|
+
Completion: @<id> <command> makes the REPL print [[pw-done:<id>:ok|error]] when it finishes.`,
|
|
147
161
|
commands: ['modes', 'dialog', 'help', 'quit'],
|
|
148
162
|
},
|
|
149
163
|
};
|
|
@@ -197,6 +211,8 @@ afresh.`,
|
|
|
197
211
|
['[button]', 'left, right or middle, after a ref or a quoted selector: click e5 right. Default: left.'],
|
|
198
212
|
],
|
|
199
213
|
options: [
|
|
214
|
+
['--button=<button>', 'the button, after any selector: click #menu --button=right. Over send, the shell takes the quotes, so pw-repl send click \'#menu\' right clicks a selector "#menu right": use --button, or quote it twice (\'"#menu"\').'],
|
|
215
|
+
['--click-count=<n>', 'clicks this many times at once, as Playwright\'s clickCount: 2 is a double-click (as dblclick), 3 a triple-click, which selects a line of text or all of a field\'s. 1 to 3. Default: 1.'],
|
|
200
216
|
['--modifiers=<keys>', 'keys held down meanwhile, comma-separated: Alt, Control, ControlOrMeta, Meta, Shift.'],
|
|
201
217
|
],
|
|
202
218
|
detail: `Several matches: hidden ones and ones covered by something else (a dialog's overlay) are passed over,
|
|
@@ -208,8 +224,23 @@ it to be ready.
|
|
|
208
224
|
Timed out after it began acting, it says what stopped it (e.g. <div class="overlay"> intercepts pointer
|
|
209
225
|
events) and that the outcome is unknown; Playwright's full call log goes to the REPL's pane or log.`,
|
|
210
226
|
},
|
|
211
|
-
dblclick: {
|
|
227
|
+
dblclick: {
|
|
228
|
+
usage: 'dblclick <selector> [button]',
|
|
229
|
+
summary: 'double-click, as click does, with its options',
|
|
230
|
+
detail: `click --click-count=2, as Playwright's dblclick is. For a triple-click: click --click-count=3.`,
|
|
231
|
+
},
|
|
212
232
|
hover: { usage: 'hover <selector>', summary: 'hover, choosing the match as click does' },
|
|
233
|
+
drag: {
|
|
234
|
+
usage: 'drag <from> <to>',
|
|
235
|
+
summary: 'drag one element onto another',
|
|
236
|
+
args: [
|
|
237
|
+
['<from> <to>', 'selectors or snapshot refs, each quoted if it has spaces: drag e5 e9, drag "text=Item 1" "#done ul".'],
|
|
238
|
+
],
|
|
239
|
+
detail: `As Playwright's dragTo: it hovers the first match of <from>, presses the left button, hovers the
|
|
240
|
+
first match of <to>, and releases, waiting up to 5s in all for each to be ready. An HTML5 drag (a
|
|
241
|
+
draggable element) gets dragstart, dragover and drop; any other gets pointer and mouse events. A drag
|
|
242
|
+
through points of its own: mousemove, mousedown, mousemove, mouseup.`,
|
|
243
|
+
},
|
|
213
244
|
mousemove: {
|
|
214
245
|
usage: 'mousemove <x> <y>',
|
|
215
246
|
summary: 'move the mouse to a point in the viewport',
|
|
@@ -220,7 +251,7 @@ events) and that the outcome is unknown; Playwright's full call log goes to the
|
|
|
220
251
|
eval (r => [r.x + r.width / 2, r.y + r.height / 2])(document.querySelector("#map")
|
|
221
252
|
.getBoundingClientRect())
|
|
222
253
|
|
|
223
|
-
A click at a point: mouseclick <x> <y>. A drag: mousedown, then mousemove to another point, then mouseup.`,
|
|
254
|
+
A click at a point: mouseclick <x> <y>; a double-click: mousedblclick <x> <y>. A drag: mousedown, then mousemove to another point, then mouseup.`,
|
|
224
255
|
},
|
|
225
256
|
mouseclick: {
|
|
226
257
|
usage: 'mouseclick <x> <y> [button]',
|
|
@@ -230,14 +261,30 @@ A click at a point: mouseclick <x> <y>. A drag: mousedown, then mousemove to ano
|
|
|
230
261
|
['[button]', 'left, right or middle. Default: left.'],
|
|
231
262
|
],
|
|
232
263
|
options: [
|
|
264
|
+
['--button=<button>', 'the button, as click\'s: mouseclick 400 300 --button=right.'],
|
|
265
|
+
['--click-count=<n>', 'as click\'s: 2 is a double-click (as mousedblclick), 3 a triple-click. 1 to 3. Default: 1.'],
|
|
233
266
|
['--delay=<ms>', 'holds the button down this long between press and release, as Playwright\'s mouse.click delay. Default: 0.'],
|
|
234
267
|
],
|
|
235
268
|
detail: `For what has no selector, such as a marker drawn on a canvas; anything a selector finds is better
|
|
236
269
|
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
|
|
237
270
|
it. The cursor (help cursor) moves there first and shows a ring at the press.`,
|
|
238
271
|
},
|
|
239
|
-
|
|
240
|
-
|
|
272
|
+
mousedblclick: {
|
|
273
|
+
usage: 'mousedblclick <x> <y> [button]',
|
|
274
|
+
summary: 'double-click at a point in the viewport',
|
|
275
|
+
args: [
|
|
276
|
+
['<x> <y>', 'CSS pixels from the top left of the tab\'s viewport, as for mousemove.'],
|
|
277
|
+
['[button]', 'left, right or middle. Default: left.'],
|
|
278
|
+
],
|
|
279
|
+
options: [
|
|
280
|
+
['--button=<button>', 'the button, as click\'s.'],
|
|
281
|
+
],
|
|
282
|
+
detail: `mouseclick --click-count=2, as Playwright's mouse.dblclick is: the page gets two clicks and a
|
|
283
|
+
dblclick. Two mouseclicks are two single clicks, which fire no dblclick. For a triple-click: mouseclick
|
|
284
|
+
<x> <y> --click-count=3.`,
|
|
285
|
+
},
|
|
286
|
+
mousedown: { usage: 'mousedown [button]', summary: 'press a mouse button at the mouse (left, right, middle)', detail: 'The button is a word or --button=<button>, as for mouseclick.' },
|
|
287
|
+
mouseup: { usage: 'mouseup [button]', summary: 'release a mouse button where the mouse is', detail: 'The button is a word or --button=<button>, as for mouseclick.' },
|
|
241
288
|
mousewheel: {
|
|
242
289
|
usage: 'mousewheel <dx> <dy>',
|
|
243
290
|
summary: 'turn the wheel where the mouse is',
|
|
@@ -259,14 +306,19 @@ scrolled or redrawn. While recording, it scrolls in eased steps over up to 1s (h
|
|
|
259
306
|
['--submit', 'at the end: presses Enter in it afterwards.'],
|
|
260
307
|
],
|
|
261
308
|
detail: `Examples: fill #name Ada Lovelace, fill "text=Your name" Ada, fill e7 Ada. It sets the whole value at
|
|
262
|
-
once, even while recording: type shows it being typed
|
|
309
|
+
once, even while recording: type shows it being typed.
|
|
310
|
+
|
|
311
|
+
At the prompt and in a send --file line, \\n, \\t and \\r in double quotes are a newline, a tab and a
|
|
312
|
+
carriage return (fill #notes "line 1\\nline 2"), and \\\\ is a backslash. Over send, the shell's quoting comes
|
|
313
|
+
first, and its "a\\nb" is a literal \\n, typed as a backslash and an n: give it a real newline, as
|
|
314
|
+
$'line 1\\nline 2'.`,
|
|
263
315
|
},
|
|
264
316
|
type: {
|
|
265
317
|
usage: 'type [--delay=<ms>] [<selector>] <text> [--submit]',
|
|
266
318
|
summary: 'type key by key after what the field holds',
|
|
267
319
|
args: [
|
|
268
320
|
['[<selector>]', 'as for fill. Without it (one word, or one quoted), into what has focus: type "garden hose".'],
|
|
269
|
-
['<text>', 'the rest of the line, as fill\'s value.'],
|
|
321
|
+
['<text>', 'the rest of the line, as fill\'s value: a newline in it presses Enter, which in a textarea starts a new line (help fill says how to give one).'],
|
|
270
322
|
],
|
|
271
323
|
options: [
|
|
272
324
|
['--delay=<ms>', 'right after type: the gap between keys. Default: none, or 60 while the tab records.'],
|
|
@@ -324,6 +376,9 @@ all.`,
|
|
|
324
376
|
to the tab whose snapshot gave it, and can change when the page changes (e102 may become f4e98): take a
|
|
325
377
|
new snapshot after it does.
|
|
326
378
|
|
|
379
|
+
Quotes: some names and text are quoted, YAML-style, as one with a quote mark in it is, and their quotes
|
|
380
|
+
are escaped: paragraph: "Say \\"hi\\"" is the text Say "hi" on the page, with no backslashes.
|
|
381
|
+
|
|
327
382
|
--grep: a hit with no named element around it prints without a path. A hit on text (a label) keeps the
|
|
328
383
|
ref of the element holding it, so snapshot <that ref> shows what sits beside it, such as the value next
|
|
329
384
|
to the label.
|
|
@@ -381,7 +436,14 @@ one no selector finds alone) stays a comment saying why, and the save says how m
|
|
|
381
436
|
ends with the last step, not with what it brought: to end on a result that comes later (search results
|
|
382
437
|
loading), add a wait for it at the end (wait text ...).
|
|
383
438
|
|
|
384
|
-
|
|
439
|
+
Watching someone: read a response body over 100 KB that matters as soon as it shows in requests, since
|
|
440
|
+
the browser drops it once the tab navigates. watch leaves out images and other static files, failed ones
|
|
441
|
+
too; console error and requests --all show what failed. Without a url-part, --next-tab takes the first
|
|
442
|
+
tab anyone opens: check tab and info once it fires, since another tab opening later does not show in
|
|
443
|
+
watch.
|
|
444
|
+
|
|
445
|
+
Shared: every client sees the same steps, and watch new's place is shared: when another client watches
|
|
446
|
+
the tab too, read it with watch <n>, since each one's watch new would hide steps from the other.`,
|
|
385
447
|
moreAbout: 'bodies kept, --live timing, what --changes misses, one --next-tab at a time, and what watch save leaves out',
|
|
386
448
|
more: `From its first watch on, a tab keeps response bodies of up to 100 KB, so body <#> still shows one
|
|
387
449
|
after the tab navigates, even when the page navigates as soon as the request answers.
|
|
@@ -526,9 +588,9 @@ shows in screenshots and recordings. Selectors, snapshot and text don't see it,
|
|
|
526
588
|
],
|
|
527
589
|
detail: `The REPL's clicks and typing don't move a pointer on screen, so a recording shows things happening
|
|
528
590
|
with nothing pointing at them. With the cursor on, these commands move it smoothly to their element or
|
|
529
|
-
point before they act: click, dblclick, hover, fill, type <selector>, press <selector>, select, check,
|
|
530
|
-
uncheck, upload, mousemove and
|
|
531
|
-
records), and the command waits for it. Clicks (click, dblclick, check, uncheck, mouseclick, mousedown, and upload
|
|
591
|
+
point before they act: click, dblclick, hover, drag (to each end), fill, type <selector>, press <selector>, select, check,
|
|
592
|
+
uncheck, upload, mousemove, mouseclick and mousedblclick. Each move takes 150 to 600ms by distance (300 to 1000ms while the tab
|
|
593
|
+
records), and the command waits for it. Clicks (click, dblclick, check, uncheck, mouseclick, mousedblclick, mousedown, and upload
|
|
532
594
|
when it clicks a button) show a ring where they land.
|
|
533
595
|
|
|
534
596
|
Only the drawing moves: no mouse events are sent along the way, so the page gets the same input as with
|
|
@@ -607,10 +669,16 @@ goes on recording. There is no --mp4 or --webm: the file's ending picks the form
|
|
|
607
669
|
summary: 'show, set or unset the viewport size',
|
|
608
670
|
args: [
|
|
609
671
|
['<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.'],
|
|
610
|
-
['off', 'back to the window\'s
|
|
611
|
-
['(none)', 'shows the size, and whether it was set or is the window\'s.'],
|
|
672
|
+
['off', 'back to the window\'s page area.'],
|
|
673
|
+
['(none)', 'shows the size, and whether it was set or is the window\'s page area.'],
|
|
612
674
|
],
|
|
613
|
-
detail: `emulate mobile sets a phone's size instead, and each refuses while the other is on
|
|
675
|
+
detail: `emulate mobile sets a phone's size instead, and each refuses while the other is on.
|
|
676
|
+
|
|
677
|
+
The window's page area is the page's size without a viewport set. Chromium's --window-size flag sizes the whole
|
|
678
|
+
window, frame included, so the page is smaller: --window-size=1280,900 gives 1280x757 in a headless one.
|
|
679
|
+
--launch starts its Chromium at --window-size=1280,800 (a page of 1280x657 headless), unless the flags
|
|
680
|
+
after -- give one; a profile it was given and has used before may keep its own last size.
|
|
681
|
+
For a page of an exact size, use viewport <W>x<H>.`,
|
|
614
682
|
},
|
|
615
683
|
emulate: {
|
|
616
684
|
usage: 'emulate [<what> [off] | off]',
|
|
@@ -828,9 +896,10 @@ in front of it usually holds the request open, so the page spins instead of fail
|
|
|
828
896
|
summary: 'evaluate JavaScript in the selected tab and print the result',
|
|
829
897
|
args: [
|
|
830
898
|
['<JavaScript>', 'the rest of the line. Promises are awaited, await works at the top level, and a function (() => document.title) is called.'],
|
|
831
|
-
['<function> <ref>', 'calls it with that element: eval "el => el.textContent" e5.'],
|
|
899
|
+
['<function> <ref>', 'calls it with that element: eval "el => el.textContent" e5. Quoted, its \\n is a newline and its \\" a quote: write \\\\n for a JS string\'s \\n.'],
|
|
832
900
|
],
|
|
833
901
|
options: [
|
|
902
|
+
['--file=<path>', 'the JavaScript is the file\'s text: anything the line takes, over many lines and with // comments. With a ref or a selector after the path, it is a function called with that element (the first match). Over send, a relative path is from your shell\'s current folder.'],
|
|
834
903
|
['--all', 'the whole result, not capped.'],
|
|
835
904
|
],
|
|
836
905
|
detail: `eval fetch(...) sends a new request, which can change state on the server; it cannot read one that
|
|
@@ -986,9 +1055,11 @@ Different on purpose, since the browser may be someone else's too:
|
|
|
986
1055
|
if (Object.hasOwn(TOPICS, topicOrCommand)) {
|
|
987
1056
|
const topic = TOPICS[topicOrCommand];
|
|
988
1057
|
const text = [topic.intro, '', ...commandLines(topic.commands)].join('\n');
|
|
989
|
-
// A topic can share its name with a command (network)
|
|
990
|
-
return Object.hasOwn(COMMANDS, topicOrCommand) ? `${text}\n\n${
|
|
1058
|
+
// A topic can share its name with a command (network): the topic, and where the command's help is.
|
|
1059
|
+
return Object.hasOwn(COMMANDS, topicOrCommand) ? `${text}\n\n${topicOrCommand} is also a command: help ${topicOrCommand} --command` : text;
|
|
991
1060
|
}
|
|
1061
|
+
const asCommand = /^(\S+)\s+--command$/.exec(topicOrCommand);
|
|
1062
|
+
if (asCommand && Object.hasOwn(COMMANDS, asCommand[1])) return renderCommand(asCommand[1]);
|
|
992
1063
|
if (Object.hasOwn(COMMANDS, topicOrCommand)) return renderCommand(topicOrCommand);
|
|
993
1064
|
// Not a topic: the mouse commands are in interact, with the others that act on the page.
|
|
994
1065
|
if (topicOrCommand === 'mouse') {
|
|
@@ -1016,4 +1087,4 @@ function notFound(topic) {
|
|
|
1016
1087
|
return `No help for ${topic}. Topics: ${Object.keys(TOPICS).join(', ')}`;
|
|
1017
1088
|
}
|
|
1018
1089
|
|
|
1019
|
-
module.exports = { render, notFound, TOPICS, COMMANDS };
|
|
1090
|
+
module.exports = { render, notFound, TOPICS, COMMANDS, TASKS, MODES };
|
package/lib/inspect.js
CHANGED
|
@@ -442,7 +442,12 @@ const commands = {
|
|
|
442
442
|
const onlyWhat = (tokens[0] === 'text' || tokens[0] === 'request') && tokens.length === 2;
|
|
443
443
|
const tookSeconds = tokens.length > 1 && !onlyWhat && /^\d+$/.test(tokens[tokens.length - 1]);
|
|
444
444
|
if (tookSeconds) seconds = Number(tokens.pop());
|
|
445
|
-
if (!tokens.length
|
|
445
|
+
if (!tokens.length) throw new Error(`${usage} (1-${WAIT_MAX})`);
|
|
446
|
+
if (seconds < 1 || seconds > WAIT_MAX) {
|
|
447
|
+
// Most likely text that ends in a number, not seconds at all.
|
|
448
|
+
const text = tokens[0] === 'text' && !/^["']/.test(tokens[1]) ? `; for text that ends in a number, quote it: wait text "${tokens.slice(1).join(' ')} ${seconds}"` : '';
|
|
449
|
+
throw new Error(`wait takes 1-${WAIT_MAX} seconds (got ${seconds})${text}`);
|
|
450
|
+
}
|
|
446
451
|
const timeout = seconds * 1000;
|
|
447
452
|
// Said plainly, not as Playwright's call log.
|
|
448
453
|
const plainly = message => error => { throw /Timeout \d+ms exceeded/.test(error.message) ? new Error(message) : error; };
|
|
@@ -479,15 +484,32 @@ const commands = {
|
|
|
479
484
|
|
|
480
485
|
async eval(args, all) {
|
|
481
486
|
if (!args) throw new Error('Usage: eval <js-expression>');
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
487
|
+
let ref = null;
|
|
488
|
+
// eval --file=<path> [<ref or selector>]: the file's text is what the line would be, over as many lines as
|
|
489
|
+
// it likes. The rest of the line can only be the element, so any selector goes there, not only a ref.
|
|
490
|
+
const fromFile = /^--file(?:=|\s+)("(?:[^"\\]|\\.)*"|'[^']*'|\S+)(?:\s+([\s\S]*))?$/.exec(args.trim());
|
|
491
|
+
if (/^--file(?:[=\s]|$)/.test(args.trim())) {
|
|
492
|
+
if (!fromFile) throw new Error('Usage: eval --file=<path> [<ref or selector>]: the JavaScript is in the file');
|
|
493
|
+
const file = path.resolve(unquote(fromFile[1]));
|
|
494
|
+
const target = fromFile[2]?.trim() || null;
|
|
495
|
+
try { args = fs.readFileSync(file, 'utf8').trim(); } catch (error) {
|
|
496
|
+
// eval --file=my dir/x.js reads "my" as the file and "dir/x.js" as the element.
|
|
497
|
+
throw new Error(`eval --file: cannot read ${file} (${error.code || error.message})${target ? '; quote a path with spaces' : ''}`);
|
|
498
|
+
}
|
|
499
|
+
if (!args) throw new Error(`eval --file: ${file} is empty`);
|
|
500
|
+
ref = target && unquote(target);
|
|
501
|
+
} else {
|
|
502
|
+
// eval <function> <ref> calls the function with that element, as playwright-cli's eval does.
|
|
503
|
+
const onRef = /^([\s\S]+?)\s+((?:f\d+)?e\d+)$/.exec(args.trim());
|
|
504
|
+
if (onRef && isFunctionText(unquote(onRef[1]))) [args, ref] = [unquote(onRef[1]), onRef[2]];
|
|
505
|
+
}
|
|
506
|
+
if (ref) {
|
|
507
|
+
const selector = toSelector(ref);
|
|
486
508
|
const el = await onElement(selector, () => state.page.locator(selector).first().elementHandle({ timeout: 5000 }));
|
|
487
509
|
try {
|
|
488
510
|
// Made from its text by the debugger, as eval is, so a page's CSP cannot
|
|
489
511
|
// refuse it, and in the element's own frame, which may be an iframe.
|
|
490
|
-
const fn = await evaluated((await el.ownerFrame()).evaluateHandle(
|
|
512
|
+
const fn = await evaluated((await el.ownerFrame()).evaluateHandle(args));
|
|
491
513
|
try {
|
|
492
514
|
printOutput(await el.evaluate((e, f) => f(e), fn), all);
|
|
493
515
|
} finally {
|
package/lib/launch.js
CHANGED
|
@@ -139,7 +139,9 @@ async function launch({ headed = false, extraArgs = [] }) {
|
|
|
139
139
|
const onExit = () => { if (group) killGroup(group); try { removeProfile(); } catch {} };
|
|
140
140
|
process.on('exit', onExit);
|
|
141
141
|
const onSpawn = proc => { group = proc.pid; };
|
|
142
|
-
|
|
142
|
+
// A laptop's window, rather than Chromium's headless 800x600 or so, unless the flags after -- size it.
|
|
143
|
+
const sized = extraArgs.some(a => a.startsWith('--window-size=')) ? [] : ['--window-size=1280,800'];
|
|
144
|
+
const ours = [...(headed ? [] : ['--headless=new']), ...sized, '--remote-debugging-port=0', ...(given ? [] : [`--user-data-dir=${profile}`]),
|
|
143
145
|
'--no-first-run', '--no-default-browser-check', ...(smallShm() ? ['--disable-dev-shm-usage'] : [])];
|
|
144
146
|
// A profile already used has tabs to restore; a blank tab of its own each time would pile up there.
|
|
145
147
|
// Local State is written at its root on the first run, whichever --profile-directory is used.
|
package/lib/record.js
CHANGED
|
@@ -391,7 +391,7 @@ function videoTime(rec) {
|
|
|
391
391
|
// What a step is, for its line: the command and what it acts on, never what it types or chooses, the
|
|
392
392
|
// code it runs or the URL it goes to, which can hold a password or a token. A steps file goes where the
|
|
393
393
|
// video goes, and the video shows a password as dots.
|
|
394
|
-
const WHOLE = new Set(['click', 'dblclick', 'hover', 'check', 'uncheck', 'mousemove', 'mouseclick', 'mousedown', 'mouseup', 'mousewheel']);
|
|
394
|
+
const WHOLE = new Set(['click', 'dblclick', 'hover', 'drag', 'check', 'uncheck', 'mousemove', 'mouseclick', 'mousedblclick', 'mousedown', 'mouseup', 'mousewheel']);
|
|
395
395
|
const TARGET_FIRST = new Set(['fill', 'type', 'select', 'press', 'upload']);
|
|
396
396
|
function stepLabel(cmd, args) {
|
|
397
397
|
if (WHOLE.has(cmd)) {
|