pw-repl 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -100,7 +100,7 @@ the Docker bridge gateway, `ip route | awk '/default/ {print $3}'`).
100
100
  | record requests and console together | `capture on`, then `capture off` |
101
101
  | record a video of the page | `record on`, then `record off` (`help video` for a take with a pointer) |
102
102
  | show a pointer where the agent acts | `cursor on`, then `cursor off` (in screenshots and videos) |
103
- | point something out on the page | `highlight <ref>` (`--style=<css>` to restyle it), then `highlight off` |
103
+ | point something out on the page | `highlight <ref>`: a box, unlabelled unless `--labels on`; `highlight off` hides it |
104
104
  | break the backend on purpose | `route <glob> <status> <json>` (fake a response), `route <glob> abort`, `network off` |
105
105
  | change or slow an API response | `route <glob> patch <json>`, `route <glob> delay <secs>` |
106
106
  | slow the whole network | `network slow` |
@@ -112,8 +112,8 @@ Everything else is in `help <topic>`; `help <command>` has usage and caveats.
112
112
 
113
113
  `watch`, `capture`, `route`, `network off` or `slow`, `emulate`, `viewport`, `record`, `highlight` and `cursor`
114
114
  stay on until you turn them off: `watch on|off`, `capture on|off`, `route ...|route off`, `network off|slow|on`,
115
- `emulate ...|emulate off`, `viewport <WxH>|viewport off`, `record on|off`, `highlight <selector>|highlight off`,
116
- `cursor on|off`.
115
+ `emulate ...|emulate off`, `viewport <WxH>|viewport off`, `record on|off`, `highlight <selector>|highlight off`
116
+ (and `highlight --labels on|off`), `cursor on|off`.
117
117
  While any are on in the selected tab, the prompt shows them: `(watch network:off routes:2) pw>`. `modes`
118
118
  lists them for every tab, and `modes off` turns them all off.
119
119
 
@@ -125,7 +125,7 @@ their own show their state and what you can run next.
125
125
  [playwright-cli](https://github.com/microsoft/playwright-cli) is Microsoft's command line for agents. It
126
126
  gives each agent a browser session of its own; pw-repl shares one browser between a person and agents.
127
127
  Its command names and most of their options work here too (`help playwright-cli` lists them), and where
128
- pw-repl differs, it is because someone else may be using the browser:
128
+ pw-repl differs, it is mostly because someone else may be using the browser:
129
129
 
130
130
  - `open` and `close` open a tab and turn off your modes; they never start, close or wipe a browser.
131
131
  - Routes, network and emulation apply to one tab, not the whole browser.
@@ -133,6 +133,8 @@ pw-repl differs, it is because someone else may be using the browser:
133
133
  - There is no snapshot after every command, since a person reads the REPL too; `snapshot` shows one.
134
134
  - Instead of `-s=<session>`, an agent is a client of its own (`send -c <name>`).
135
135
  - `watch` records what a person does as steps with their requests, not as generated code.
136
+ - `highlight` draws its box without Playwright's locator label, which covers what is below it and puts
137
+ selectors in screenshots and videos; `highlight --labels on` shows it.
136
138
 
137
139
  For a browser of its own per agent, isolated sessions, traces, video, PDFs or generated test code,
138
140
  playwright-cli is the better fit. `record on` records a tab's page, and `cursor on` draws a pointer where the
package/lib/cursor.js CHANGED
@@ -316,13 +316,25 @@ const commands = {
316
316
  } else if (where) {
317
317
  const target = p.locator(toSelector(unquote(where))).first();
318
318
  try {
319
- await target.scrollIntoViewIfNeeded({ timeout: 5000 });
319
+ await target.waitFor({ state: 'visible', timeout: 5000 });
320
320
  } catch {
321
321
  throw new Error(`No element matches ${where} that can be shown (waited 5s); ${still}`);
322
322
  }
323
- const box = await target.boundingBox();
323
+ // Only pointing: an element partly in view is pointed at where it shows, not scrolled, so that
324
+ // one the page cuts off still looks cut off (a bug in a video). One wholly out of view is scrolled to.
325
+ const view = await p.evaluate(() => ({ width: innerWidth, height: innerHeight }));
326
+ const inView = b => b && b.x < view.width && b.y < view.height && b.x + b.width > 0 && b.y + b.height > 0;
327
+ let box = await target.boundingBox();
328
+ if (!inView(box)) {
329
+ await target.scrollIntoViewIfNeeded({ timeout: 5000 }).catch(() => {});
330
+ box = await target.boundingBox();
331
+ }
324
332
  if (!box) throw new Error(`${where} is not on the page to show the cursor at; ${still}`);
325
- at = { x: Math.round(box.x + box.width / 2), y: Math.round(box.y + box.height / 2) };
333
+ const left = Math.max(box.x, 0);
334
+ const top = Math.max(box.y, 0);
335
+ const right = Math.min(box.x + box.width, view.width);
336
+ const bottom = Math.min(box.y + box.height, view.height);
337
+ at = { x: Math.round((left + right) / 2), y: Math.round((top + bottom) / 2) };
326
338
  } else {
327
339
  at = tabState(p).mouseAt || await p.evaluate(() => ({ x: Math.round(innerWidth / 2), y: Math.round(innerHeight / 2) }));
328
340
  }
package/lib/elements.js CHANGED
@@ -9,6 +9,7 @@ const { numbers, hints } = require('./util');
9
9
  const { tabState } = require('./tabstate');
10
10
  const { cursorOf, aiming, aimAt, glideTo, showClick, showSelect } = require('./cursor');
11
11
  const { recordingOf, stepTarget } = require('./record');
12
+ const { settled } = require('./emulation');
12
13
 
13
14
  // The type a page sees for a file it is given, from its extension.
14
15
  const MIME_TYPES = {
@@ -282,7 +283,7 @@ async function hideHighlights(p, mine = false) {
282
283
  if (!highlights?.size) return 0;
283
284
  const hidden = [...highlights].filter(([, h]) => !mine || (h.owner ?? null) === (state.client ?? null));
284
285
  if (hidden.length === highlights.size) await p.hideHighlight();
285
- else for (const [selector] of hidden) await p.locator(selector).hideHighlight();
286
+ else for (const [selector, h] of hidden) await (h.drawn || p.locator(selector)).hideHighlight();
286
287
  for (const [selector] of hidden) highlights.delete(selector);
287
288
  return hidden.length;
288
289
  }
@@ -293,6 +294,67 @@ onShutdown(async () => {
293
294
  await Promise.all(pages.map(p => hideHighlights(p).catch(() => {})));
294
295
  });
295
296
 
297
+ // Playwright's highlight labels each box with its locator, which covers what is below it, and has
298
+ // no option not to; highlights are drawn without it unless highlight --labels on. Its overlay is an
299
+ // x-pw-glass element with a closed shadow root, which only CDP reaches: a stylesheet adopted there
300
+ // hides the labels, under any CSP, and stays as Playwright redraws them. Playwright removes the
301
+ // overlay with its last highlight, and the sheet with it, so it is applied again after each one.
302
+ const LABELS_OFF = 'x-pw-tooltip:not([data-pw-repl])';
303
+ // The clients told that the labels could not be hidden, each once.
304
+ const labelsWarned = new Set();
305
+
306
+ // Adds or removes the sheet in the main document's overlay, and says whether it worked: false when
307
+ // there is an overlay but its labels are not hidden. Null when there is no overlay.
308
+ async function applyLabels(p, hide) {
309
+ const session = await p.context().newCDPSession(p);
310
+ try {
311
+ const { root } = await session.send('DOM.getDocument', { depth: 0 });
312
+ const { nodeId } = await session.send('DOM.querySelector', { nodeId: root.nodeId, selector: 'x-pw-glass' });
313
+ if (!nodeId) return null;
314
+ const { node } = await session.send('DOM.describeNode', { nodeId, pierce: true });
315
+ const shadow = node.shadowRoots?.[0];
316
+ if (!shadow) return false;
317
+ const { object } = await session.send('DOM.resolveNode', { backendNodeId: shadow.backendNodeId });
318
+ const call = (fn, args) => session.send('Runtime.callFunctionOn', { objectId: object.objectId, functionDeclaration: fn.toString(), arguments: args.map(value => ({ value })), returnByValue: true });
319
+ await call(function (rule, hidden) {
320
+ const sheets = [...this.adoptedStyleSheets].filter(s => ![...s.cssRules].some(r => r.selectorText === rule));
321
+ if (hidden) {
322
+ const sheet = new CSSStyleSheet();
323
+ sheet.replaceSync(`${rule} { display: none !important; }`);
324
+ sheets.push(sheet);
325
+ }
326
+ this.adoptedStyleSheets = sheets;
327
+ }, [LABELS_OFF, hide]);
328
+ if (!hide) return true;
329
+ // Playwright draws its labels on a later frame.
330
+ await settled(p);
331
+ const { result } = await call(function (rule) {
332
+ const ours = [...this.adoptedStyleSheets].some(s => [...s.cssRules].some(r => r.selectorText === rule));
333
+ return ours && [...this.querySelectorAll('x-pw-tooltip')].every(t => getComputedStyle(t).display === 'none');
334
+ }, [LABELS_OFF]);
335
+ return result.value === true;
336
+ } finally {
337
+ await session.detach().catch(() => {});
338
+ }
339
+ }
340
+
341
+ // Hides the tab's labels unless they are on; says so once if that failed, and the highlight is drawn
342
+ // with its label as Playwright draws it. A CDP call that fails, as one does when the tab navigates or
343
+ // closes, says nothing of Playwright's overlay: only the check finding the labels shown does.
344
+ async function keepLabels(p) {
345
+ if (tabState(p).labelsOn) return;
346
+ const worked = await applyLabels(p, true).catch(() => null);
347
+ const client = state.client ?? null;
348
+ if (worked !== false || labelsWarned.has(client)) return;
349
+ labelsWarned.add(client);
350
+ out.log(`highlight: could not hide the labels; Playwright's highlight has changed (playwright-core ${require('playwright-core/package.json').version}). The boxes are drawn with their labels.`);
351
+ }
352
+
353
+ async function setLabels(p, on) {
354
+ if (on) tabState(p).labelsOn = { owner: state.client }; else delete tabState(p).labelsOn;
355
+ if (on) await applyLabels(p, false); else await keepLabels(p);
356
+ }
357
+
296
358
  const commands = {
297
359
  async click(args) {
298
360
  const { selector, shown, options } = clickArgs(args, 'click <selector> [left|right|middle] [--modifiers=<key>[,<key>]]');
@@ -313,15 +375,27 @@ const commands = {
313
375
  },
314
376
 
315
377
  async highlight(args) {
316
- const usage = 'Usage: highlight [<selector> [--style=<css>] | --hide [<selector>] | off]';
317
- const { found, rest } = takeOptions((args || '').trim(), { style: 'value', hide: 'flag' });
378
+ const usage = 'Usage: highlight [<selector> [--style=<css>] | --hide [<selector>] | --labels off|on | off]';
379
+ const { found, rest } = takeOptions((args || '').trim(), { style: 'value', hide: 'flag', labels: 'value' });
318
380
  const style = found.find(o => o.name === 'style');
319
381
  const hide = found.some(o => o.name === 'hide');
382
+ const labels = found.find(o => o.name === 'labels');
320
383
  const highlights = highlightsOf(state.page);
384
+ // For the whole tab, with a highlight to draw or on its own.
385
+ if (labels) {
386
+ if (hide || rest === 'off' || !['off', 'on'].includes(labels.value)) throw new Error(usage);
387
+ await setLabels(state.page, labels.value === 'on');
388
+ out.log(labels.value === 'on'
389
+ ? 'Labels on in the selected tab: each highlight is labelled with its locator; highlight --labels off hides them'
390
+ : 'Labels off in the selected tab');
391
+ if (!rest) return;
392
+ }
321
393
  if (!rest && !found.length) {
322
394
  if (!highlights.size) { out.log(`Nothing is highlighted in the selected tab.\n${hints([['highlight <selector>', 'draw a box around it']])}`); return; }
323
- for (const [selector, h] of highlights) out.log(`${selector}${h.style ? ` --style=${JSON.stringify(h.style)}` : ''}${h.owner ? ` (by ${h.owner})` : ''}`);
324
- out.log(hints([['highlight --hide [<selector>]', 'hide one, or all']]));
395
+ for (const [selector, h] of highlights) out.log(`${selector}${h.drawn?.toString() !== state.page.locator(selector).toString() ? ` as ${h.drawn}` : ''}${h.style ? ` --style=${JSON.stringify(h.style)}` : ''}${h.owner ? ` (by ${h.owner})` : ''}`);
396
+ const on = tabState(state.page).labelsOn;
397
+ if (on) out.log(`Labels are on${on.owner ? ` (by ${on.owner})` : ''}.`);
398
+ out.log(hints([['highlight --hide [<selector>]', 'hide one, or all'], on ? ['highlight --labels off', 'hide their labels'] : ['highlight --labels on', 'label each with its locator']]));
325
399
  return;
326
400
  }
327
401
  if (rest === 'off' && !found.length || hide && !rest) {
@@ -334,7 +408,7 @@ const commands = {
334
408
  const locator = state.page.locator(selector);
335
409
  if (hide) {
336
410
  if (!highlights.has(selector)) throw new Error(`${rest} is not highlighted; highlight lists what is`);
337
- await locator.hideHighlight();
411
+ await (highlights.get(selector).drawn || locator).hideHighlight();
338
412
  highlights.delete(selector);
339
413
  out.log(`Hid the highlight on ${rest}`);
340
414
  return;
@@ -342,9 +416,13 @@ const commands = {
342
416
  const count = await locator.count();
343
417
  if (!count) throw new Error(`No element matches ${rest}; nothing was highlighted`);
344
418
  // Drawn again, with its new style, rather than twice.
345
- if (highlights.has(selector)) await locator.hideHighlight();
346
- await locator.highlight(style ? { style: style.value } : {});
347
- highlights.set(selector, { owner: state.client, style: style?.value || null });
419
+ if (highlights.has(selector)) await (highlights.get(selector).drawn || locator).hideHighlight();
420
+ // Playwright labels a box with the locator it is given, and a snapshot ref's (aria-ref=e5) finds
421
+ // nothing in the page's code: one is drawn with the locator Playwright makes for its element.
422
+ const drawn = REF.test(rest) ? await locator.normalize().catch(() => locator) : locator;
423
+ await drawn.highlight(style ? { style: style.value } : {});
424
+ await keepLabels(state.page);
425
+ highlights.set(selector, { owner: state.client, style: style?.value || null, drawn });
348
426
  out.log(`Highlighted ${rest}${count > 1 ? ` (all ${count} matches)` : ''}; highlight --hide ${rest} hides it`);
349
427
  },
350
428
 
@@ -511,4 +589,4 @@ const commands = {
511
589
  },
512
590
  };
513
591
 
514
- module.exports = { commands, soleSelector, onElement, hideHighlights };
592
+ module.exports = { commands, soleSelector, onElement, hideHighlights, setLabels };
package/lib/emulation.js CHANGED
@@ -66,6 +66,18 @@ function settled(p) {
66
66
  ]);
67
67
  }
68
68
 
69
+ // Bringing a tab to the front can leave Chrome capturing it at the window's size after its next
70
+ // navigation, although the page is still laid out at its viewport's or phone's: sending the same
71
+ // metrics again, on the kept session, has it draw at them.
72
+ async function reapplyMetrics(p) {
73
+ const tab = tabState(p);
74
+ if (!tab.viewport && !tab.emulation?.device) return;
75
+ const session = await keptSession(p);
76
+ if (tab.viewport) await viewportMetrics(session, tab.viewport);
77
+ else if (tab.emulation?.device) await deviceMetrics(session, tab.emulation.device);
78
+ await settled(p);
79
+ }
80
+
69
81
  async function setViewport(p, size) {
70
82
  const session = await keptSession(p);
71
83
  if (size) {
@@ -220,4 +232,4 @@ const commands = {
220
232
  },
221
233
  };
222
234
 
223
- module.exports = { commands, viewportText, EMULATE_KINDS, emulatedKinds, deviceMetrics, viewportMetrics, setEmulation, setViewport, settled };
235
+ module.exports = { commands, viewportText, EMULATE_KINDS, emulatedKinds, deviceMetrics, viewportMetrics, setEmulation, setViewport, settled, reapplyMetrics };
package/lib/help.js CHANGED
@@ -359,26 +359,37 @@ Playwright adds to window as it acts are left out, and it says how many.`,
359
359
  links: { usage: 'links [--all]', summary: 'links on the page (text and href)' },
360
360
  inputs: { usage: 'inputs [--all]', summary: 'form controls on the page; values are omitted' },
361
361
  highlight: {
362
- usage: 'highlight [<selector> [--style=<css>] | off]',
362
+ usage: 'highlight [<selector> | off] [options]',
363
363
  summary: 'draw a box over elements, for the user or a screenshot',
364
364
  args: [
365
- ['<selector>', 'draws Playwright\'s highlight over every match, as playwright-cli\'s does: a tinted box labelled with its locator, that follows the element as the page scrolls or changes, and is in screenshots.'],
365
+ ['<selector>', 'draws Playwright\'s highlight over every match, as playwright-cli\'s does: a tinted box that follows the element as the page scrolls or changes, and is in screenshots.'],
366
366
  ['(none)', 'lists the selected tab\'s highlights.'],
367
367
  ['off', 'hides them all, as highlight --hide does.'],
368
368
  ],
369
369
  options: [
370
370
  ['--style=<css>', 'CSS added to the box, quoted if it has spaces: highlight e5 --style="outline: 3px solid blue; border-radius: 50%" circles e5 in blue.'],
371
371
  ['--hide <selector>', 'hides one.'],
372
+ ['--labels on|off', 'labels every box in the selected tab with its locator, as Playwright does, or not: those drawn already too. With a selector or on its own (highlight --labels on). Default: off.'],
372
373
  ],
373
374
  detail: `A highlight is a mode of its tab: modes shows it, modes off hides it, and the REPL hides every one as
374
375
  it stops. Loading a new page drops them; a page that changes its route without loading keeps them.
375
376
 
376
377
  The box is drawn over the page, in an element of Playwright's own added to it (x-pw-glass); the page's
377
- own elements are not changed, and clicks and typing reach them as before. Playwright always labels the
378
- box, and the label can cover what is just below it.`,
379
- moreAbout: 'marking a screenshot without the label',
380
- more: `For a screenshot without the label, eval can outline the element itself (with border-radius: 50% for
381
- a circle). That changes the page, so put it back afterwards, as it was if it had a style of its own:
378
+ own elements are not changed, and clicks and typing reach them as before.
379
+
380
+ Labels (--labels): Playwright labels each box with its locator, which covers what is just below it and
381
+ shows selectors in a screenshot or a video, so the REPL hides the labels unless they are on. On is a mode
382
+ of the tab, until --labels off or modes off. A box is labelled with the selector it was drawn from, e.g.
383
+ locator('role=button[name="Pay"]'); one drawn from a snapshot ref, with the locator Playwright makes for
384
+ its element, e.g. getByRole('button', { name: 'Pay' }), as the ref's own is not in the page's code.`,
385
+ moreAbout: 'labels inside a frame, and when they cannot be hidden',
386
+ more: `A highlight on an element inside an iframe is drawn in the frame's own document, and keeps its label.
387
+
388
+ The REPL hides the labels by reaching into Playwright's overlay, which a new Playwright could change: if
389
+ it cannot, it tells each client once, and the boxes are drawn with their labels.
390
+
391
+ For a screenshot without one then, eval can outline the element itself (with border-radius: 50% for a
392
+ circle). That changes the page, so put it back afterwards, as it was if it had a style of its own:
382
393
  eval document.querySelector("#rate-3").style.outline = "3px solid blue"
383
394
  eval document.querySelector("#rate-3").style.outline = ""`,
384
395
  },
@@ -399,14 +410,15 @@ else in /tmp.
399
410
 
400
411
  It brings the tab to the front of its window first: Chrome draws only the tab in front. The image is
401
412
  at CSS pixel size, one image pixel per CSS pixel. While emulate mobile is on, it is what the phone
402
- shows: a page laid out wider than the phone (no viewport meta tag) comes out shrunk to fit.`,
413
+ shows: a page laid out wider than the phone (no viewport meta tag) comes out shrunk to fit. An element bigger
414
+ than the viewport is taken with the page's scrollbar hidden for a moment, so that it does not move over.`,
403
415
  },
404
416
  cursor: {
405
417
  usage: 'cursor [on [<selector> | <x> <y>] | off]',
406
418
  summary: 'draw a pointer where the REPL acts, for videos',
407
419
  args: [
408
420
  ['on', 'fades it in where the mouse last went, or the middle.'],
409
- ['on <selector>', 'fades it in on the element (a video\'s first, say), scrolled into view; when it is on already, glides it there.'],
421
+ ['on <selector>', 'fades it in on the element (a video\'s first, say); when it is on already, glides it there. An element partly in view is pointed at where it shows, without scrolling, so one the page cuts off still looks cut off; one out of view is scrolled to.'],
410
422
  ['on <x> <y>', 'the same, at a point in the viewport.'],
411
423
  ['off', 'fades it out.'],
412
424
  ['(none)', 'says whether it is on, and where.'],
@@ -511,7 +523,8 @@ goes on recording. There is no --mp4 or --webm: the file's ending picks the form
511
523
  ['(none)', 'shows what is on.'],
512
524
  ],
513
525
  detail: `Per tab, until turned off or the REPL exits; emulate mobile <device> while another is on switches to
514
- it. While mobile is on, viewport refuses: the device sets the size.
526
+ it. Mobile and viewport both set the screen size, so each refuses while the other is on: viewport off before
527
+ emulate mobile, and emulate mobile off before viewport.
515
528
 
516
529
  The page sees the user agent, touch and navigator.languages from its next load: reload after turning
517
530
  mobile or locale on or off. Touch means the page sees a touch screen; click still clicks with the
package/lib/inspect.js CHANGED
@@ -12,7 +12,7 @@ const { keptSession } = require('./cdp');
12
12
  const { CDP_REFUSED } = require('./dialogs');
13
13
  const { onElement, soleSelector } = require('./elements');
14
14
  const { waitForLoad, waitForRequest } = require('./requestlog');
15
- const { deviceMetrics, viewportMetrics } = require('./emulation');
15
+ const { deviceMetrics, viewportMetrics, reapplyMetrics } = require('./emulation');
16
16
 
17
17
  const SCREENSHOT_DELAY_MAX = 60;
18
18
 
@@ -51,6 +51,32 @@ async function deviceShot(device, { ref, full, type }) {
51
51
  return Buffer.from(data, 'base64');
52
52
  }
53
53
 
54
+ // An element bigger than the viewport is taken with the page laid out again at its whole size, where
55
+ // the page's scrollbar is gone and what was beside it moves over by its width, after the element was
56
+ // measured: the shot came out shifted. Laid out without the scrollbar for the shot, by a stylesheet
57
+ // adopted for it (under any CSP), the element is measured where it is taken. Someone looking at the
58
+ // page sees its scrollbar go for a moment.
59
+ const NO_SCROLLBAR = 'html:not([data-pw-repl])';
60
+ async function elementShot(locator, type) {
61
+ const fits = await locator.evaluate(el => { const r = el.getBoundingClientRect(); return r.width <= innerWidth && r.height <= innerHeight; }, null, { timeout: 5000 });
62
+ if (fits) return locator.screenshot({ type, timeout: 5000 });
63
+ const sheet = hide => state.page.evaluate(([rule, on]) => {
64
+ const sheets = [...document.adoptedStyleSheets].filter(s => ![...s.cssRules].some(r => r.selectorText === rule));
65
+ if (on) {
66
+ const s = new CSSStyleSheet();
67
+ s.replaceSync(`${rule} { scrollbar-width: none !important; }`);
68
+ sheets.push(s);
69
+ }
70
+ document.adoptedStyleSheets = sheets;
71
+ }, [NO_SCROLLBAR, hide]);
72
+ await sheet(true);
73
+ try {
74
+ return await locator.screenshot({ type, timeout: 5000 });
75
+ } finally {
76
+ await sheet(false).catch(() => {});
77
+ }
78
+ }
79
+
54
80
  // Unnamed generic nodes are layout wrappers (mostly divs): they add depth and
55
81
  // nothing to read. Drop them, lift their children, and drop cursor hints.
56
82
  function compactSnapshot(text) {
@@ -271,6 +297,8 @@ const commands = {
271
297
  // Chrome does not draw a tab that is not in front, and tabs open in the
272
298
  // background, so the tab is brought to the front for the shot.
273
299
  await state.page.bringToFront();
300
+ // A tab brought to the front before can be drawn at the window's size since its last navigation.
301
+ await reapplyMetrics(state.page);
274
302
  let image;
275
303
  const device = tabState(state.page).emulation?.device;
276
304
  try {
@@ -278,7 +306,7 @@ const commands = {
278
306
  image = await deviceShot(device, { ref, full, type });
279
307
  } else if (ref) {
280
308
  const selector = toSelector(ref);
281
- image = await onElement(selector, () => state.page.locator(selector).screenshot({ type, timeout: 5000 }));
309
+ image = await onElement(selector, () => elementShot(state.page.locator(selector), type));
282
310
  } else {
283
311
  image = await state.page.screenshot({ fullPage: full, type });
284
312
  }
package/lib/modes.js CHANGED
@@ -9,7 +9,7 @@ const { emulatedKinds, setEmulation, setViewport } = require('./emulation');
9
9
  const { recordingOf, finishRecording } = require('./record');
10
10
  const { currentCapture, endCapture } = require('./capture');
11
11
  const { nextTabWanted, waitingText, stopWaitingForNextTab, stopWatching } = require('./watch');
12
- const { hideHighlights } = require('./elements');
12
+ const { hideHighlights, setLabels } = require('./elements');
13
13
  const { cursorOf, cursorOff } = require('./cursor');
14
14
 
15
15
  // The modes turned on in a tab, in the order the prompt shows them.
@@ -26,6 +26,7 @@ function activeModes(p) {
26
26
  if (routes) modes.push(`routes:${routes}`);
27
27
  const highlights = tabState(p).highlights?.size;
28
28
  if (highlights) modes.push(`highlight:${highlights}`);
29
+ if (tabState(p).labelsOn) modes.push('labels');
29
30
  if (cursorOf(p)) modes.push('cursor');
30
31
  if (currentCapture()?.page === p) modes.push('capture');
31
32
  if (recordingOf(p)) modes.push('record');
@@ -76,6 +77,8 @@ function modeOwners(p) {
76
77
  if (viewport) owners.push([viewport.owner ?? null, 'viewport']);
77
78
  for (const [glob, route] of tabState(p).routes || []) owners.push([route.owner ?? null, `route ${glob}`]);
78
79
  for (const [selector, h] of tabState(p).highlights || []) owners.push([h.owner ?? null, `highlight ${selector}`]);
80
+ const labels = tabState(p).labelsOn;
81
+ if (labels) owners.push([labels.owner ?? null, 'labels']);
79
82
  const cursor = cursorOf(p);
80
83
  if (cursor) owners.push([cursor.owner ?? null, 'cursor']);
81
84
  const capture = currentCapture();
@@ -114,6 +117,7 @@ async function allModesOff(mine = false) {
114
117
  if ('network' in tabState(p) && ours(tabState(p).network.owner)) { await setNetwork(p, null); done.push('network on'); }
115
118
  const hidden = await hideHighlights(p, mine);
116
119
  if (hidden) done.push(`${hidden} highlight${hidden === 1 ? '' : 's'} hidden`);
120
+ if (tabState(p).labelsOn && ours(tabState(p).labelsOn.owner)) { await setLabels(p, false); done.push('labels off'); }
117
121
  if (cursorOf(p) && ours(cursorOf(p).owner) && await cursorOff(p)) done.push('cursor off');
118
122
  // Before the page's size is reset, which the recording would otherwise end with.
119
123
  const recording = recordingOf(p);
package/lib/record.js CHANGED
@@ -8,8 +8,7 @@ const out = require('./output');
8
8
  const { tabState } = require('./tabstate');
9
9
  const { clock, NO_TAB, STOPPED_RECORDING, ENDED_UNSEEN } = require('./util');
10
10
  const { shellWords, splitSelector } = require('./syntax');
11
- const { keptSession } = require('./cdp');
12
- const { deviceMetrics, viewportMetrics, settled } = require('./emulation');
11
+ const { reapplyMetrics } = require('./emulation');
13
12
 
14
13
  const FPS = 25;
15
14
  const MAX_SECONDS = 3600;
@@ -134,13 +133,7 @@ function recordingOf(p) {
134
133
 
135
134
  async function restoreMetrics(p, rec) {
136
135
  if (rec?.finishing) return;
137
- const tab = tabState(p);
138
- if (!tab.viewport && !tab.emulation?.device) return;
139
- const kept = await keptSession(p);
140
- if (rec?.finishing) return;
141
- if (tab.viewport) await viewportMetrics(kept, tab.viewport);
142
- else if (tab.emulation?.device) await deviceMetrics(kept, tab.emulation.device);
143
- await settled(p);
136
+ await reapplyMetrics(p);
144
137
  }
145
138
 
146
139
  // The tabs recording now, for the modes they count as.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pw-repl",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
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
@@ -15,16 +15,18 @@ shared session: a person can have their own tabs open in it and be using it whil
15
15
  `npx pw-repl@latest` in its place, in that same form for every command, so each one runs the same
16
16
  version; from a clone, `<clone>/bin/pw-repl.js`.
17
17
 
18
- `pw-repl where` also names the skill of the pw-repl you run: `skill: e5f6a7b8 (pw-repl 0.4.3)`. When that
19
- is not the skill named at the top of this file, tell the user before going on:
18
+ `pw-repl where` also names the skill of the pw-repl you run: `skill: e5f6a7b8 (pw-repl 0.4.3)`. When
19
+ that is not the skill named at the top of this file, tell the user once, in these words and in passing,
20
+ with whatever else you are telling them, and carry on with the task:
20
21
 
21
- - if this file's pw-repl version is newer than that one, pw-repl is behind: say "⚠️ pw-repl is older
22
- than this skill (pw-repl <that version>, skill from <this file's>); `npx pw-repl@latest` or
22
+ - if this file's pw-repl version is newer than that one, pw-repl is behind: "🔄 pw-repl is older than
23
+ this skill (pw-repl <that version>, skill from <this file's>); `npx pw-repl@latest` or
23
24
  `npm install -g pw-repl` updates it."
24
- - otherwise, the skill is: ask "⚠️ This pw-repl skill is out of date (from pw-repl <this file's
25
- version>; <that version> has a newer one). Update it? Everything but its Custom rules section will be
26
- replaced." On a yes, save what `pw-repl skill` prints over this file, with this file's Custom rules
27
- section in place of the new one's.
25
+ - otherwise, the skill is: "🔄 This pw-repl skill is out of date (from pw-repl <this file's version>;
26
+ <that version> has a newer one). If you'd like me to update it, just ask: everything but its Custom
27
+ rules section would be replaced." Update it only when the user asks for that; a yes to something else
28
+ is not one. To update it, save what `pw-repl skill` prints over this file, with this file's Custom
29
+ rules section in place of the new one's.
28
30
 
29
31
  ## Arguments
30
32
 
@@ -34,11 +36,12 @@ the sections below are how to carry it out. With no args, get a REPL running and
34
36
 
35
37
  ## Start it
36
38
 
37
- `pw-repl where` says whether a REPL is running and how `send` reaches it, or that one is still starting
38
- (retry shortly; it exits 64 until the REPL serves, as when none is running). Right after you start one,
39
- for a moment before its process is up, it can still say none is there: retry for a few seconds before
40
- taking that as a failed start. There are three ways to run one; each takes an optional start URL, which
41
- opens in a new tab.
39
+ Run `pw-repl where` first, before anything else (with `-e <socket>` for a REPL you were told is on one),
40
+ and check the skill it names against the top of this file. It says whether a REPL is running and how
41
+ `send` reaches it, or that one is still starting (retry shortly; it exits 64 until the REPL serves, as
42
+ when none is running). Right after you start one, for a moment before its process is up, it can still
43
+ say none is there: retry for a few seconds before taking that as a failed start. There are three ways to
44
+ run one; each takes an optional start URL, which opens in a new tab.
42
45
 
43
46
  - `pw-repl serve --background` runs it detached, with a command server on `/tmp/playwright-repl.sock`
44
47
  (owner-only) and its output in `/tmp/playwright-repl.log`. `pw-repl attach` shows everything it does
@@ -212,8 +215,8 @@ pw-repl send -c <name> close # your modes off; tab close
212
215
  `modes off` turns off every mode in every tab.
213
216
  - Someone may be attached to the REPL's tmux session; killing the session ends it for them too.
214
217
  - To point something out, to the person or in a screenshot, `highlight <ref|selector>` draws a box over it
215
- without changing the page (`--style=<css>` restyles it; `highlight off` hides them). Its label can
216
- cover what is below it; `help highlight` says how to mark a screenshot without one.
218
+ without changing the page (`--style=<css>` restyles it; `highlight off` hides them), without
219
+ Playwright's locator label unless `highlight --labels on`.
217
220
  - For a video, `help video` says how: a take written as a file and sent with `send --file`, `record on`
218
221
  around it, `cursor on` for a pointer, and the pacing recording adds for the viewer; a screen recorder on
219
222
  the user's machine records what they see.