pw-repl 0.12.6 → 0.12.8
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 +4 -2
- package/README.md +76 -74
- package/bin/pw-repl.js +152 -65
- package/lib/cli-names.js +3 -3
- package/lib/elements.js +115 -26
- package/lib/emulation.js +5 -3
- package/lib/help.js +118 -47
- package/lib/inspect.js +28 -6
- package/lib/launch.js +3 -1
- package/lib/record.js +1 -1
- package/lib/runner.js +11 -0
- package/lib/send.js +16 -4
- package/lib/start.js +9 -3
- package/lib/syntax.js +11 -5
- package/package.json +1 -1
- package/skill/SKILL.md +30 -42
package/lib/runner.js
CHANGED
|
@@ -38,6 +38,8 @@ const NO_TAB_NEEDED = new Set(['tab', 'modes', 'capture', 'profile', 'record', '
|
|
|
38
38
|
// Commands that accept --all, right after the command or at the end, to lift the output limit.
|
|
39
39
|
const INSPECTION = ['info', 'listeners', 'text', 'html', 'attrs', 'links', 'inputs', 'eval', 'cdp', 'cookies', 'storage', 'capture', 'profile', 'body', 'console', 'snapshot', 'watch'];
|
|
40
40
|
|
|
41
|
+
const SUBCOMMANDS = ['run', 'serve', 'attach', 'stop', 'send', 'where', 'skill'];
|
|
42
|
+
|
|
41
43
|
let queue = Promise.resolve();
|
|
42
44
|
// The server command running now, so a quit at the prompt can answer it.
|
|
43
45
|
let inFlight = null;
|
|
@@ -100,6 +102,11 @@ async function executeCommand(text) {
|
|
|
100
102
|
state.currentCommandAt = Date.now();
|
|
101
103
|
const run = Object.hasOwn(commands, cmd) ? commands[cmd] : translated && Object.hasOwn(cliCommands, cmd) ? cliCommands[cmd] : null;
|
|
102
104
|
if (!run) {
|
|
105
|
+
// An agent fresh from the skill tries send where; those are the command line's, not the REPL's.
|
|
106
|
+
if (SUBCOMMANDS.includes(cmd)) {
|
|
107
|
+
out.error(`Error: ${cmd} is a pw-repl subcommand, run at the shell: pw-repl ${cmd}${args ? ' ...' : ''}`);
|
|
108
|
+
return { status: 'error', cmd };
|
|
109
|
+
}
|
|
103
110
|
out.log(`Unknown command: ${cmd}. Type 'help' for commands.`);
|
|
104
111
|
return { status: 'error', cmd };
|
|
105
112
|
}
|
|
@@ -117,6 +124,10 @@ async function executeCommand(text) {
|
|
|
117
124
|
stepEnded(step, true);
|
|
118
125
|
// Playwright colours its call log; the codes are noise anywhere but a terminal.
|
|
119
126
|
e.message = String(e.message || '').replace(/\x1b\[[0-9;]*m/g, '');
|
|
127
|
+
// A page's own error (eval's SyntaxError, a cdp method not found) without Playwright's method name
|
|
128
|
+
// or the stack of its injected script, which point at Playwright, not at the line that was sent.
|
|
129
|
+
e.message = e.message.split('\n').filter(line => !/^\s+at /.test(line)).join('\n')
|
|
130
|
+
.replace(/^(?:page|frame|elementHandle|jsHandle|cdpSession)\.(?:evaluate|evaluateHandle|send): /, '');
|
|
120
131
|
out.error(`Error: ${e.message}`);
|
|
121
132
|
const uncertain = !READ_ONLY.has(cmd) && isUncertain(e);
|
|
122
133
|
if (uncertain) out.error('Outcome unknown: it timed out after it began acting on the page. Check the page before trying again.');
|
package/lib/send.js
CHANGED
|
@@ -88,6 +88,11 @@ async function viaTmux(session, command, timeoutMs) {
|
|
|
88
88
|
return 2;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
// A word in double quotes, as the REPL reads them (unescapeDouble): every other character goes as it is.
|
|
92
|
+
function doubleQuote(word) {
|
|
93
|
+
return `"${word.replace(/[\\"]/g, '\\$&').replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/\t/g, '\\t')}"`;
|
|
94
|
+
}
|
|
95
|
+
|
|
91
96
|
// A command's words as send was given them, made into the line the REPL reads: quoted again where the
|
|
92
97
|
// shell had kept a word whole, and with the files it reads or writes made absolute from the sender's
|
|
93
98
|
// folder, since the REPL's may be another.
|
|
@@ -100,7 +105,7 @@ function commandLine(words) {
|
|
|
100
105
|
// So does playwright-cli's storage key (localstorage-set "my key" v).
|
|
101
106
|
// And record's file, which may have spaces in it, and wait's text, whose last word would otherwise be
|
|
102
107
|
// taken as the seconds when it is a number (wait text 'Showing 1').
|
|
103
|
-
const requote = words.length > 1 && (SELECTOR_FIRST.has(words[0]) || /^(?:local|session)storage-/.test(words[0]) || words[0] === 'record' || words[0] === 'wait' || /^video-(?:start|stop)$/.test(words[0]));
|
|
108
|
+
const requote = words.length > 1 && (SELECTOR_FIRST.has(words[0]) || words[0] === 'drag' || /^(?:local|session)storage-/.test(words[0]) || words[0] === 'record' || words[0] === 'wait' || /^video-(?:start|stop)$/.test(words[0]));
|
|
104
109
|
// An empty word (fill #name "") is the empty value.
|
|
105
110
|
// upload's files are read by the REPL, whose folder may not be this one.
|
|
106
111
|
// So are the files upload reads and screenshot --filename writes.
|
|
@@ -117,15 +122,21 @@ function commandLine(words) {
|
|
|
117
122
|
// watch save and profile save write their files.
|
|
118
123
|
if (words[0] === 'profile' && words[1] === 'save' && words.length > 2) resolved = ['profile', 'save', resolve(words.slice(2).join(' '))];
|
|
119
124
|
if (words[0] === 'watch' && words[1] === 'save' && words.length > 2) resolved = ['watch', 'save', resolve(words.slice(2).join(' '))];
|
|
125
|
+
// So does the file eval --file reads.
|
|
126
|
+
if (words[0] === 'eval' && /^--file(?:=|$)/.test(words[1] || '')) {
|
|
127
|
+
// As one word, --file=<path>, which is quoted below when the path has spaces.
|
|
128
|
+
const [file, rest] = words[1] === '--file' ? [words[2], words.slice(3)] : [words[1].slice(7), words.slice(2)];
|
|
129
|
+
if (file) resolved = ['eval', `--file=${resolve(file)}`, ...rest];
|
|
130
|
+
}
|
|
120
131
|
if (words[0] === 'screenshot') {
|
|
121
132
|
resolved = words.map((w, n) => (w.startsWith('--filename=') && w.length > 11 ? `--filename=${resolve(w.slice(11))}` : words[n - 1] === '--filename' ? resolve(w) : w));
|
|
122
133
|
}
|
|
123
|
-
const quoted = requote ? resolved.map(w => (w === '' || /[\s"']/.test(w) ?
|
|
134
|
+
const quoted = requote ? resolved.map(w => (w === '' || /[\s"']/.test(w) ? doubleQuote(w) : w)) : resolved;
|
|
124
135
|
// An option's value the shell kept whole stays whole too: --device='iPhone 15'.
|
|
125
136
|
// Single quotes escape nothing, so a backslash (a regexp's \d) reaches it as typed.
|
|
126
137
|
for (const [n, w] of quoted.entries()) {
|
|
127
138
|
const option = /^(--[A-Za-z][\w-]*=)([\s\S]*\s[\s\S]*)$/.exec(w);
|
|
128
|
-
if (n && option && !requote) quoted[n] = `${option[1]}${option[2].includes("'") ?
|
|
139
|
+
if (n && option && !requote) quoted[n] = `${option[1]}${option[2].includes("'") ? doubleQuote(option[2]) : `'${option[2]}'`}`;
|
|
129
140
|
}
|
|
130
141
|
return quoted.join(' ').trim();
|
|
131
142
|
}
|
|
@@ -155,7 +166,8 @@ async function sendFile(options) {
|
|
|
155
166
|
console.log(`> ${line}`);
|
|
156
167
|
// Read as the prompt reads it; a file it reads or writes is the sender's, as on send's command line.
|
|
157
168
|
const words = shellWords(line);
|
|
158
|
-
|
|
169
|
+
// eval's own line keeps its quotes; only eval --file names a file.
|
|
170
|
+
const command = PATHS.test(words[0]) || (words[0] === 'eval' && /^--file(?:=|$)/.test(words[1] || '')) ? commandLine(words) : line;
|
|
159
171
|
const found = options.noVars ? null : variablesFor(command);
|
|
160
172
|
if (found?.error) { console.error(`pw-repl: stopped at line ${i + 1} of ${options.file} (${line}): ${found.error}`); return 1; }
|
|
161
173
|
const status = await send({ ...options, file: null, command, vars: found?.vars || null });
|
package/lib/start.js
CHANGED
|
@@ -5,6 +5,7 @@ const { state, withTimeout, shutdown, onBeforeExit, beforeExit } = require('./st
|
|
|
5
5
|
const out = require('./output');
|
|
6
6
|
const { watchPage, complete } = require('./commands');
|
|
7
7
|
const { listTabs, openTab, markStartTab } = require('./tabs');
|
|
8
|
+
const { tabState } = require('./tabstate');
|
|
8
9
|
const runner = require('./runner');
|
|
9
10
|
|
|
10
11
|
// A browser --launch starts has its own address instead.
|
|
@@ -88,10 +89,14 @@ async function start(options) {
|
|
|
88
89
|
|
|
89
90
|
if (START_URL) {
|
|
90
91
|
state.startingStep = 'opening the start URL';
|
|
91
|
-
|
|
92
|
+
// A Chromium the REPL launched has only its first blank tab, which nobody else is using: the start
|
|
93
|
+
// URL goes there, rather than into a second tab behind a blank one.
|
|
94
|
+
const first = state.launched && pages.length === 1 && pages[0].url() === 'about:blank' ? pages[0] : null;
|
|
95
|
+
if (first) Object.assign(tabState(first), { opened: true, owner: state.client });
|
|
96
|
+
state.page = first || await openTab();
|
|
92
97
|
markStartTab(state.page);
|
|
93
98
|
await state.page.goto(START_URL, { waitUntil: 'networkidle', timeout: 15000 });
|
|
94
|
-
out.log(`Opened ${state.page.url()} in a new tab`);
|
|
99
|
+
out.log(`Opened ${state.page.url()}${first ? '' : ' in a new tab'}`);
|
|
95
100
|
}
|
|
96
101
|
|
|
97
102
|
out.log('');
|
|
@@ -148,7 +153,8 @@ async function start(options) {
|
|
|
148
153
|
rl.on('close', () => {
|
|
149
154
|
if (state.stopping) return;
|
|
150
155
|
endPromptLine();
|
|
151
|
-
|
|
156
|
+
// With no terminal (an agent's shell, a pipe), input ends at once: say what runs without one.
|
|
157
|
+
out.error(process.stdin.isTTY ? 'Input ended; disconnecting.' : 'Input ended (stdin is not a terminal); disconnecting. pw-repl serve --background runs it without one.');
|
|
152
158
|
runner.drained().then(() => shutdown(), () => shutdown())
|
|
153
159
|
.then(beforeExit).then(() => process.exit(process.exitCode || 0));
|
|
154
160
|
});
|
package/lib/syntax.js
CHANGED
|
@@ -11,25 +11,31 @@ function toSelector(word) {
|
|
|
11
11
|
return REF.test(word) ? `aria-ref=${word}` : word;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
//
|
|
14
|
+
// Inside double quotes, \n, \t and \r are a newline, tab and carriage return, so a value send quotes
|
|
15
|
+
// keeps them on one line; any other \X is X (\" a quote, \\ a backslash).
|
|
16
|
+
function unescapeDouble(text) {
|
|
17
|
+
return text.replace(/\\(.)/g, (_, c) => (c === 'n' ? '\n' : c === 't' ? '\t' : c === 'r' ? '\r' : c));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Quotes around the whole of a value are removed.
|
|
15
21
|
function unquote(text) {
|
|
16
22
|
const match = /^"((?:[^"\\]|\\.)*)"$|^'([^']*)'$/.exec(text);
|
|
17
23
|
if (!match) return text;
|
|
18
|
-
return match[1] !== undefined ? match[1]
|
|
24
|
+
return match[1] !== undefined ? unescapeDouble(match[1]) : match[2];
|
|
19
25
|
}
|
|
20
26
|
|
|
21
27
|
// The words of a line as a shell reads them: quoted parts may hold spaces, and lose their quotes
|
|
22
28
|
// (--filename="my dir/a.webm" is one word, --filename=my dir/a.webm).
|
|
23
29
|
function shellWords(text) {
|
|
24
30
|
return [...(text || '').matchAll(/(?:"(?:[^"\\]|\\.)*"|'[^']*'|[^\s"']+|["'])+/g)]
|
|
25
|
-
.map(m => m[0].replace(/"((?:[^"\\]|\\.)*)"|'([^']*)'/g, (_, double, single) => (double !== undefined ? double
|
|
31
|
+
.map(m => m[0].replace(/"((?:[^"\\]|\\.)*)"|'([^']*)'/g, (_, double, single) => (double !== undefined ? unescapeDouble(double) : single)));
|
|
26
32
|
}
|
|
27
33
|
|
|
28
34
|
// The first word of args, or a quoted selector with spaces in it, and the rest of the line.
|
|
29
35
|
function splitSelector(args) {
|
|
30
36
|
const match = /^(?:"((?:[^"\\]|\\.)*)"|'([^']*)'|(\S+))(?:\s+([\s\S]*))?$/.exec((args || '').trim());
|
|
31
37
|
if (!match) return null;
|
|
32
|
-
const word = match[1] !== undefined ? match[1]
|
|
38
|
+
const word = match[1] !== undefined ? unescapeDouble(match[1]) : match[2] !== undefined ? match[2] : match[3];
|
|
33
39
|
return { word, selector: toSelector(word), rest: match[4] };
|
|
34
40
|
}
|
|
35
41
|
|
|
@@ -83,4 +89,4 @@ function expandVariables(line, values) {
|
|
|
83
89
|
});
|
|
84
90
|
}
|
|
85
91
|
|
|
86
|
-
module.exports = { SELECTOR_FIRST, REF, toSelector, unquote, shellWords, splitSelector, keyName, variablesIn, expandVariables };
|
|
92
|
+
module.exports = { SELECTOR_FIRST, REF, toSelector, unescapeDouble, unquote, shellWords, splitSelector, keyName, variablesIn, expandVariables };
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -43,7 +43,8 @@ and check the skill it names against the top of this file. It says whether a REP
|
|
|
43
43
|
`send` reaches it, or that one is still starting (retry shortly; it exits 64 until the REPL serves, as
|
|
44
44
|
when none is running). Right after you start one, for a moment before its process is up, it can still
|
|
45
45
|
say none is there: retry for a few seconds before taking that as a failed start. There are three ways to
|
|
46
|
-
run one; each takes an optional start URL, which opens in a new tab
|
|
46
|
+
run one; each takes an optional start URL, which opens in a new tab (with `--launch`, in its browser's
|
|
47
|
+
first tab).
|
|
47
48
|
|
|
48
49
|
- `pw-repl serve --background` runs it detached, with a command server on `/tmp/pw-repl.sock`
|
|
49
50
|
(owner-only) and its output in `/tmp/pw-repl.log`. `pw-repl attach` shows everything it does and
|
|
@@ -52,14 +53,14 @@ run one; each takes an optional start URL, which opens in a new tab.
|
|
|
52
53
|
- `pw-repl serve` runs the same in a terminal, where the pane shows every command. Its prompt is
|
|
53
54
|
`pw[serve]>`. It stops at that prompt (`quit`, or Ctrl-C); `pw-repl stop` is for a background one.
|
|
54
55
|
- Add `--launch` to `run` or `serve` (with or without `--background`) to have it start a Chromium of its
|
|
55
|
-
own instead of connecting to one: headless unless `--headed`, in a
|
|
56
|
-
of its own (never 9222). It stops with the REPL, Ctrl-C while it starts
|
|
57
|
-
once it has exited. It prints the command it ran; flags after `--` are
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
record.
|
|
56
|
+
own instead of connecting to one: headless unless `--headed`, in a 1280x800 window, in a temporary
|
|
57
|
+
profile, on a free port of its own (never 9222). It stops with the REPL, Ctrl-C while it starts
|
|
58
|
+
included, and `stop` returns once it has exited. It prints the command it ran; flags after `--` are
|
|
59
|
+
passed to that Chromium (only flags: a start URL goes before `--`), and a `--user-data-dir=<dir>`
|
|
60
|
+
among them is used instead of the temporary profile, and kept. Started again in a kept profile,
|
|
61
|
+
Chromium picks up where it left off, as it would for the user, headless too: its tabs come back, and
|
|
62
|
+
session cookies with them, so a login stays. They start loading before the REPL is attached, so
|
|
63
|
+
`requests` and `console` have only what came after, often nothing: `reload` for a full record.
|
|
63
64
|
- `pw-repl run` runs it in a terminal with no server; `send` then reaches it through tmux, if it runs in
|
|
64
65
|
the tmux session `pw-repl`:
|
|
65
66
|
|
|
@@ -112,8 +113,9 @@ of a REPL of your own, which `send` refuses. The words after `send` are the comm
|
|
|
112
113
|
`select`, `press`, `upload` and `wait`, a word quoted in your shell stays one word
|
|
113
114
|
(`pw-repl send fill "text=Your name" Ada`, `pw-repl send wait text "Showing 1"`); other commands get the words as they are, joined by
|
|
114
115
|
spaces (`pw-repl send eval "document.title + ' x'"`). A command that takes only a selector takes the
|
|
115
|
-
whole line, spaces and all: `pw-repl send click "text=Your name"`.
|
|
116
|
-
|
|
116
|
+
whole line, spaces and all: `pw-repl send click "text=Your name"`. `send`'s own options (`-t`, `-c`, `-e`,
|
|
117
|
+
`-s`, `--no-vars`) go before the command or after it (`pw-repl send open <url> -t 30`); after `--`, every
|
|
118
|
+
word is the command's. It works however the REPL was started:
|
|
117
119
|
|
|
118
120
|
- `run` (in tmux): `send` types the command into the tmux pane and reads the result back off the screen.
|
|
119
121
|
It types only when the pane's last line is a bare prompt, so nothing lands in a shell or in the middle
|
|
@@ -135,9 +137,10 @@ line, not in it; the REPL puts it in only as it runs the line, and its pane and
|
|
|
135
137
|
written, so neither the file nor the line as logged holds the secret. Only `PW_` names are variables;
|
|
136
138
|
any other `{{ ... }}` (a page's template text) is sent as written. One that is not set stops the file;
|
|
137
139
|
`--no-vars` sends it as written. A single `send` takes them too: `PW_PASSWORD=... pw-repl send fill
|
|
138
|
-
'#password' "{{ PW_PASSWORD }}"` types a password without it in the command.
|
|
139
|
-
|
|
140
|
-
|
|
140
|
+
'#password' "{{ PW_PASSWORD }}"` types a password without it in the command. Give the value without
|
|
141
|
+
typing it, from a file only you can read: `PW_PASSWORD="$(cat <file>)" pw-repl send ...`. What reads it
|
|
142
|
+
back can still show it: `snapshot`, `html` and `attrs` hide a password field's value, `eval` on the
|
|
143
|
+
field does not.
|
|
141
144
|
|
|
142
145
|
`send` takes one command; chain several in your shell. Use `&&` when a step only makes sense if the one
|
|
143
146
|
before it worked: it stops at any exit status but 0, a failure and an outcome not confirmed included.
|
|
@@ -160,40 +163,24 @@ background REPL (server commands as `[server]` lines), so whoever looks there se
|
|
|
160
163
|
shows REPL commands only, not what was clicked in the browser (unless `watch on --live` is on); for
|
|
161
164
|
that, look at the browser itself: `tab` and `info` for where things are, `requests` for the requests the
|
|
162
165
|
clicks made (`body <#>` for what one returned), `console` for console messages and page errors.
|
|
163
|
-
`watch on` records each step someone takes in a tab, with the requests it caused
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
runs again, a repro to keep. What is typed is recorded, except in password fields; `watch on
|
|
168
|
-
--no-values` leaves it out. The file has a `{{ PW_PASSWORD }}` variable where a password was typed,
|
|
169
|
-
set when it runs (`PW_PASSWORD=... pw-repl send --file login.txt`), so the file can be shared without
|
|
170
|
-
it.
|
|
171
|
-
|
|
172
|
-
When watching someone:
|
|
173
|
-
|
|
174
|
-
- A watched tab keeps response bodies of up to 100 KB, so `body <#>` still has them after the tab
|
|
175
|
-
navigates. Read a bigger one that matters as soon as it shows up in `requests`: the browser drops it
|
|
176
|
-
once the tab navigates.
|
|
177
|
-
- `watch` leaves out images and other static files, failed ones too; `console error` and
|
|
178
|
-
`requests --all` show what failed.
|
|
179
|
-
- Without a url-part, `--next-tab` takes the first tab anyone opens; check `tab` and `info` once it
|
|
180
|
-
fires, since another tab opening later does not show in `watch`.
|
|
181
|
-
- A watch belongs to the tab: when another client watches it too, read it with `watch <n>`, since
|
|
182
|
-
`watch new`'s place is shared and each one's would hide steps from the other.
|
|
166
|
+
`watch on` records each step someone takes in a tab, with the requests it caused; `watch` reads it back,
|
|
167
|
+
and `watch save <file>` writes the steps as commands that `send --file` runs again, a repro to keep.
|
|
168
|
+
Before you watch someone, read `help watch`: what it keeps and leaves out, passwords, and sharing a watch
|
|
169
|
+
with another client.
|
|
183
170
|
|
|
184
171
|
When a page is slow or feels laggy, `requests` shows a slow response; for what the page itself is busy
|
|
185
|
-
with, `profile on` and `profile off` around what you do, or while the user uses it
|
|
186
|
-
trace of the tab: long tasks and the function behind each, forced layouts, each click's time to the next
|
|
187
|
-
paint, and frames dropped while scrolling or dragging. `profile reload` profiles a load. `help profile`.
|
|
172
|
+
with, `profile on` and `profile off` around what you do, or while the user uses it (`help profile`).
|
|
188
173
|
|
|
189
174
|
Exit status: `0` ok, `1` the command failed, `2` completion not confirmed (outcome unknown: do not
|
|
190
|
-
blindly retry a change), `64` usage or the REPL is not reachable. `pw-repl --help` has the options
|
|
175
|
+
blindly retry a change), `64` usage or the REPL is not reachable. `pw-repl --help` has the options, and
|
|
176
|
+
`pw-repl send --help` the rest of send's.
|
|
191
177
|
|
|
192
178
|
## Learn the commands
|
|
193
179
|
|
|
194
180
|
Run `pw-repl send help`. It lists seven topics; `help <topic>` lists their commands, `help <command>`
|
|
195
|
-
gives usage and caveats: read it before you use a command, since this file does not repeat them.
|
|
196
|
-
|
|
181
|
+
gives usage and caveats: read it before you use a command, since this file does not repeat them. When a
|
|
182
|
+
command fails or does something you did not expect, read its `help <command>` before working around it.
|
|
183
|
+
`help --all` prints everything at once. pw-repl answers help itself: it needs no running REPL, and never
|
|
197
184
|
reaches one. If you know playwright-cli, its command names work too (`help playwright-cli` lists them).
|
|
198
185
|
|
|
199
186
|
### Coming from playwright-cli
|
|
@@ -209,8 +196,9 @@ pw-repl send -c <name> click e5
|
|
|
209
196
|
pw-repl send -c <name> close # your modes off; tab close <url-part> closes yours
|
|
210
197
|
```
|
|
211
198
|
|
|
212
|
-
- There is no browser of your own to open or close: `open` opens a tab in the one the REPL uses,
|
|
213
|
-
|
|
199
|
+
- There is no browser of your own to open or close: `open` opens a new tab in the one the REPL uses,
|
|
200
|
+
each time, so use it once and `goto <url>` after that, which navigates the selected tab. `close` turns
|
|
201
|
+
off the modes you turned on and leaves every tab open.
|
|
214
202
|
- Instead of `-s=<session>`, send as a client: `-c <name>`, with a selected tab of its own, in a browser
|
|
215
203
|
whose cookies and storage are shared.
|
|
216
204
|
- `upload <selector> <file>` names the file input or the button that opens it; do not click a file
|