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 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 --grep='npm test:'`): a run that grew by more than
61
- a few seconds means a slow test crept in, usually one waiting out a real timeout (5s for a click).
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 (user ruling). When the newest breaks a test, fix it in our code;
110
- only a regression of the dependency's own, with the fix already in its next release, keeps the version
111
- before it, until that release ships. playwright-core stays at 1.63 for that reason: on 1.64.1 a click
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"`. `pw-repl serve <port>` serves TCP on 127.0.0.1; other addresses are refused.
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 options = { ...(button ? { button } : {}), ...(modifiers.length ? { modifiers } : {}), ...(clickCount > 1 ? { clickCount } : {}) };
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
- // Worked out from the whole log: the summary no longer says whether the element was found.
108
- summary.uncertain = !/waiting for locator/.test(message) || /resolved to/.test(message);
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 after it began acting, it says what stopped it (e.g. <div class="overlay"> intercepts pointer
227
- events) and that the outcome is unknown; Playwright's full call log goes to the REPL's pane or log.`,
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 through a temporary session',
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: `The session is detached afterwards, so subscriptions and settings do not persist. Browser.close and
927
- Target.closeTarget are refused, and so is Page.handleJavaScriptDialog: dialog answers one.`,
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
- resolved = words.map((w, n) => (w.startsWith('--filename=') && w.length > 11 ? `--filename=${resolve(w.slice(11))}` : words[n - 1] === '--filename' ? resolve(w) : w));
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
- module.exports = { SELECTOR_FIRST, REF, toSelector, unescapeDouble, unquote, shellWords, splitSelector, keyName, variablesIn, expandVariables };
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pw-repl",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Text REPL for driving an existing Chromium through Playwright over CDP",
5
5
  "keywords": [
6
6
  "playwright",