pw-repl 0.15.0 → 0.15.1
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/elements.js +57 -4
- package/lib/emulation.js +20 -1
- package/lib/help.js +20 -8
- package/lib/inspect.js +83 -62
- package/lib/send.js +6 -3
- package/lib/touch.js +4 -0
- package/package.json +1 -1
- package/skill/SKILL.md +9 -8
package/lib/elements.js
CHANGED
|
@@ -9,7 +9,7 @@ const { numbers, hints, BUSY_PAGE } = require('./util');
|
|
|
9
9
|
const { tabState } = require('./tabstate');
|
|
10
10
|
const { cursorOf, aiming, aimAt, glideTo, showClick, showSelect } = require('./cursor');
|
|
11
11
|
const { recordingOf, stepTarget, pauseToShow } = require('./record');
|
|
12
|
-
const { settled } = require('./emulation');
|
|
12
|
+
const { settled, outsideViewport, IN_VIEW } = require('./emulation');
|
|
13
13
|
|
|
14
14
|
// The type a page sees for a file it is given, from its extension.
|
|
15
15
|
const MIME_TYPES = {
|
|
@@ -439,7 +439,54 @@ function dateInputHolds(kind, value, what, selector) {
|
|
|
439
439
|
return `, a ${kind} input, which now holds ${value}`;
|
|
440
440
|
}
|
|
441
441
|
|
|
442
|
+
// drag <x1> <y1> <x2> <y2>: for what has no element to drag by, such as a canvas or a map's pan. Moved in
|
|
443
|
+
// steps, as a page that tracks the pointer needs moves between the two points, not one jump.
|
|
444
|
+
const DRAG_STEPS = 10;
|
|
445
|
+
|
|
446
|
+
async function dragBetween(x1, y1, x2, y2) {
|
|
447
|
+
await refuseOutside(x1, y1, 'dragged');
|
|
448
|
+
stepTarget(state.page, null, { x: x1, y: y1 });
|
|
449
|
+
await glideTo(state.page, x1, y1);
|
|
450
|
+
await state.page.mouse.move(x1, y1);
|
|
451
|
+
await state.page.mouse.down();
|
|
452
|
+
tabState(state.page).mouseAt = { x: x1, y: y1 };
|
|
453
|
+
// Pressed from here on: a failure would leave the button down, which the page would take as a drag still going.
|
|
454
|
+
try {
|
|
455
|
+
await showClick(state.page);
|
|
456
|
+
await glideTo(state.page, x2, y2);
|
|
457
|
+
await state.page.mouse.move(x2, y2, { steps: DRAG_STEPS });
|
|
458
|
+
} catch (error) {
|
|
459
|
+
await state.page.mouse.up().catch(() => {});
|
|
460
|
+
throw error;
|
|
461
|
+
}
|
|
462
|
+
await state.page.mouse.up();
|
|
463
|
+
tabState(state.page).mouseAt = { x: x2, y: y2 };
|
|
464
|
+
out.log(`Dragged from ${x1}, ${y1} to ${x2}, ${y2}${await outsideNote(x2, y2)}`);
|
|
465
|
+
}
|
|
466
|
+
|
|
442
467
|
// mouse's actions.
|
|
468
|
+
// Playwright's mouse starts at the top left.
|
|
469
|
+
const mouseAt = () => tabState(state.page).mouseAt || { x: 0, y: 0 };
|
|
470
|
+
|
|
471
|
+
// A press outside the viewport reaches nothing, so it is refused rather than reported done.
|
|
472
|
+
async function refuseOutside(x, y, nothing) {
|
|
473
|
+
const outside = await outsideViewport(x, y);
|
|
474
|
+
if (outside) throw new Error(`${x}, ${y} is outside the viewport (${outside}); nothing was ${nothing}. ${IN_VIEW}`);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// mouse down and wheel act where the mouse is.
|
|
478
|
+
async function refuseMouseOutside(nothing) {
|
|
479
|
+
const { x, y } = mouseAt();
|
|
480
|
+
const outside = await outsideViewport(x, y);
|
|
481
|
+
if (outside) throw new Error(`The mouse is at ${x}, ${y}, outside the viewport (${outside}); nothing was ${nothing}. mouse move to a point inside it first, or scroll what you want into view`);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// A move or a release may go outside it, as a drag the page holds on to can.
|
|
485
|
+
async function outsideNote(x, y) {
|
|
486
|
+
const outside = await outsideViewport(x, y);
|
|
487
|
+
return outside ? ` (outside the viewport, ${outside})` : '';
|
|
488
|
+
}
|
|
489
|
+
|
|
443
490
|
const MOUSE = {
|
|
444
491
|
// At a point, in CSS pixels from the top left of the tab's viewport, as playwright-cli's are.
|
|
445
492
|
async move(args) {
|
|
@@ -449,7 +496,7 @@ const MOUSE = {
|
|
|
449
496
|
await glideTo(state.page, x, y);
|
|
450
497
|
await state.page.mouse.move(x, y);
|
|
451
498
|
tabState(state.page).mouseAt = { x, y };
|
|
452
|
-
out.log(`Moved the mouse to ${x}, ${y}`);
|
|
499
|
+
out.log(`Moved the mouse to ${x}, ${y}${await outsideNote(x, y)}`);
|
|
453
500
|
},
|
|
454
501
|
|
|
455
502
|
// A click at a point, as Playwright's mouse.click: for what has no selector, such as a marker drawn on a canvas.
|
|
@@ -462,6 +509,7 @@ const MOUSE = {
|
|
|
462
509
|
const held = delay ? Number(delay.value) : 0;
|
|
463
510
|
const [x, y, extra] = numbers(words.join(' '), usage);
|
|
464
511
|
if (y === undefined || extra !== undefined) throw new Error(`Usage: ${usage}, in CSS pixels from the top left of the viewport`);
|
|
512
|
+
await refuseOutside(x, y, 'clicked');
|
|
465
513
|
stepTarget(state.page, null, { x, y });
|
|
466
514
|
await glideTo(state.page, x, y);
|
|
467
515
|
await state.page.mouse.move(x, y);
|
|
@@ -483,6 +531,7 @@ const MOUSE = {
|
|
|
483
531
|
const { words, button } = mouseArgs(line, usage);
|
|
484
532
|
const [x, y, extra] = numbers(words.join(' '), usage);
|
|
485
533
|
if (y === undefined || extra !== undefined) throw new Error(`Usage: ${usage}, in CSS pixels from the top left of the viewport`);
|
|
534
|
+
await refuseOutside(x, y, 'clicked');
|
|
486
535
|
stepTarget(state.page, null, { x, y });
|
|
487
536
|
await glideTo(state.page, x, y);
|
|
488
537
|
await state.page.mouse.dblclick(x, y, { button });
|
|
@@ -493,6 +542,7 @@ const MOUSE = {
|
|
|
493
542
|
|
|
494
543
|
async down(args) {
|
|
495
544
|
const button = mouseButton(args, 'mouse down');
|
|
545
|
+
await refuseMouseOutside('pressed');
|
|
496
546
|
await glideToMouse();
|
|
497
547
|
await state.page.mouse.down({ button });
|
|
498
548
|
await showClick(state.page);
|
|
@@ -503,13 +553,14 @@ const MOUSE = {
|
|
|
503
553
|
const button = mouseButton(args, 'mouse up');
|
|
504
554
|
await glideToMouse();
|
|
505
555
|
await state.page.mouse.up({ button });
|
|
506
|
-
out.log(`Released the ${button} button`);
|
|
556
|
+
out.log(`Released the ${button} button${await outsideNote(mouseAt().x, mouseAt().y)}`);
|
|
507
557
|
},
|
|
508
558
|
|
|
509
559
|
// Wheel events where the mouse is (mouse move puts it there): a map zooms around that point.
|
|
510
560
|
async wheel(args) {
|
|
511
561
|
const [dx, dy, extra] = numbers(args, 'mouse wheel <dx> <dy>');
|
|
512
562
|
if (dy === undefined || extra !== undefined) throw new Error('Usage: mouse wheel <dx> <dy>, e.g. mouse wheel 0 300 scrolls down (negative dy up)');
|
|
563
|
+
await refuseMouseOutside('scrolled');
|
|
513
564
|
await glideToMouse();
|
|
514
565
|
if (recordingOf(state.page)) await smoothWheel(dx, dy);
|
|
515
566
|
else await state.page.mouse.wheel(dx, dy);
|
|
@@ -534,7 +585,9 @@ const commands = {
|
|
|
534
585
|
// source, press, hover the target, release), so the cursor can glide to each in turn. Chromium turns
|
|
535
586
|
// the moves into an HTML5 drag when the source is draggable.
|
|
536
587
|
async drag(args) {
|
|
537
|
-
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"';
|
|
588
|
+
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"; or drag <x1> <y1> <x2> <y2>, points in the viewport';
|
|
589
|
+
const words = (args || '').trim().split(/\s+/);
|
|
590
|
+
if (words.length === 4 && words.every(w => /^-?\d+(?:\.\d+)?$/.test(w))) return dragBetween(...words.map(Number));
|
|
538
591
|
const from = splitSelector(args);
|
|
539
592
|
const to = from?.rest !== undefined ? splitSelector(from.rest) : null;
|
|
540
593
|
if (!to || to.rest !== undefined) throw new Error(usage);
|
package/lib/emulation.js
CHANGED
|
@@ -24,6 +24,25 @@ async function viewportText() {
|
|
|
24
24
|
return size ? `${size} (the window's page area)` : 'unknown';
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
// The viewport's size, in the CSS pixels mouse and touch points are given in: a phone's, a size set
|
|
28
|
+
// with viewport, or else the page's (a tab attached over CDP has none set, so Playwright knows none).
|
|
29
|
+
async function viewportSize() {
|
|
30
|
+
const device = tabState(state.page).emulation?.device;
|
|
31
|
+
if (device) return devices[device].viewport;
|
|
32
|
+
const set = tabState(state.page).viewport;
|
|
33
|
+
if (set) return set;
|
|
34
|
+
return state.page.evaluate(() => ({ width: innerWidth, height: innerHeight })).catch(() => null);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// The viewport's size (1280x657) when a point is outside it; null when it is inside, or the size is unknown.
|
|
38
|
+
async function outsideViewport(x, y) {
|
|
39
|
+
const size = await viewportSize();
|
|
40
|
+
if (!size || x >= 0 && y >= 0 && x < size.width && y < size.height) return null;
|
|
41
|
+
return `${size.width}x${size.height}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const IN_VIEW = 'A point is in CSS pixels from the top left of the viewport: scroll what you want into view (eval document.querySelector(<selector>).scrollIntoView(), or mouse wheel), then measure it again';
|
|
45
|
+
|
|
27
46
|
const DEFAULT_DEVICE = 'Pixel 7';
|
|
28
47
|
const EMULATE_KINDS = ['mobile', 'dark', 'light', 'locale', 'timezone'];
|
|
29
48
|
|
|
@@ -234,4 +253,4 @@ const commands = {
|
|
|
234
253
|
},
|
|
235
254
|
};
|
|
236
255
|
|
|
237
|
-
module.exports = { commands, viewportText, EMULATE_KINDS, emulatedKinds, deviceMetrics, viewportMetrics, setEmulation, setViewport, settled, reapplyMetrics };
|
|
256
|
+
module.exports = { commands, viewportText, outsideViewport, IN_VIEW, EMULATE_KINDS, emulatedKinds, deviceMetrics, viewportMetrics, setEmulation, setViewport, settled, reapplyMetrics };
|
package/lib/help.js
CHANGED
|
@@ -237,15 +237,16 @@ sent, as when the page stops answering, the outcome is unknown.`,
|
|
|
237
237
|
},
|
|
238
238
|
hover: { usage: 'hover <selector>', summary: 'hover, choosing the match as click does' },
|
|
239
239
|
drag: {
|
|
240
|
-
usage: 'drag <from> <to>',
|
|
241
|
-
summary: 'drag
|
|
240
|
+
usage: 'drag <from> <to> | <x1> <y1> <x2> <y2>',
|
|
241
|
+
summary: 'drag an element onto another, or between points',
|
|
242
242
|
args: [
|
|
243
243
|
['<from> <to>', 'selectors or snapshot refs, each quoted if it has spaces: drag e5 e9, drag "text=Item 1" "#done ul".'],
|
|
244
|
+
['<x1> <y1> <x2> <y2>', 'points, as for mouse commands: presses the left button at the first, moves to the second in 10 steps, and releases. For what has no element to drag by: a canvas, panning a map. The first point must be in the viewport; the second may be outside it.'],
|
|
244
245
|
],
|
|
245
246
|
detail: `As Playwright's dragTo: it hovers the first match of <from>, presses the left button, hovers the
|
|
246
247
|
first match of <to>, and releases, waiting up to 5s in all for each to be ready. An HTML5 drag (a
|
|
247
248
|
draggable element) gets dragstart, dragover and drop; any other gets pointer and mouse events. A drag
|
|
248
|
-
through points
|
|
249
|
+
through more points than two: mouse move, mouse down, mouse move as often as needed, mouse up.`,
|
|
249
250
|
},
|
|
250
251
|
touch: {
|
|
251
252
|
usage: 'touch <gesture> <x> <y> ...',
|
|
@@ -257,7 +258,8 @@ through points of its own: mouse move, mouse down, mouse move, mouse up.`,
|
|
|
257
258
|
['swipe <x> <y> <dx> <dy>', 'one finger down at x,y, moved by dx,dy in small steps about a frame apart, then lifted.'],
|
|
258
259
|
['pinch <x> <y> <scale>', 'two fingers side by side about x,y, 100px apart, moved apart until they are scale times that far apart, then lifted: 2 zooms in, under 1 (0.5) pinches in.'],
|
|
259
260
|
],
|
|
260
|
-
detail: `<x> <y> are CSS pixels from the top left of the viewport, as for mouse commands.
|
|
261
|
+
detail: `<x> <y> are CSS pixels from the top left of the viewport, as for mouse commands. A finger put down outside the
|
|
262
|
+
viewport is refused; a swipe may end outside it. It needs a touch screen:
|
|
261
263
|
emulate mobile, then reload, so the page sees one; without it, touch refuses.
|
|
262
264
|
|
|
263
265
|
Chromium takes a tap sent soon after a swipe or pinch as part of it, with no click: up to a few seconds
|
|
@@ -277,12 +279,13 @@ with no points, which lifts the rest.`,
|
|
|
277
279
|
['move <x> <y>', 'moves the mouse there: CSS pixels from the top left of the tab\'s viewport (not the page), as mouse events have them.'],
|
|
278
280
|
['click <x> <y> [button]', 'clicks there: left, right or middle (Default: left), or --button=<button>; --click-count=<n> as click\'s (2 a double-click, 3 a triple-click); --delay=<ms> holds the button that long, as Playwright\'s mouse.click delay.'],
|
|
279
281
|
['dblclick <x> <y> [button]', 'mouse click --click-count=2, as Playwright\'s mouse.dblclick: two clicks and a dblclick. Two mouse clicks are two single clicks, which fire no dblclick.'],
|
|
280
|
-
['down [button] | up [button]', 'presses or releases a button where the mouse is.
|
|
282
|
+
['down [button] | up [button]', 'presses or releases a button where the mouse is. From one point to another, drag <x1> <y1> <x2> <y2> drags; through more: mouse down, mouse move to each point, mouse up.'],
|
|
281
283
|
['wheel <dx> <dy>', 'turns the wheel where the mouse is; positive dy scrolls down. A map zooms around the mouse: mouse move 400 300, then mouse wheel 0 -300 zooms in. It returns once the events are sent, not once the page has redrawn; while recording, it scrolls in eased steps over up to 1s (help record).'],
|
|
282
284
|
],
|
|
283
285
|
detail: `For what has no selector, such as a marker drawn on a canvas; anything a selector finds is better
|
|
284
286
|
clicked with click, which waits for it. The mouse stays where it went, for a mouse wheel or mouse down
|
|
285
|
-
after it.
|
|
287
|
+
after it. A click, or a mouse down or wheel, outside the viewport is refused: nothing there gets
|
|
288
|
+
it. mouse move and mouse up go there and say so, as a drag the page holds on to can. The cursor (help cursor) moves there first and shows a ring at a press. Under emulate mobile it is
|
|
286
289
|
still the mouse; touch sends a finger's events (help touch).
|
|
287
290
|
|
|
288
291
|
A snapshot has no coordinates; eval of getBoundingClientRect gives an element's centre:
|
|
@@ -896,10 +899,12 @@ in front of it usually holds the request open, so the page spins instead of fail
|
|
|
896
899
|
],
|
|
897
900
|
options: [
|
|
898
901
|
['--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.'],
|
|
899
|
-
['--timeout=<time>', 'how long the page has to answer
|
|
902
|
+
['--timeout=<time>', 'how long the page has to answer. Default: 15s. Past 20s, give send time too, or it gives up first and cancels the eval: pw-repl send -t 70s eval --timeout=60s ...'],
|
|
900
903
|
['--all', 'the whole result, not capped.'],
|
|
901
904
|
],
|
|
902
|
-
detail: `
|
|
905
|
+
detail: `Its options go before the JavaScript, in any order; --all may also go at the very end, after it.
|
|
906
|
+
|
|
907
|
+
eval fetch(...) sends a new request, which can change state on the server; it cannot read one that
|
|
903
908
|
already happened (requests lists those).
|
|
904
909
|
|
|
905
910
|
Long work (--timeout): every other command waits while an eval runs, the other clients' too. For work of
|
|
@@ -1113,8 +1118,15 @@ const SHELL_COMMANDS = new Set(['run', 'serve', 'send', 'attach', 'stop', 'where
|
|
|
1113
1118
|
// Commands that were renamed, so the old name says where it went.
|
|
1114
1119
|
const RENAMED = { mouseclick: 'mouse click', mousedblclick: 'mouse dblclick' };
|
|
1115
1120
|
|
|
1121
|
+
// An action of mouse or touch (help swipe) is found under its command; one that is a command itself (click) has its own help.
|
|
1122
|
+
function actionOf(topic) {
|
|
1123
|
+
return ['mouse', 'touch'].find(command => COMMANDS[command].args.some(([usage]) => usage.split(' | ').some(u => u.split(' ')[0] === topic)));
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1116
1126
|
function notFound(topic) {
|
|
1117
1127
|
if (Object.hasOwn(RENAMED, topic)) return `${topic} is now ${RENAMED[topic]}: help ${RENAMED[topic].split(' ')[0]}`;
|
|
1128
|
+
const command = actionOf(topic);
|
|
1129
|
+
if (command) return `${topic} is ${command} ${topic}: help ${command}`;
|
|
1118
1130
|
if (SHELL_COMMANDS.has(topic)) return `${topic} is run at the shell (pw-repl ${topic}), not in the REPL; pw-repl --help has its options`;
|
|
1119
1131
|
return `No help for ${topic}. Topics: ${Object.keys(TOPICS).join(', ')}`;
|
|
1120
1132
|
}
|
package/lib/inspect.js
CHANGED
|
@@ -493,75 +493,96 @@ const commands = {
|
|
|
493
493
|
|
|
494
494
|
async eval(args, all) {
|
|
495
495
|
if (!args) throw new Error('Usage: eval <js-expression>');
|
|
496
|
-
//
|
|
496
|
+
// Its options, in any order, before the JavaScript; once that starts, nothing is an option (x --all is JavaScript).
|
|
497
|
+
// --timeout=<time>: how long the page has to answer. --file=<path>: the JavaScript is the file's text.
|
|
497
498
|
let timeout = COMMAND_TIMEOUT;
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
if (
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
throw new Error(`eval --file: cannot read ${file} (${error.code || error.message})${target ? '; quote a path with spaces' : ''}`);
|
|
499
|
+
let file = null;
|
|
500
|
+
args = args.trim();
|
|
501
|
+
for (let option; (option = /^--(all|timeout|file)(?=[=\s]|$)/.exec(args));) {
|
|
502
|
+
if (option[1] === 'all') {
|
|
503
|
+
if (!/^--all(?:\s|$)/.test(args)) break;
|
|
504
|
+
all = true;
|
|
505
|
+
args = args.slice(5).trimStart();
|
|
506
|
+
} else if (option[1] === 'timeout') {
|
|
507
|
+
const timed = /^--timeout(?:=|\s+)(\S+)(?:\s+|$)/.exec(args);
|
|
508
|
+
timeout = parseDuration(timed?.[1]);
|
|
509
|
+
if (!timeout) throw new Error('Usage: eval --timeout=<time> <JavaScript>, a time in ms or with s: eval --timeout=60s ...');
|
|
510
|
+
args = args.slice(timed[0].length);
|
|
511
|
+
} else {
|
|
512
|
+
const named = /^--file(?:=|\s+)("(?:[^"\\]|\\.)*"|'[^']*'|\S+)(?:\s+|$)/.exec(args);
|
|
513
|
+
if (!named) throw new Error('Usage: eval --file=<path> [<ref or selector>]: the JavaScript is in the file');
|
|
514
|
+
file = path.resolve(unquote(named[1]));
|
|
515
|
+
args = args.slice(named[0].length);
|
|
516
516
|
}
|
|
517
|
-
if (!args) throw new Error(`eval --file: ${file} is empty`);
|
|
518
|
-
ref = target && unquote(target);
|
|
519
|
-
} else {
|
|
520
|
-
// eval <function> <ref> calls the function with that element, as playwright-cli's eval does.
|
|
521
|
-
const onRef = /^([\s\S]+?)\s+((?:f\d+)?e\d+)$/.exec(args.trim());
|
|
522
|
-
if (onRef && isFunctionText(unquote(onRef[1]))) [args, ref] = [unquote(onRef[1]), onRef[2]];
|
|
523
517
|
}
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
518
|
+
// An option after the JavaScript is JavaScript to the page; a SyntaxError from it says so.
|
|
519
|
+
const late = !file && /(?:^|\s)(--(?:timeout|file|all))(?=[=\s]|$)/.exec(args)?.[1];
|
|
520
|
+
const run = async () => {
|
|
521
|
+
let ref = null;
|
|
522
|
+
// eval --file=<path> [<ref or selector>]: the file's text is what the line would be, over as many lines as
|
|
523
|
+
// it likes. The rest of the line can only be the element, so any selector goes there, not only a ref.
|
|
524
|
+
if (file) {
|
|
525
|
+
const target = args || null;
|
|
526
|
+
try { args = fs.readFileSync(file, 'utf8').trim(); } catch (error) {
|
|
527
|
+
// eval --file=my dir/x.js reads "my" as the file and "dir/x.js" as the element.
|
|
528
|
+
throw new Error(`eval --file: cannot read ${file} (${error.code || error.message})${target ? '; quote a path with spaces' : ''}`);
|
|
529
|
+
}
|
|
530
|
+
if (!args) throw new Error(`eval --file: ${file} is empty`);
|
|
531
|
+
ref = target && unquote(target);
|
|
532
|
+
} else {
|
|
533
|
+
if (!args) throw new Error('Usage: eval [--all] [--timeout=<time>] <JavaScript>');
|
|
534
|
+
// eval <function> <ref> calls the function with that element, as playwright-cli's eval does.
|
|
535
|
+
const onRef = /^([\s\S]+?)\s+((?:f\d+)?e\d+)$/.exec(args);
|
|
536
|
+
if (onRef && isFunctionText(unquote(onRef[1]))) [args, ref] = [unquote(onRef[1]), onRef[2]];
|
|
537
|
+
}
|
|
538
|
+
if (ref) {
|
|
539
|
+
const selector = toSelector(ref);
|
|
540
|
+
const el = await onElement(selector, () => state.page.locator(selector).first().elementHandle({ timeout: 5000 }));
|
|
531
541
|
try {
|
|
532
|
-
|
|
542
|
+
// Made from its text by the debugger, as eval is, so a page's CSP cannot
|
|
543
|
+
// refuse it, and in the element's own frame, which may be an iframe.
|
|
544
|
+
const fn = await evaluated((await el.ownerFrame()).evaluateHandle(args), timeout);
|
|
545
|
+
try {
|
|
546
|
+
printOutput(await evaluated(el.evaluate((e, f) => f(e), fn), timeout), all);
|
|
547
|
+
} finally {
|
|
548
|
+
await fn.dispose().catch(() => {});
|
|
549
|
+
}
|
|
533
550
|
} finally {
|
|
534
|
-
await
|
|
551
|
+
await el.dispose().catch(() => {});
|
|
535
552
|
}
|
|
536
|
-
|
|
537
|
-
await el.dispose().catch(() => {});
|
|
553
|
+
return;
|
|
538
554
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
555
|
+
// A function, as playwright-cli's eval takes, is called rather than returned.
|
|
556
|
+
// Only a single expression can be one; statements are evaluated as they are.
|
|
557
|
+
// await at the top level: one expression is run as an async function; statements as DevTools'
|
|
558
|
+
// console runs them (replMode), which gives back the last one's value.
|
|
559
|
+
const compiles = (Kind, body) => { try { new Kind(body); return true; } catch { return false; } };
|
|
560
|
+
const AsyncFunction = (async () => {}).constructor;
|
|
561
|
+
const awaits = /\bawait\b/.test(args) && !compiles(Function, `return (${args}\n);`);
|
|
562
|
+
const single = compiles(awaits ? AsyncFunction : Function, `return (${args}\n);`);
|
|
563
|
+
const call = `const value = (${args}\n); return typeof value === 'function' ? value() : value;`;
|
|
564
|
+
if (awaits && !single && compiles(AsyncFunction, args)) {
|
|
565
|
+
const session = await keptSession(state.page);
|
|
566
|
+
const { result, exceptionDetails } = await evaluated(session.send('Runtime.evaluate', { expression: args, replMode: true, awaitPromise: true, returnByValue: true }), timeout);
|
|
567
|
+
if (exceptionDetails) throw new Error(exceptionDetails.exception?.description || exceptionDetails.text);
|
|
568
|
+
printOutput(result.value !== undefined ? result.value : result.description ?? 'undefined', all);
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
const expression = single ? `(${awaits ? 'async ' : ''}() => { ${call} })()` : args;
|
|
572
|
+
const value = await evaluated(state.page.evaluate(expression), timeout);
|
|
573
|
+
// An empty string is shown as one, not as no output at all.
|
|
574
|
+
printOutput(value === '' ? '""' : value, all);
|
|
575
|
+
// Quotes around the whole of it, as a shell's quoting sent on, make it a string, not code to run:
|
|
576
|
+
// one plain literal only, not "a" + b or a template with ${...} in it, which ran.
|
|
577
|
+
if (typeof value === 'string' && value && /^(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\$]|\\.|\$(?!\{))*`)$/s.test(args.trim())) {
|
|
578
|
+
out.log('(That is a string: the JavaScript was in quotes, which eval keeps. Without them, it runs: eval document.title)');
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
try {
|
|
582
|
+
await run();
|
|
583
|
+
} catch (error) {
|
|
584
|
+
if (late && /SyntaxError/.test(error.message)) error.message += `\n(${late} after the JavaScript is read as JavaScript: eval's options go before it)`;
|
|
585
|
+
throw error;
|
|
565
586
|
}
|
|
566
587
|
},
|
|
567
588
|
|
package/lib/send.js
CHANGED
|
@@ -93,12 +93,15 @@ function doubleQuote(word) {
|
|
|
93
93
|
return `"${word.replace(/[\\"]/g, '\\$&').replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/\t/g, '\\t')}"`;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
// Where eval's --file is,
|
|
96
|
+
// Where eval's --file is, among its options before the JavaScript (--all, --timeout); -1 when it has none.
|
|
97
97
|
function evalFileAt(words) {
|
|
98
98
|
if (words[0] !== 'eval') return -1;
|
|
99
99
|
let at = 1;
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
for (;;) {
|
|
101
|
+
if (words[at] === '--all' || /^--timeout=/.test(words[at] || '')) at += 1;
|
|
102
|
+
else if (words[at] === '--timeout') at += 2;
|
|
103
|
+
else break;
|
|
104
|
+
}
|
|
102
105
|
return /^--file(?:=|$)/.test(words[at] || '') ? at : -1;
|
|
103
106
|
}
|
|
104
107
|
|
package/lib/touch.js
CHANGED
|
@@ -6,6 +6,7 @@ const out = require('./output');
|
|
|
6
6
|
const { numbers, parseDuration, durationText } = require('./util');
|
|
7
7
|
const { tabState } = require('./tabstate');
|
|
8
8
|
const { keptSession } = require('./cdp');
|
|
9
|
+
const { outsideViewport, IN_VIEW } = require('./emulation');
|
|
9
10
|
|
|
10
11
|
const GESTURES = ['tap', 'doubletap', 'longpress', 'swipe', 'pinch'];
|
|
11
12
|
const LONGPRESS = 800;
|
|
@@ -53,6 +54,9 @@ const commands = {
|
|
|
53
54
|
const values = numbers(rest.join(' '), usageOf(gesture));
|
|
54
55
|
if (values.length < (gesture === 'longpress' ? 2 : counts) || values.length > counts) throw new Error(usage);
|
|
55
56
|
const [x, y] = values;
|
|
57
|
+
// A finger put down outside the viewport touches nothing; a swipe may still end outside it.
|
|
58
|
+
const outside = await outsideViewport(x, y);
|
|
59
|
+
if (outside) throw new Error(`${x}, ${y} is outside the viewport (${outside}); nothing was touched. ${IN_VIEW}`);
|
|
56
60
|
if (gesture === 'tap') {
|
|
57
61
|
await tap(x, y);
|
|
58
62
|
out.log(`Tapped at ${x}, ${y}`);
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -53,14 +53,15 @@ first tab).
|
|
|
53
53
|
- `pw-repl serve` runs the same in a terminal, where the pane shows every command. Its prompt is
|
|
54
54
|
`pw[serve]>`. It stops at that prompt (`quit`, or Ctrl-C); `pw-repl stop` is for a background one.
|
|
55
55
|
- Add `--launch` to `run` or `serve` (with or without `--background`) to have it start a Chromium of its
|
|
56
|
-
own instead of connecting to one: headless unless `--headed`, in a 1280x800 window
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
passed to that Chromium (only flags: a
|
|
60
|
-
among them is used instead of the temporary
|
|
61
|
-
Chromium picks up where it left off, as it would for
|
|
62
|
-
session cookies with them, so a login stays. They start
|
|
63
|
-
`requests` and `console` have only what came after, often
|
|
56
|
+
own instead of connecting to one: headless unless `--headed`, in a 1280x800 window (a 1280x657 page
|
|
57
|
+
headless; info shows the viewport mouse points are in), in a temporary profile, on a free port of its
|
|
58
|
+
own (never 9222). It stops with the REPL, Ctrl-C while it starts included, and `stop` returns once it
|
|
59
|
+
has exited. It prints the command it ran; flags after `--` are passed to that Chromium (only flags: a
|
|
60
|
+
start URL goes before `--`), and a `--user-data-dir=<dir>` among them is used instead of the temporary
|
|
61
|
+
profile, and kept. Started again in a kept profile, Chromium picks up where it left off, as it would for
|
|
62
|
+
the user, headless too: its tabs come back, and session cookies with them, so a login stays. They start
|
|
63
|
+
loading before the REPL is attached, so `requests` and `console` have only what came after, often
|
|
64
|
+
nothing: `reload` for a full record.
|
|
64
65
|
- `pw-repl run` runs it in a terminal with no server; `send` then reaches it through tmux, if it runs in
|
|
65
66
|
the tmux session `pw-repl`:
|
|
66
67
|
|