pw-repl 0.13.0 → 0.14.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/AGENTS.md +12 -34
- package/README.md +7 -2
- package/lib/elements.js +21 -6
- package/lib/help.js +22 -6
- package/lib/inspect.js +14 -2
- package/lib/send.js +3 -2
- package/lib/syntax.js +4 -1
- package/lib/tabs.js +6 -0
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -56,48 +56,24 @@ If you hit a limitation or write a workaround, consider adding the capability to
|
|
|
56
56
|
after an intended change to it, save it again (the command is in `test/units.test.js`). The common
|
|
57
57
|
tasks and modes on help's front page (`TASKS` and `MODES`) are the README's too: change both, as a
|
|
58
58
|
unit test compares them.
|
|
59
|
-
- Time each full run, and put the result in the body of the commit it tests (`npm test: 151 tests, 79s`)
|
|
60
|
-
Compare with the last commit that has one (`git log
|
|
61
|
-
|
|
59
|
+
- Time each full run, and put the result in the body of the commit it tests (`npm test: 151 tests, 79s`),
|
|
60
|
+
with the load average (`uptime`) when it is high. Compare with the last commit that has one (`git log
|
|
61
|
+
--grep='npm test:'`): a run that grew by more than a few seconds, at a like load (a busy machine slows
|
|
62
|
+
every test), means a slow test crept in, usually one waiting out a real timeout (5s for a click).
|
|
62
63
|
Find it (each test prints its time) and make it faster, or say why it can't be, before committing.
|
|
63
64
|
- Don't race the clock in a test: a page that changes on a timer, or a command timed against a budget,
|
|
64
65
|
fails on a busy machine. Have the page change when the test asks (eval), or measure the thing itself
|
|
65
66
|
(frames drawn, not clicks timed). A click waits for its element to hold still for two frames.
|
|
66
67
|
- For anything the tests can't reach, exercise the change in a running REPL of your own: on a socket of
|
|
67
68
|
your own, with `--launch` unless the change needs the user's browser, and then in a tab of your own.
|
|
69
|
+
One started before a change to `lib/` runs the old code: restart it after a change. Stop a process by
|
|
70
|
+
its pid, after checking what it is; a broad `pkill -f` pattern can match your own shell, or a REPL
|
|
71
|
+
someone else is using.
|
|
68
72
|
- `skill/SKILL.md` is how agents learn to use the REPL: keep it in step with a change to how it is
|
|
69
73
|
used, and leave its Custom rules section empty. Keep its `<!-- pw-repl skill stamp -->` line: `pw-repl
|
|
70
74
|
skill` replaces it with the version and a hash of the skill's text, which `where` reports, so agents
|
|
71
75
|
can tell a saved copy is out of date.
|
|
72
76
|
|
|
73
|
-
## Waves
|
|
74
|
-
|
|
75
|
-
Work goes in waves, and each one ends with a release that is ready to push.
|
|
76
|
-
|
|
77
|
-
1. Build what was asked, with tests, and try it in a running REPL. For anything the user will look at
|
|
78
|
-
(a cursor, an overlay, a video), show them a demo while it is still a prototype (a recording or
|
|
79
|
-
screenshots they can open) and get their go-ahead on how it looks before writing tests or
|
|
80
|
-
building on it.
|
|
81
|
-
2. Try it on a fresh agent that has not seen the code or the change (a guinea pig), on something real: a
|
|
82
|
-
small local page whose planted bugs need the change to find.
|
|
83
|
-
- Brief it with goals only: what to find out or do in the browser, never the commands.
|
|
84
|
-
- First it reads back its plan, learned from `pw-repl skill` and help alone: the commands, where it
|
|
85
|
-
found them, and what is unclear. It runs nothing yet.
|
|
86
|
-
- A wrong or unsure plan means the docs or the brief were unclear: find out which, fix that, then
|
|
87
|
-
ask again.
|
|
88
|
-
- Then it carries the plan out, in a browser and on a socket of its own, and reports as it goes:
|
|
89
|
-
above all, where the tool did something other than what the docs led it to expect.
|
|
90
|
-
- Once it has confirmed the fixes work, it reviews them as a second pair of eyes: anything important
|
|
91
|
-
they miss or get wrong, not nitpicks.
|
|
92
|
-
3. Triage what it reports. Fix what matters, and what is small and useful; don't put off something
|
|
93
|
-
useful for later. Leave trivia alone, and say why.
|
|
94
|
-
4. Repeat 2 and 3, with a fresh agent each time, until a pass comes back clean: no real bugs, and
|
|
95
|
-
nothing unclear in the docs or the brief that changed the plan. A pass after fixes tries the fixes,
|
|
96
|
-
and something not tried yet.
|
|
97
|
-
5. Only then cut the release (Releasing, steps 1-3), and say it is ready. The maintainer pushes and
|
|
98
|
-
publishes it, and the next wave starts from what they find. A release cut too early and not pushed
|
|
99
|
-
yet is undone (its commit and tag) and cut again after the fixes.
|
|
100
|
-
|
|
101
77
|
## Releasing
|
|
102
78
|
|
|
103
79
|
Commits follow [Conventional Commits](https://www.conventionalcommits.org/): `feat:`, `fix:`, `docs:`,
|
|
@@ -106,9 +82,9 @@ Commits follow [Conventional Commits](https://www.conventionalcommits.org/): `fe
|
|
|
106
82
|
|
|
107
83
|
1. `npm test` passes and the working tree is clean.
|
|
108
84
|
2. `npm outdated` and `npm audit` show nothing that needs doing first. Dependencies are always the newest
|
|
109
|
-
stable release, never pinned back
|
|
110
|
-
|
|
111
|
-
|
|
85
|
+
stable release, never pinned back. When the newest breaks a test, fix it in our code; only a
|
|
86
|
+
regression of the dependency's own, with the fix already in its next release, keeps the version before
|
|
87
|
+
it, until that release ships. playwright-core stays at 1.63 for that reason: on 1.64.1 a click
|
|
112
88
|
on a link to a 204 hangs at "waiting for scheduled navigations to finish" until its timeout, in plain
|
|
113
89
|
Playwright too (chromium.launch, a page with a link to a 204, page.click on it); 1.65's alphas fix it.
|
|
114
90
|
The test "waits for the page to load, counting a navigation that just began" shows it: move up once it
|
|
@@ -118,4 +94,6 @@ Commits follow [Conventional Commits](https://www.conventionalcommits.org/): `fe
|
|
|
118
94
|
4. `git push --follow-tags` pushes the commits and the tag.
|
|
119
95
|
5. `npm publish` publishes it; `prepublishOnly` runs the tests again first.
|
|
120
96
|
|
|
97
|
+
A release cut too early and not pushed yet is undone (its commit and tag) and cut again after the fixes.
|
|
98
|
+
|
|
121
99
|
Pushing and publishing need the maintainer's GitHub and npm credentials.
|
package/README.md
CHANGED
|
@@ -199,7 +199,10 @@ pw-repl where # which REPL send would reach
|
|
|
199
199
|
|
|
200
200
|
`pw-repl send` sends one command and prints its result, through the server when one is running, and
|
|
201
201
|
through the `pw-repl` tmux session otherwise. Chain several in the shell: `&&` stops at the first that
|
|
202
|
-
fails or is not confirmed (exit 1 or 2), and `;` carries on.
|
|
202
|
+
fails or is not confirmed (exit 1 or 2), and `;` carries on. `-t` is how long it waits for the answer
|
|
203
|
+
(default 20s). Times, there and in every command, are in milliseconds, as in Playwright, or in seconds
|
|
204
|
+
with an `s`: `-t 90000` or `-t 90s`. A send that gives up cancels its command: one still queued never
|
|
205
|
+
runs, and one running is no longer waited for.
|
|
203
206
|
|
|
204
207
|
Several agents can share one REPL. `pw-repl send -c <name>` (or `PW_CLIENT=<name>`) sends as a client
|
|
205
208
|
with a selected tab of its own, shown in the REPL as `[server:<name>]`. `modes` says which client turned
|
|
@@ -227,7 +230,8 @@ The body may also have `"client": "<name>"`, and `"vars": {"PW_NAME": "<value>"}
|
|
|
227
230
|
"error", "output": "..."}`, plus `"unconfirmed": true` when the command may or may not have done what it
|
|
228
231
|
was sent to do (it timed out, or the REPL quit while it ran and it was not read-only). While the REPL
|
|
229
232
|
starts, `/run` answers 503 with `"starting": true`: retry. `GET /health` says `"status": "starting"` until
|
|
230
|
-
it serves, then `"ok"`.
|
|
233
|
+
it serves, then `"ok"`. A client that closes its connection before the answer cancels the command, as
|
|
234
|
+
`send` does when it gives up. `pw-repl serve <port>` serves TCP on 127.0.0.1; other addresses are refused.
|
|
231
235
|
|
|
232
236
|
## How it differs from playwright-cli
|
|
233
237
|
|
|
@@ -237,6 +241,7 @@ playwright-cli's command names and most of their options work here too (`help pl
|
|
|
237
241
|
them). Where pw-repl differs, it is mostly because someone else may be using the browser:
|
|
238
242
|
|
|
239
243
|
- `open` and `close` open a tab and turn off your modes; they never start, close or wipe a browser.
|
|
244
|
+
`close` also lists the files you wrote that are still there.
|
|
240
245
|
- Routes, network and emulation apply to one tab, not the whole browser.
|
|
241
246
|
- File pickers are not caught, since they may be the user's: `upload` names the input instead.
|
|
242
247
|
- There is no snapshot after every command, since the user reads the REPL too; `snapshot` shows one.
|
package/lib/elements.js
CHANGED
|
@@ -39,7 +39,7 @@ const MODIFIERS = ['Alt', 'Control', 'ControlOrMeta', 'Meta', 'Shift'];
|
|
|
39
39
|
// click takes --click-count too; dblclick is click's count of 2.
|
|
40
40
|
function clickArgs(args, usage, counts = false) {
|
|
41
41
|
// Found outside quotes only, so a quoted selector keeps whatever is in it.
|
|
42
|
-
const { found, rest: text } = takeOptions((args || '').trim(), { modifiers: 'value', button: 'value', 'click-count': 'value' });
|
|
42
|
+
const { found, rest: text } = takeOptions((args || '').trim(), { modifiers: 'value', button: 'value', 'click-count': 'value', force: 'flag' });
|
|
43
43
|
if (!counts && found.some(o => o.name === 'click-count')) throw new Error('dblclick is a click count of 2; for another count, click --click-count=<n>');
|
|
44
44
|
const clickCount = clickCountOf(found, usage);
|
|
45
45
|
const modifiers = found.filter(o => o.name === 'modifiers').flatMap(o => (o.value || '').split(',').filter(Boolean).map(keyName));
|
|
@@ -49,12 +49,14 @@ function clickArgs(args, usage, counts = false) {
|
|
|
49
49
|
const positional = parsed && /^(?:left|right|middle)$/.test(parsed.rest || '') && (REF.test(parsed.word) || /^["']/.test(text)) ? parsed.rest : null;
|
|
50
50
|
const button = chooseButton(found, positional, usage);
|
|
51
51
|
const selector = positional ? parsed.selector : soleSelector(text, usage);
|
|
52
|
-
const
|
|
52
|
+
const force = found.some(o => o.name === 'force');
|
|
53
|
+
const options = { ...(button ? { button } : {}), ...(modifiers.length ? { modifiers } : {}), ...(clickCount > 1 ? { clickCount } : {}), ...(force ? { force } : {}) };
|
|
53
54
|
// The selector as written, then the kind of click, so an option never reads as part of the selector.
|
|
54
55
|
const kind = [
|
|
55
56
|
...(button && button !== 'left' ? [`${button} button`] : []),
|
|
56
57
|
...(clickCount > 1 ? [`${clickCount} times`] : []),
|
|
57
58
|
...(modifiers.length ? [`with ${modifiers.join('+')}`] : []),
|
|
59
|
+
...(force ? ['forced'] : []),
|
|
58
60
|
];
|
|
59
61
|
return { selector, shown: [positional ? parsed.word : unquote(text), ...kind].join(', '), options };
|
|
60
62
|
}
|
|
@@ -104,8 +106,9 @@ async function onElement(selector, action, budget = 5000) {
|
|
|
104
106
|
console.error(`Playwright's call log:\n${message}`);
|
|
105
107
|
// Found, so the ref is not stale: no hint.
|
|
106
108
|
const summary = new Error(`Timed out after ${budget / 1000}s on ${ref || selector}: ${lastBlocker(message)} (its call log is in the REPL's pane or log)`);
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
+
// Unknown only once input was sent, in any retry. Playwright's checks (visible, enabled, stable, and
|
|
110
|
+
// an element covering the point) come before it logs "performing <x> action" and sends it.
|
|
111
|
+
summary.uncertain = /performing \w+ action/.test(message);
|
|
109
112
|
throw summary;
|
|
110
113
|
}
|
|
111
114
|
}
|
|
@@ -227,6 +230,18 @@ function mouseButton(args, name) {
|
|
|
227
230
|
// A page that renders the chosen element again (as React lists do) replaces it before it is
|
|
228
231
|
// acted on; it is chosen again then, as Playwright finds a locator's element again.
|
|
229
232
|
async function pointerAction(selector, act, options = {}, clicks = 0) {
|
|
233
|
+
try {
|
|
234
|
+
return await choosingPointerAction(selector, act, options, clicks);
|
|
235
|
+
} catch (error) {
|
|
236
|
+
// Playwright counts aria-disabled as disabled, though a page may keep such an element clickable.
|
|
237
|
+
if (clicks && !options.force && /: element is not enabled \(/.test(error.message)) {
|
|
238
|
+
error.message += '\nIt may be aria-disabled, which counts as disabled; --force clicks it anyway (help click).';
|
|
239
|
+
}
|
|
240
|
+
throw error;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function choosingPointerAction(selector, act, options, clicks) {
|
|
230
245
|
const TIMEOUT = 5000;
|
|
231
246
|
const deadline = Date.now() + TIMEOUT;
|
|
232
247
|
const left = () => Math.max(1000, deadline - Date.now());
|
|
@@ -426,13 +441,13 @@ function dateInputHolds(kind, value, what, selector) {
|
|
|
426
441
|
|
|
427
442
|
const commands = {
|
|
428
443
|
async click(args) {
|
|
429
|
-
const { selector, shown, options } = clickArgs(args, 'click <selector> [left|right|middle] [--button=<button>] [--click-count=<n>] [--modifiers=<key>[,<key>]]', true);
|
|
444
|
+
const { selector, shown, options } = clickArgs(args, 'click <selector> [left|right|middle] [--button=<button>] [--click-count=<n>] [--modifiers=<key>[,<key>]] [--force]', true);
|
|
430
445
|
const which = await pointerAction(selector, (on, sel, o) => (sel ? on.click(sel, o) : on.click(o)), options, options.clickCount || 1);
|
|
431
446
|
out.log(`Clicked: ${shown}${which}`);
|
|
432
447
|
},
|
|
433
448
|
|
|
434
449
|
async dblclick(args) {
|
|
435
|
-
const { selector, shown, options } = clickArgs(args, 'dblclick <selector> [left|right|middle] [--button=<button>] [--modifiers=<key>[,<key>]]');
|
|
450
|
+
const { selector, shown, options } = clickArgs(args, 'dblclick <selector> [left|right|middle] [--button=<button>] [--modifiers=<key>[,<key>]] [--force]');
|
|
436
451
|
const which = await pointerAction(selector, (on, sel, o) => (sel ? on.dblclick(sel, o) : on.dblclick(o)), options, 2);
|
|
437
452
|
out.log(`Double-clicked: ${shown}${which}`);
|
|
438
453
|
},
|
package/lib/help.js
CHANGED
|
@@ -216,6 +216,7 @@ afresh.`,
|
|
|
216
216
|
['--button=<button>', 'the button, after any selector: click #menu --button=right. Over send, the shell takes the quotes, so pw-repl send click \'#menu\' right clicks a selector "#menu right": use --button, or quote it twice (\'"#menu"\').'],
|
|
217
217
|
['--click-count=<n>', 'clicks this many times at once, as Playwright\'s clickCount: 2 is a double-click (as dblclick), 3 a triple-click, which selects a line of text or all of a field\'s. 1 to 3. Default: 1.'],
|
|
218
218
|
['--modifiers=<keys>', 'keys held down meanwhile, comma-separated: Alt, Control, ControlOrMeta, Meta, Shift.'],
|
|
219
|
+
['--force', 'skips Playwright\'s actionability checks (enabled, stable, not covered), as Playwright\'s force. For an element marked aria-disabled, which Playwright counts as disabled, that a page keeps clickable (a greyed menu item that says why). Several matches are still chosen from as below; a covered ref or single match is clicked where it is, so what covers it gets the click.'],
|
|
219
220
|
],
|
|
220
221
|
detail: `Several matches: hidden ones and ones covered by something else (a dialog's overlay) are passed over,
|
|
221
222
|
and it says which it used: Clicked: text=Buy (match 3 of 4; ...), or names the first when it was the
|
|
@@ -223,8 +224,10 @@ one: (the first of 2 matches: <h1> Buy), as it may not be the one meant. If none
|
|
|
223
224
|
says why for each and does nothing. A ref, or a single match, is clicked as it is, waiting up to 5s for
|
|
224
225
|
it to be ready.
|
|
225
226
|
|
|
226
|
-
Timed out
|
|
227
|
-
|
|
227
|
+
Timed out, it says what stopped it, and Playwright's full call log goes to the REPL's pane or log. Stopped by
|
|
228
|
+
one of Playwright's checks before the click is sent, it did nothing: the element is not visible, enabled
|
|
229
|
+
or stable, or <div class="overlay"> intercepts pointer events (something covers it). Stopped after it was
|
|
230
|
+
sent, as when the page stops answering, the outcome is unknown.`,
|
|
228
231
|
},
|
|
229
232
|
dblclick: {
|
|
230
233
|
usage: 'dblclick <selector> [button]',
|
|
@@ -533,7 +536,7 @@ circle). That changes the page, so put it back afterwards, as it was if it had a
|
|
|
533
536
|
summary: 'save a PNG of the viewport, --full page or an element',
|
|
534
537
|
args: [
|
|
535
538
|
['[<ref>]', 'only that element, from a snapshot ref: screenshot e5.'],
|
|
536
|
-
['[name]', 'saved as screenshot-<name>.png; letters, digits, underscores and hyphens. Default: a timestamp.'],
|
|
539
|
+
['[name]', 'saved as screenshot-<name>.png; letters, digits, underscores and hyphens. Default: a timestamp. A word with a / or ending in .png, .jpg or .jpeg is a file instead, as --filename: x.png is the file x.png, not screenshot-x.png. At the prompt, a path with a space goes in --filename="<file>"; through pw-repl send, either works.'],
|
|
537
540
|
],
|
|
538
541
|
options: [
|
|
539
542
|
['--full', 'the whole page, not only the viewport; --full-page too.'],
|
|
@@ -915,7 +918,7 @@ window.__done until it is set.`,
|
|
|
915
918
|
},
|
|
916
919
|
cdp: {
|
|
917
920
|
usage: 'cdp [--all] <method> <JSON object>',
|
|
918
|
-
summary: 'send one CDP command
|
|
921
|
+
summary: 'send one CDP command to the selected tab',
|
|
919
922
|
args: [
|
|
920
923
|
['<method>', 'a CDP method: Network.getCookies.'],
|
|
921
924
|
['<JSON object>', 'its parameters: {} for none.'],
|
|
@@ -923,8 +926,21 @@ window.__done until it is set.`,
|
|
|
923
926
|
options: [
|
|
924
927
|
['--all', 'the whole result, not capped.'],
|
|
925
928
|
],
|
|
926
|
-
detail: `
|
|
927
|
-
Target.closeTarget are refused, and so is Page.handleJavaScriptDialog: dialog
|
|
929
|
+
detail: `A command goes through a temporary session, detached afterwards, so subscriptions and settings do not
|
|
930
|
+
persist (input aside, below). Browser.close and Target.closeTarget are refused, and so is Page.handleJavaScriptDialog: dialog
|
|
931
|
+
answers one.
|
|
932
|
+
|
|
933
|
+
Input (Input.dispatchTouchEvent, dispatchMouseEvent, the synthesize gestures): through a session kept for
|
|
934
|
+
the tab, so a sequence sent one call at a time stays one sequence (Chrome drops it when the session that
|
|
935
|
+
began it detaches). End one before other commands (touchEnd, or touchCancel to drop it): left open, the
|
|
936
|
+
next click lands mid-touch. Input.setIgnoreInputEvents and setInterceptDrags go through a temporary session,
|
|
937
|
+
as they would otherwise stay on and break later clicks.
|
|
938
|
+
|
|
939
|
+
Touch (under emulate mobile, for pointerType touch): touchStart and touchEnd sent this way make a tap, with
|
|
940
|
+
click, and two in a row a dblclick; held, it is still a tap, with no contextmenu. Chrome takes a tap sent
|
|
941
|
+
soon after another gesture as part of it, with no click: up to a few seconds after a drag (sleep 3s). The synthesize gestures
|
|
942
|
+
(Input.synthesizeTapGesture, synthesizePinchGesture, synthesizeScrollGesture) send pointer and touch events
|
|
943
|
+
but no click, dblclick or contextmenu.`,
|
|
928
944
|
},
|
|
929
945
|
clipboard: {
|
|
930
946
|
usage: 'clipboard [--all] [--shared-clipboard]',
|
package/lib/inspect.js
CHANGED
|
@@ -5,7 +5,7 @@ const { state, withTimeout } = require('./state');
|
|
|
5
5
|
const out = require('./output');
|
|
6
6
|
const { printOutput } = out;
|
|
7
7
|
const { devices } = require('playwright-core');
|
|
8
|
-
const { toSelector, REF, unquote } = require('./syntax');
|
|
8
|
+
const { toSelector, REF, SCREENSHOT_FILE, unquote } = require('./syntax');
|
|
9
9
|
const { regexOf, COMMAND_TIMEOUT, BUSY_PAGE, TIME, parseDuration, durationText } = require('./util');
|
|
10
10
|
const { tabState } = require('./tabstate');
|
|
11
11
|
const { noteWritten } = require('./written');
|
|
@@ -316,7 +316,7 @@ const commands = {
|
|
|
316
316
|
},
|
|
317
317
|
|
|
318
318
|
async screenshot(args) {
|
|
319
|
-
const usage = 'Usage: screenshot [<ref>] [--full] [--delay|-d <time>] [name | --filename=<file>]\nNames may contain letters, digits, underscores, and hyphens.';
|
|
319
|
+
const usage = 'Usage: screenshot [<ref>] [--full] [--delay|-d <time>] [name | <file> | --filename=<file>]\nNames may contain letters, digits, underscores, and hyphens; a word with a / or ending in .png, .jpg or .jpeg is a file.';
|
|
320
320
|
// The file first: a path may be quoted, with spaces in it.
|
|
321
321
|
let file = null;
|
|
322
322
|
const rest = (args || '').replace(/(?:^|\s)--filename(?:=|\s+)("(?:[^"\\]|\\.)*"|'[^']*'|\S+)/, (_, value) => { file = unquote(value); return ' '; });
|
|
@@ -339,6 +339,11 @@ const commands = {
|
|
|
339
339
|
if (delay === null || delay > SCREENSHOT_DELAY_MAX) throw new Error(`Usage: screenshot --delay <time> (ms, or 5s; at most ${durationText(SCREENSHOT_DELAY_MAX)})`);
|
|
340
340
|
continue;
|
|
341
341
|
}
|
|
342
|
+
if (SCREENSHOT_FILE.test(token)) {
|
|
343
|
+
if (file) throw new Error(usage);
|
|
344
|
+
file = token;
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
342
347
|
names.push(token);
|
|
343
348
|
}
|
|
344
349
|
if (names.length > 1 || (names[0] && !/^[a-zA-Z0-9_-]+$/.test(names[0])) || (file && names.length) || (ref && full)) throw new Error(usage);
|
|
@@ -599,6 +604,13 @@ const commands = {
|
|
|
599
604
|
if (match[1] === 'Page.handleJavaScriptDialog') throw new Error(CDP_REFUSED);
|
|
600
605
|
const params = JSON.parse(match[2]);
|
|
601
606
|
if (!params || Array.isArray(params) || typeof params !== 'object') throw new Error('Parameters must be a JSON object');
|
|
607
|
+
// Input sent through the tab's kept session: Chrome drops a touch or mouse sequence when the session
|
|
608
|
+
// that began it detaches. Not Input's two switches, which would stay on and break later clicks.
|
|
609
|
+
if (/^Input\./.test(match[1]) && !['Input.setIgnoreInputEvents', 'Input.setInterceptDrags'].includes(match[1])) {
|
|
610
|
+
const kept = await keptSession(state.page);
|
|
611
|
+
printOutput(await withTimeout(kept.send(match[1], params), 'CDP operation', COMMAND_TIMEOUT), all);
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
602
614
|
const operation = async () => {
|
|
603
615
|
const cdp = await state.page.context().newCDPSession(state.page);
|
|
604
616
|
try {
|
package/lib/send.js
CHANGED
|
@@ -6,7 +6,7 @@ const fs = require('fs');
|
|
|
6
6
|
const crypto = require('crypto');
|
|
7
7
|
const { execFileSync } = require('child_process');
|
|
8
8
|
const client = require('./client');
|
|
9
|
-
const { shellWords, variablesIn } = require('./syntax');
|
|
9
|
+
const { shellWords, variablesIn, SCREENSHOT_FILE } = require('./syntax');
|
|
10
10
|
|
|
11
11
|
// The prompt, after the modes on in the selected tab if any: (watch network:off) pw[serve]>
|
|
12
12
|
const PROMPT = /^(?:\([^()]*\) )?pw(\[serve\])?>\s*$/;
|
|
@@ -139,7 +139,8 @@ function commandLine(words) {
|
|
|
139
139
|
if (file) resolved = [...words.slice(0, at), `--file=${resolve(file)}`, ...rest];
|
|
140
140
|
}
|
|
141
141
|
if (words[0] === 'screenshot') {
|
|
142
|
-
|
|
142
|
+
// A file as a word of its own is sent as --filename, so a space in it stays in it.
|
|
143
|
+
resolved = words.map((w, n) => (w.startsWith('--filename=') && w.length > 11 ? `--filename=${resolve(w.slice(11))}` : words[n - 1] === '--filename' ? resolve(w) : n && !w.startsWith('-') && SCREENSHOT_FILE.test(w) ? `--filename=${resolve(w)}` : w));
|
|
143
144
|
}
|
|
144
145
|
const quoted = requote ? resolved.map(w => (w === '' || /[\s"']/.test(w) ? doubleQuote(w) : w)) : resolved;
|
|
145
146
|
// An option's value the shell kept whole stays whole too: --device='iPhone 15'.
|
package/lib/syntax.js
CHANGED
|
@@ -89,4 +89,7 @@ function expandVariables(line, values) {
|
|
|
89
89
|
});
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
// A screenshot's file given as a word of its own: a name has neither a / nor an extension.
|
|
93
|
+
const SCREENSHOT_FILE = /\/|\.(?:png|jpe?g)$/i;
|
|
94
|
+
|
|
95
|
+
module.exports = { SELECTOR_FIRST, REF, SCREENSHOT_FILE, toSelector, unescapeDouble, unquote, shellWords, splitSelector, keyName, variablesIn, expandVariables };
|
package/lib/tabs.js
CHANGED
|
@@ -21,6 +21,12 @@ async function navigating(go) {
|
|
|
21
21
|
for (let waited = 0; !find() && waited < 1000; waited += 50) await new Promise(resolve => setTimeout(resolve, 50));
|
|
22
22
|
const navigation = find();
|
|
23
23
|
if (!navigation) throw error;
|
|
24
|
+
// Chrome commits its error page just after the request fails: waited for, so info right after reads it.
|
|
25
|
+
// Capped at 1s, for a failure with no error page; a tab already on one matches at once, which info
|
|
26
|
+
// still reads right, from the log.
|
|
27
|
+
if (!stayedPut(navigation) && !/ERR_ABORTED/.test(`${navigation.status} ${navigation.failed}`)) {
|
|
28
|
+
for (let waited = 0; !state.page.url().startsWith('chrome-error://') && waited < 1000; waited += 50) await new Promise(resolve => setTimeout(resolve, 50));
|
|
29
|
+
}
|
|
24
30
|
throw new Error(notLoaded(navigation));
|
|
25
31
|
}
|
|
26
32
|
}
|