pw-repl 0.12.8 → 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 +19 -37
- package/README.md +9 -4
- package/bin/pw-repl.js +12 -10
- package/lib/background.js +2 -2
- package/lib/capture.js +11 -12
- package/lib/cli-names.js +1 -1
- package/lib/commands.js +3 -0
- package/lib/elements.js +21 -6
- package/lib/help.js +75 -33
- package/lib/inspect.js +95 -38
- package/lib/output.js +10 -3
- package/lib/profile.js +16 -14
- package/lib/record.js +17 -13
- package/lib/routes.js +8 -9
- package/lib/runner.js +28 -9
- package/lib/send.js +19 -8
- package/lib/server.js +4 -1
- package/lib/state.js +26 -9
- package/lib/syntax.js +4 -1
- package/lib/tabs.js +6 -0
- package/lib/toast.js +1 -6
- package/lib/util.js +25 -1
- package/lib/watch.js +4 -2
- package/lib/written.js +67 -0
- package/package.json +1 -1
- package/skill/SKILL.md +12 -4
package/AGENTS.md
CHANGED
|
@@ -23,7 +23,7 @@ If you hit a limitation or write a workaround, consider adding the capability to
|
|
|
23
23
|
`lib/routes.js`, `lib/network.js`, `lib/emulation.js` (and viewport), `lib/capture.js`, `lib/profile.js` (performance traces;
|
|
24
24
|
`lib/trace.js` reads and sums them up), `lib/record.js`
|
|
25
25
|
(video, through ffmpeg, and the steps file saved with it), `lib/dialogs.js` and `lib/modes.js`. Beneath them, `lib/tabstate.js` is the one record kept per tab, `lib/cdp.js` the CDP
|
|
26
|
-
session kept per tab,
|
|
26
|
+
session kept per tab, `lib/util.js` small shared helpers, and `lib/written.js` the files written and for whom. No facility requires `lib/modes.js` or
|
|
27
27
|
`lib/commands.js`, and a unit test fails on a require cycle, which CommonJS would not report.
|
|
28
28
|
- `bin/pw-repl.js` is the command line; `lib/start.js` connects and runs the prompt; `lib/runner.js`
|
|
29
29
|
runs commands one at a time (`quit` and `dialog` skip its queue); `lib/server.js` is the command
|
|
@@ -39,7 +39,7 @@ If you hit a limitation or write a workaround, consider adding the capability to
|
|
|
39
39
|
accepts theirs too (`lib/cli-names.js`, `help playwright-cli`). Deviate only where it is necessary, as
|
|
40
40
|
where sharing a browser with a user needs it, and say why in the help.
|
|
41
41
|
- Comment the *why* when it isn't obvious from the code.
|
|
42
|
-
- `npm test` runs the suite (
|
|
42
|
+
- `npm test` runs the suite (about two and a half minutes): a private headless Chromium, a local test site
|
|
43
43
|
(`test/harness.js`), and the real REPL with its server. Nothing is mocked, and the shared browser is
|
|
44
44
|
never touched. It finds Chromium through `PW_TEST_CHROME`, or where `--launch` looks (Playwright's
|
|
45
45
|
browsers, then the `PATH`), and skips the browser tests if there is none. Add a test with each new
|
|
@@ -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:`,
|
|
@@ -105,13 +81,19 @@ Commits follow [Conventional Commits](https://www.conventionalcommits.org/): `fe
|
|
|
105
81
|
1.0, a change that breaks how the REPL is used is a minor bump (0.2.0 to 0.3.0), anything else a patch.
|
|
106
82
|
|
|
107
83
|
1. `npm test` passes and the working tree is clean.
|
|
108
|
-
2. `npm outdated` and `npm audit` show nothing that needs doing first.
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
84
|
+
2. `npm outdated` and `npm audit` show nothing that needs doing first. Dependencies are always the newest
|
|
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
|
|
88
|
+
on a link to a 204 hangs at "waiting for scheduled navigations to finish" until its timeout, in plain
|
|
89
|
+
Playwright too (chromium.launch, a page with a link to a 204, page.click on it); 1.65's alphas fix it.
|
|
90
|
+
The test "waits for the page to load, counting a navigation that just began" shows it: move up once it
|
|
91
|
+
passes on the newest release.
|
|
112
92
|
3. `npm version <patch|minor|major> -m "chore: release %s"` sets the version in `package.json` and
|
|
113
93
|
`package-lock.json`, commits it as `chore: release X.Y.Z`, and tags that commit `vX.Y.Z`.
|
|
114
94
|
4. `git push --follow-tags` pushes the commits and the tag.
|
|
115
95
|
5. `npm publish` publishes it; `prepublishOnly` runs the tests again first.
|
|
116
96
|
|
|
97
|
+
A release cut too early and not pushed yet is undone (its commit and tag) and cut again after the fixes.
|
|
98
|
+
|
|
117
99
|
Pushing and publishing need the maintainer's GitHub and npm credentials.
|
package/README.md
CHANGED
|
@@ -83,7 +83,7 @@ With `npx`, use `npx pw-repl@latest` the same way every time, so every command r
|
|
|
83
83
|
whole tab's network.
|
|
84
84
|
- **Emulate:** a phone, dark mode, a locale, a timezone, a viewport size.
|
|
85
85
|
- **Run code:** `eval` any JavaScript in the page, a line or a file of it, or send a raw CDP command with
|
|
86
|
-
`cdp
|
|
86
|
+
`cdp`; read what a Copy button put on the clipboard (`clipboard`).
|
|
87
87
|
- **Wait:** for an element, some text, a response or a page load.
|
|
88
88
|
- **Record:** what someone does in a tab (`watch`), a timeline of requests and console (`capture`), or a
|
|
89
89
|
video of the tab (`record`), with a virtual pointer (`cursor`), captions (`toast`) and highlights
|
|
@@ -154,7 +154,7 @@ example the Docker bridge gateway, `ip route | awk '/default/ {print $3}'`).
|
|
|
154
154
|
| record requests and console together | `capture on`, then `capture off` |
|
|
155
155
|
| find what makes the page slow | `profile on`, then `profile off`; `profile reload` for a load |
|
|
156
156
|
| fake a backend response | `route <glob> <status> <json>`, `route <glob> abort` |
|
|
157
|
-
| change or slow an API response | `route <glob> patch <json>`, `route <glob> delay <
|
|
157
|
+
| change or slow an API response | `route <glob> patch <json>`, `route <glob> delay <time>` |
|
|
158
158
|
| cut or slow the whole network | `network off`, `network slow`, then `network on` |
|
|
159
159
|
| record a video of the page | `record on`, then `record off` (`help video` for a take) |
|
|
160
160
|
| show a pointer where the agent acts | `cursor on`, then `cursor off` |
|
|
@@ -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/bin/pw-repl.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// The one command: pw-repl run | serve | attach | stop | send | where | help | skill.
|
|
3
3
|
|
|
4
4
|
const { looksLikeEndpoint } = require('../lib/client');
|
|
5
|
+
const { longDuration } = require('../lib/util');
|
|
5
6
|
|
|
6
7
|
const USAGE = `Usage:
|
|
7
8
|
pw-repl run [--launch [--headed]] [start-url] [-- <chromium flags>]
|
|
@@ -19,7 +20,7 @@ const USAGE = `Usage:
|
|
|
19
20
|
stop a background REPL, and say what became of each recording it ended; its socket is removed, its
|
|
20
21
|
log stays
|
|
21
22
|
|
|
22
|
-
pw-repl send [-e endpoint | -s session] [-c client] [-t
|
|
23
|
+
pw-repl send [-e endpoint | -s session] [-c client] [-t time] [--no-vars]
|
|
23
24
|
<command...> | --file <file>
|
|
24
25
|
run one command in a running REPL and print its output; --file runs a file of them, one per line
|
|
25
26
|
|
|
@@ -56,12 +57,14 @@ serves on that same one: $PW_ENDPOINT, then $PW_SOCKET, then the default.
|
|
|
56
57
|
send -c <client> (or $PW_CLIENT) sends as a client of the REPL's with a selected tab of its own, so
|
|
57
58
|
several agents can share one REPL; pw-repl help session has the rest.
|
|
58
59
|
|
|
59
|
-
send's options can also follow the command, as in pw-repl send open <url> -t
|
|
60
|
-
command, every word is the command's, -t
|
|
60
|
+
send's options can also follow the command, as in pw-repl send open <url> -t 30s; after a -- before the
|
|
61
|
+
command, every word is the command's, -t 30s included.
|
|
61
62
|
|
|
62
63
|
Exit status: 0 ok, 1 command error, 2 completion not confirmed, 64 usage or unreachable. -t is how long
|
|
63
|
-
send waits for the answer (default 20s); it does not
|
|
64
|
-
action waits for its element.
|
|
64
|
+
send waits for the answer, in milliseconds or with s for seconds (default 20s: 20000); it does not
|
|
65
|
+
lengthen a command's own limits, such as the 5s an action waits for its element. Over the server, giving
|
|
66
|
+
up cancels the command: one still queued is never run, and one running is no longer waited for, though
|
|
67
|
+
what it began in the page may go on.
|
|
65
68
|
|
|
66
69
|
--file <file> runs each line of a file as a command, in order, as if typed at the REPL's prompt: quoted
|
|
67
70
|
as the prompt reads it (no shell), with a file it records, uploads or saves relative to this folder, as
|
|
@@ -105,10 +108,10 @@ function shellLine(words) {
|
|
|
105
108
|
}
|
|
106
109
|
|
|
107
110
|
// -e, -s, -c and -t, then the command words (unquoted words are one command), and send's options again
|
|
108
|
-
// at the end, as an agent types them (send open <url> -t
|
|
111
|
+
// at the end, as an agent types them (send open <url> -t 30s); after --, every word is the command.
|
|
109
112
|
function parseSendArgs(args, subcommand) {
|
|
110
113
|
const allowCommand = subcommand === 'send';
|
|
111
|
-
const options = { endpoint: null, session: null, client: null, timeout:
|
|
114
|
+
const options = { endpoint: null, session: null, client: null, timeout: 20000, command: '', file: null };
|
|
112
115
|
const given = new Set();
|
|
113
116
|
const take = (arg, value, after) => {
|
|
114
117
|
if (value === undefined) usage(`${arg} needs a value`, subcommand);
|
|
@@ -120,8 +123,7 @@ function parseSendArgs(args, subcommand) {
|
|
|
120
123
|
if (arg === '-s') options.session = value;
|
|
121
124
|
if (arg === '-c') options.client = value;
|
|
122
125
|
if (arg === '-t') {
|
|
123
|
-
|
|
124
|
-
options.timeout = Number(value);
|
|
126
|
+
try { options.timeout = longDuration(value, '-t'); } catch (error) { usage(error.message, subcommand); }
|
|
125
127
|
}
|
|
126
128
|
};
|
|
127
129
|
let i = 0;
|
|
@@ -296,7 +298,7 @@ async function main() {
|
|
|
296
298
|
case '--help':
|
|
297
299
|
return console.log(`${USAGE}\n\n${REPL_HELP}`);
|
|
298
300
|
default: {
|
|
299
|
-
// An option before the subcommand (pw-repl -t
|
|
301
|
+
// An option before the subcommand (pw-repl -t 30s send ...): say where it goes, with the line fixed.
|
|
300
302
|
if (subcommand.startsWith('-')) {
|
|
301
303
|
const all = [subcommand, ...args];
|
|
302
304
|
const named = all.find(word => SUBCOMMANDS.includes(word));
|
package/lib/background.js
CHANGED
|
@@ -5,7 +5,7 @@ const path = require('path');
|
|
|
5
5
|
const readline = require('readline');
|
|
6
6
|
const { spawn } = require('child_process');
|
|
7
7
|
const client = require('./client');
|
|
8
|
-
const { STOPPED_RECORDING, ENDED_UNSEEN } = require('./util');
|
|
8
|
+
const { STOPPED_RECORDING, ENDED_UNSEEN, FILES_LEFT } = require('./util');
|
|
9
9
|
|
|
10
10
|
const BIN = path.join(__dirname, '..', 'bin', 'pw-repl.js');
|
|
11
11
|
const START_TIMEOUT = 20000;
|
|
@@ -162,7 +162,7 @@ async function stop(options) {
|
|
|
162
162
|
fs.closeSync(fd);
|
|
163
163
|
logged = buffer.toString('utf8');
|
|
164
164
|
} catch {}
|
|
165
|
-
for (const line of logged.split('\n')) if (line.startsWith(STOPPED_RECORDING) || line.startsWith(ENDED_UNSEEN)) console.log(line);
|
|
165
|
+
for (const line of logged.split('\n')) if (line.startsWith(STOPPED_RECORDING) || line.startsWith(ENDED_UNSEEN) || line.startsWith(FILES_LEFT)) console.log(line);
|
|
166
166
|
return 0;
|
|
167
167
|
}
|
|
168
168
|
|
package/lib/capture.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
const { state, onShutdown } = require('./state');
|
|
3
3
|
const out = require('./output');
|
|
4
4
|
const { printOutput } = out;
|
|
5
|
-
const { numbers, MAX_EVENT_TEXT, NO_TAB, clock, hints } = require('./util');
|
|
5
|
+
const { numbers, MAX_EVENT_TEXT, NO_TAB, clock, hints, TIME, longDuration, durationText } = require('./util');
|
|
6
6
|
const { consoleText, requestEntries } = require('./requestlog');
|
|
7
7
|
|
|
8
8
|
const MAX_CAPTURE_EVENTS = 10000;
|
|
@@ -17,16 +17,15 @@ function currentCapture() {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
// Records the selected tab's requests and console messages together, in time
|
|
20
|
-
// order, until capture off, or for a set
|
|
20
|
+
// order, until capture off, or for a set time.
|
|
21
21
|
async function startCapture(tokens) {
|
|
22
22
|
if (cap) { out.log('Already capturing; capture off stops it.'); return; }
|
|
23
23
|
if (!state.page || state.page.isClosed()) throw new Error(NO_TAB);
|
|
24
|
-
const usage = 'Usage: capture on [requests|console] [
|
|
24
|
+
const usage = 'Usage: capture on [requests|console] [time] (ms, or 30s)';
|
|
25
25
|
const kinds = tokens.filter(t => t === 'requests' || t === 'console');
|
|
26
|
-
const
|
|
27
|
-
if (kinds.length > 1 ||
|
|
28
|
-
const
|
|
29
|
-
if (seconds !== null && (seconds < 1 || seconds > 3600)) throw new Error(usage);
|
|
26
|
+
const times = tokens.filter(t => TIME.test(t));
|
|
27
|
+
if (kinds.length > 1 || times.length > 1 || kinds.length + times.length !== tokens.length) throw new Error(usage);
|
|
28
|
+
const ms = times.length ? longDuration(times[0], 'capture on\'s time') : null;
|
|
30
29
|
const label = kinds[0] || 'requests and console';
|
|
31
30
|
const startedAt = Date.now();
|
|
32
31
|
const events = [];
|
|
@@ -61,11 +60,11 @@ async function startCapture(tokens) {
|
|
|
61
60
|
get shortened() { return shortened; },
|
|
62
61
|
};
|
|
63
62
|
cap = capture;
|
|
64
|
-
if (
|
|
65
|
-
out.log(`Capturing ${label} for ${
|
|
63
|
+
if (ms) {
|
|
64
|
+
out.log(`Capturing ${label} for ${durationText(ms)}...`);
|
|
66
65
|
// Also ends early if the capture does, e.g. because its tab closed.
|
|
67
66
|
await new Promise(resolve => {
|
|
68
|
-
const timer = setTimeout(resolve,
|
|
67
|
+
const timer = setTimeout(resolve, ms);
|
|
69
68
|
capture.wake = () => { clearTimeout(timer); resolve(); };
|
|
70
69
|
});
|
|
71
70
|
if (cap === capture) stopCapture();
|
|
@@ -120,7 +119,7 @@ const commands = {
|
|
|
120
119
|
const [sub, ...rest] = (args || '').trim().split(/\s+/).filter(Boolean);
|
|
121
120
|
if (sub === 'on') return startCapture(rest);
|
|
122
121
|
if (sub === 'off' && !rest.length) return stopCapture(all);
|
|
123
|
-
if (sub) throw new Error('Usage: capture on [requests|console] [
|
|
122
|
+
if (sub) throw new Error('Usage: capture on [requests|console] [time] | capture off');
|
|
124
123
|
if (cap) {
|
|
125
124
|
out.log(`Capturing ${cap.label} on ${cap.page.url()} since ${clock(cap.startedAt)} (${cap.events.length} so far)`);
|
|
126
125
|
out.log(hints([['capture off', 'stop and print what it recorded']]));
|
|
@@ -132,7 +131,7 @@ const commands = {
|
|
|
132
131
|
} else {
|
|
133
132
|
out.log('Not capturing.');
|
|
134
133
|
}
|
|
135
|
-
out.log(hints([['capture on [requests|console] [
|
|
134
|
+
out.log(hints([['capture on [requests|console] [time]', 'record the selected tab\'s requests and console messages in time order']]));
|
|
136
135
|
},
|
|
137
136
|
};
|
|
138
137
|
|
package/lib/cli-names.js
CHANGED
|
@@ -15,7 +15,7 @@ const NAMES = {
|
|
|
15
15
|
'tab-close': [a => (a ? null : 'tab close'), 'tab close [url-part], or tab <index> then tab close'],
|
|
16
16
|
open: [a => `tab new ${a}`.trim(), 'tab new [url] (goto <url> navigates); --mobile, --device=<name>: emulate mobile'],
|
|
17
17
|
// It ends playwright-cli's own browser; this one is shared, so tabs stay open.
|
|
18
|
-
close: [() => 'close', 'modes off --mine
|
|
18
|
+
close: [() => 'close', 'modes off --mine; tabs stay open; lists the files you wrote'],
|
|
19
19
|
'go-back': [() => 'back', 'back'],
|
|
20
20
|
'go-forward': [() => 'forward', 'forward'],
|
|
21
21
|
find: [a => `snapshot --grep ${a}`, 'snapshot --grep <text>'],
|
package/lib/commands.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// The REPL's commands, gathered from each facility's module, and what the prompt and the runner
|
|
2
2
|
// need besides: playwright-cli's own commands, the hooks every page gets, and tab completion.
|
|
3
3
|
const { state, shutdown } = require('./state');
|
|
4
|
+
const { ownFilesText } = require('./written');
|
|
4
5
|
const out = require('./output');
|
|
5
6
|
const HELP = require('./help');
|
|
6
7
|
const { tabState } = require('./tabstate');
|
|
@@ -80,6 +81,8 @@ const cliCommands = {
|
|
|
80
81
|
await allModesOff(true);
|
|
81
82
|
const own = state.browser.contexts().flatMap(c => c.pages()).filter(p => tabState(p).owner === state.client);
|
|
82
83
|
if (own.length) out.log(`Tabs you opened are still open: ${own.map(p => p.url()).join(', ')}; tab close <url-part> closes one, or select it and tab close.`);
|
|
84
|
+
const files = ownFilesText(state.client);
|
|
85
|
+
if (files) out.log(files);
|
|
83
86
|
},
|
|
84
87
|
};
|
|
85
88
|
|
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
|
},
|