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 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 one element onto another',
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 of its own: mouse move, mouse down, mouse move, mouse up.`,
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. It needs a touch screen:
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. A drag: mouse down, mouse move to another point, mouse up.'],
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. The cursor (help cursor) moves there first and shows a ring at a press. Under emulate mobile it is
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, before --file. Default: 15s. Past 20s, give send time too, or it gives up first and cancels the eval: pw-repl send -t 70s eval --timeout=60s ...'],
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: `eval fetch(...) sends a new request, which can change state on the server; it cannot read one that
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
- // eval --timeout=<time> [--file=...] <JavaScript>: how long the page has to answer.
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
- const timed = /^--timeout(?:=|\s+)(\S+)(?:\s+([\s\S]*))?$/.exec(args.trim());
499
- if (/^--timeout(?:[=\s]|$)/.test(args.trim())) {
500
- timeout = parseDuration(timed?.[1]);
501
- if (!timeout) throw new Error('Usage: eval --timeout=<time> <JavaScript>, a time in ms or with s: eval --timeout=60s ...');
502
- args = timed[2] || '';
503
- if (!args.trim()) throw new Error('Usage: eval --timeout=<time> <JavaScript>');
504
- }
505
- let ref = null;
506
- // eval --file=<path> [<ref or selector>]: the file's text is what the line would be, over as many lines as
507
- // it likes. The rest of the line can only be the element, so any selector goes there, not only a ref.
508
- const fromFile = /^--file(?:=|\s+)("(?:[^"\\]|\\.)*"|'[^']*'|\S+)(?:\s+([\s\S]*))?$/.exec(args.trim());
509
- if (/^--file(?:[=\s]|$)/.test(args.trim())) {
510
- if (!fromFile) throw new Error('Usage: eval --file=<path> [<ref or selector>]: the JavaScript is in the file');
511
- const file = path.resolve(unquote(fromFile[1]));
512
- const target = fromFile[2]?.trim() || null;
513
- try { args = fs.readFileSync(file, 'utf8').trim(); } catch (error) {
514
- // eval --file=my dir/x.js reads "my" as the file and "dir/x.js" as the element.
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
- if (ref) {
525
- const selector = toSelector(ref);
526
- const el = await onElement(selector, () => state.page.locator(selector).first().elementHandle({ timeout: 5000 }));
527
- try {
528
- // Made from its text by the debugger, as eval is, so a page's CSP cannot
529
- // refuse it, and in the element's own frame, which may be an iframe.
530
- const fn = await evaluated((await el.ownerFrame()).evaluateHandle(args), timeout);
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
- printOutput(await evaluated(el.evaluate((e, f) => f(e), fn), timeout), all);
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 fn.dispose().catch(() => {});
551
+ await el.dispose().catch(() => {});
535
552
  }
536
- } finally {
537
- await el.dispose().catch(() => {});
553
+ return;
538
554
  }
539
- return;
540
- }
541
- // A function, as playwright-cli's eval takes, is called rather than returned.
542
- // Only a single expression can be one; statements are evaluated as they are.
543
- // await at the top level: one expression is run as an async function; statements as DevTools'
544
- // console runs them (replMode), which gives back the last one's value.
545
- const compiles = (Kind, body) => { try { new Kind(body); return true; } catch { return false; } };
546
- const AsyncFunction = (async () => {}).constructor;
547
- const awaits = /\bawait\b/.test(args) && !compiles(Function, `return (${args}\n);`);
548
- const single = compiles(awaits ? AsyncFunction : Function, `return (${args}\n);`);
549
- const call = `const value = (${args}\n); return typeof value === 'function' ? value() : value;`;
550
- if (awaits && !single && compiles(AsyncFunction, args)) {
551
- const session = await keptSession(state.page);
552
- const { result, exceptionDetails } = await evaluated(session.send('Runtime.evaluate', { expression: args, replMode: true, awaitPromise: true, returnByValue: true }), timeout);
553
- if (exceptionDetails) throw new Error(exceptionDetails.exception?.description || exceptionDetails.text);
554
- printOutput(result.value !== undefined ? result.value : result.description ?? 'undefined', all);
555
- return;
556
- }
557
- const expression = single ? `(${awaits ? 'async ' : ''}() => { ${call} })()` : args;
558
- const value = await evaluated(state.page.evaluate(expression), timeout);
559
- // An empty string is shown as one, not as no output at all.
560
- printOutput(value === '' ? '""' : value, all);
561
- // Quotes around the whole of it, as a shell's quoting sent on, make it a string, not code to run:
562
- // one plain literal only, not "a" + b or a template with ${...} in it, which ran.
563
- if (typeof value === 'string' && value && /^(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\$]|\\.|\$(?!\{))*`)$/s.test(args.trim())) {
564
- out.log('(That is a string: the JavaScript was in quotes, which eval keeps. Without them, it runs: eval document.title)');
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, after its --timeout if it has one; -1 when it has none.
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
- if (/^--timeout=/.test(words[at] || '')) at += 1;
101
- else if (words[at] === '--timeout') at += 2;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pw-repl",
3
- "version": "0.15.0",
3
+ "version": "0.15.1",
4
4
  "description": "Text REPL for driving an existing Chromium through Playwright over CDP",
5
5
  "keywords": [
6
6
  "playwright",
package/skill/SKILL.md CHANGED
@@ -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, in a temporary
57
- profile, on a free port of its own (never 9222). It stops with the REPL, Ctrl-C while it starts
58
- included, and `stop` returns once it has exited. It prints the command it ran; flags after `--` are
59
- passed to that Chromium (only flags: a start URL goes before `--`), and a `--user-data-dir=<dir>`
60
- among them is used instead of the temporary profile, and kept. Started again in a kept profile,
61
- Chromium picks up where it left off, as it would for the user, headless too: its tabs come back, and
62
- session cookies with them, so a login stays. They start loading before the REPL is attached, so
63
- `requests` and `console` have only what came after, often nothing: `reload` for a full record.
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