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 +6 -4
- package/lib/cursor.js +15 -3
- package/lib/elements.js +88 -10
- package/lib/emulation.js +13 -1
- package/lib/help.js +23 -10
- package/lib/inspect.js +30 -2
- package/lib/modes.js +5 -1
- package/lib/record.js +2 -9
- package/package.json +1 -1
- package/skill/SKILL.md +18 -15
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
347
|
-
|
|
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>
|
|
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
|
|
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.
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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)
|
|
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.
|
|
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)
|
|
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 {
|
|
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
|
-
|
|
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
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
|
|
19
|
-
is not the skill named at the top of this file, tell the user
|
|
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:
|
|
22
|
-
|
|
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:
|
|
25
|
-
|
|
26
|
-
replaced."
|
|
27
|
-
|
|
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`
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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)
|
|
216
|
-
|
|
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.
|