pw-repl 0.12.4 → 0.12.6

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
@@ -20,7 +20,8 @@ If you hit a limitation or write a workaround, consider adding the capability to
20
20
  drawn over the page, which the element and mouse commands glide first), `lib/toast.js` (a line of text
21
21
  over the page), `lib/overlay.js` (the element the cursor and toast are drawn in, one layer each), `lib/inspect.js` (reading the
22
22
  page, screenshots, eval, CDP, waits), `lib/watch.js`, `lib/requestlog.js` (requests, bodies, console),
23
- `lib/routes.js`, `lib/network.js`, `lib/emulation.js` (and viewport), `lib/capture.js`, `lib/record.js`
23
+ `lib/routes.js`, `lib/network.js`, `lib/emulation.js` (and viewport), `lib/capture.js`, `lib/profile.js` (performance traces;
24
+ `lib/trace.js` reads and sums them up), `lib/record.js`
24
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
25
26
  session kept per tab, and `lib/util.js` small shared helpers. No facility requires `lib/modes.js` or
26
27
  `lib/commands.js`, and a unit test fails on a require cycle, which CommonJS would not report.
package/README.md CHANGED
@@ -54,6 +54,8 @@ pw> console error # and see how the page copes
54
54
  - **Record:** what someone does in a tab (`watch`), a timeline of requests and console (`capture`), or a
55
55
  video of the tab (`record`), with a virtual pointer (`cursor`), captions (`toast`) and highlights
56
56
  (`highlight`).
57
+ - **Profile:** what keeps a page's main thread busy (`profile`): long tasks and the function behind each,
58
+ forced style and layout, each click's or key's time to the next paint, and dropped frames.
57
59
  - **Share:** many clients on one REPL, each with its own tab; a command server on a Unix socket or a local
58
60
  port, with an HTTP+JSON protocol.
59
61
 
@@ -146,6 +148,7 @@ example the Docker bridge gateway, `ip route | awk '/default/ {print $3}'`).
146
148
  | see each step as I click | `watch on --live` |
147
149
  | replay what I clicked through | `watch on`, click around, `watch save flow.txt`, then `pw-repl send --file flow.txt` |
148
150
  | record requests and console together | `capture on`, then `capture off` |
151
+ | find what makes the page slow | `profile on`, then `profile off`; `profile reload` for a page load |
149
152
  | fake a backend response | `route <glob> <status> <json>`, `route <glob> abort` |
150
153
  | change or slow an API response | `route <glob> patch <json>`, `route <glob> delay <secs>` |
151
154
  | cut or slow the whole network | `network off`, `network slow`, then `network on` |
@@ -159,11 +162,11 @@ example the Docker bridge gateway, `ip route | awk '/default/ {print $3}'`).
159
162
 
160
163
  ### Modes
161
164
 
162
- Some commands turn on a mode that stays on until you turn it off: `watch`, `capture`, `route`, `network
163
- off` or `slow`, `emulate`, `viewport`, `record`, `highlight`, `cursor` and `toast`. While any are on in
164
- the selected tab, the prompt shows them: `(watch network:off routes:2) pw>`. `modes` lists them for every
165
- tab, and `modes off` turns them all off. Each of these commands on its own shows its state and what you
166
- can run next.
165
+ Some commands turn on a mode that stays on until you turn it off: `watch`, `capture`, `profile`, `route`,
166
+ `network off` or `slow`, `emulate`, `viewport`, `record`, `highlight`, `cursor` and `toast`. While any
167
+ are on in the selected tab, the prompt shows them: `(watch network:off routes:2) pw>`. `modes` lists them
168
+ for every tab, and `modes off` turns them all off. Each of these commands on its own shows its state and
169
+ what you can run next.
167
170
 
168
171
  ## For agents and scripts
169
172
 
package/bin/pw-repl.js CHANGED
@@ -38,7 +38,9 @@ $PW_ENDPOINT, then $PW_SOCKET, then the default.
38
38
  send -c <client> (or $PW_CLIENT) sends as a client of the REPL's with a selected tab of its own, so
39
39
  several agents can share one REPL; pw-repl help session has the rest.
40
40
 
41
- send exit status: 0 ok, 1 command error, 2 completion not confirmed, 64 usage or unreachable.
41
+ send exit status: 0 ok, 1 command error, 2 completion not confirmed, 64 usage or unreachable. send -t is
42
+ how long send waits for the answer (default 20s); it does not lengthen a command's own limits, such as
43
+ the 5s an action waits for its element.
42
44
 
43
45
  send --file <file> runs each line of a file as a command, in order, as if typed at the REPL's prompt:
44
46
  quoted as the prompt reads it (no shell), with a file it records, uploads or saves relative to this
package/lib/commands.js CHANGED
@@ -14,6 +14,7 @@ const { commands: networkCommands } = require('./network');
14
14
  const { commands: emulationCommands, EMULATE_KINDS } = require('./emulation');
15
15
  const { commands: captureCommands } = require('./capture');
16
16
  const { commands: recordCommands } = require('./record');
17
+ const { commands: profileCommands } = require('./profile');
17
18
  const { commands: inspectCommands } = require('./inspect');
18
19
  const { commands: watchCommands, nextTabWanted, waitingForMe, watchNewTab } = require('./watch');
19
20
  const { commands: tabsCommands, isOpening } = require('./tabs');
@@ -33,6 +34,7 @@ const commands = {
33
34
  ...emulationCommands,
34
35
  ...captureCommands,
35
36
  ...recordCommands,
37
+ ...profileCommands,
36
38
  ...dialogsCommands,
37
39
  ...modesCommands,
38
40
 
@@ -117,6 +119,7 @@ function complete(line) {
117
119
  if (command === 'watch') return match(['on', 'off', 'new', '--all']);
118
120
  if (command === 'capture') return match(['on', 'off']);
119
121
  if (command === 'record') return match(['on', 'off']);
122
+ if (command === 'profile') return match(['on', 'off', 'save', 'reload']);
120
123
  if (command === 'cursor') return match(['on', 'off']);
121
124
  if (command === 'toast') return match(['off']);
122
125
  if (command === 'viewport') return match(['off']);
@@ -124,6 +127,7 @@ function complete(line) {
124
127
  }
125
128
  if (words.length >= 3 && command === 'watch' && words[1] === 'on') return match(['--changes', '--live', '--next-tab'].filter(f => !words.slice(2, -1).includes(f)));
126
129
  if (words.length === 3 && command === 'capture' && words[1] === 'on') return match(['requests', 'console']);
130
+ if (words.length >= 3 && command === 'profile' && words[1] === 'on') return match(['--cpu']);
127
131
  if (words.length === 3 && command === 'emulate' && EMULATE_KINDS.includes(words[1])) return match(['off']);
128
132
  if (words.length === 3 && command === 'route' && words[1] === 'off') return match(['--all', ...(state.page ? routesFor(state.page).keys() : [])]);
129
133
  return [[], current];
package/lib/elements.js CHANGED
@@ -5,7 +5,7 @@ const { state, onShutdown } = require('./state');
5
5
  const out = require('./output');
6
6
  const { takeOptions } = require('./cli-names');
7
7
  const { unquote, toSelector, keyName, splitSelector, REF } = require('./syntax');
8
- const { numbers, hints } = require('./util');
8
+ const { numbers, hints, BUSY_PAGE } = require('./util');
9
9
  const { tabState } = require('./tabstate');
10
10
  const { cursorOf, aiming, aimAt, glideTo, showClick, showSelect } = require('./cursor');
11
11
  const { recordingOf, stepTarget, pauseToShow } = require('./record');
@@ -95,8 +95,13 @@ async function onElement(selector, action, budget = 5000) {
95
95
  // or the check the element kept failing.
96
96
  function lastBlocker(log) {
97
97
  const lines = log.split('\n').map(line => line.trim().replace(/^-\s*/, ''));
98
+ // Its last step was the action itself: it was sent, and the page stopped answering, as when the click
99
+ // sets off a long task. A profile started now would miss that task: Chrome traces only what begins
100
+ // after it starts.
101
+ const sent = /^performing (\w+) action$/.exec(lines.filter(Boolean).pop() || '');
102
+ if (sent) return `the ${sent[1]} was sent, and may have landed, but the page stopped answering after it, likely busy with what it set off. A profile shows that only if it is on before the action: profile on, then the action again, if repeating it is safe`;
98
103
  const line = lines.reverse().find(l => /intercepts pointer events|element is not |element is outside|not attached|detached/.test(l));
99
- if (!line) return 'it never became ready to act on';
104
+ if (!line) return `it never became ready to act on; ${BUSY_PAGE}`;
100
105
  return line.length > 240 ? `${line.slice(0, 240)}…` : line;
101
106
  }
102
107
 
package/lib/help.js CHANGED
@@ -69,12 +69,13 @@ tab on its own lists them all.`,
69
69
  interact: {
70
70
  intro: `Selectors are Playwright selectors (CSS, text=..., role=...) or snapshot refs such as e5.
71
71
  Commands use the first match (click, dblclick and hover the first they can use: help click); see help
72
- fill for selectors with spaces. mousemove and the other mouse commands act at a point instead.`,
72
+ fill for selectors with spaces. mousemove and the other mouse commands act at a point instead. An action
73
+ scrolls its element into view first, as Playwright does; profile counts that scroll in the action's step.`,
73
74
  commands: ['click', 'dblclick', 'hover', 'fill', 'type', 'press', 'select', 'check', 'uncheck', 'upload', 'mousemove', 'mouseclick', 'mousedown', 'mouseup', 'mousewheel'],
74
75
  },
75
76
  inspect: {
76
77
  intro: `Output is capped; --all right after the command, or at the end, shows everything (text --all body).`,
77
- commands: ['snapshot', 'watch', 'text', 'html', 'attrs', 'listeners', 'count', 'visible', 'links', 'inputs', 'screenshot', 'highlight', 'viewport', 'emulate', 'wait', 'sleep'],
78
+ commands: ['snapshot', 'watch', 'profile', 'text', 'html', 'attrs', 'listeners', 'count', 'visible', 'links', 'inputs', 'screenshot', 'highlight', 'viewport', 'emulate', 'wait', 'sleep'],
78
79
  },
79
80
  video: {
80
81
  intro: `A video of what you do in a tab, for someone to watch. Record it from a file of commands sent with
@@ -235,7 +236,7 @@ A click at a point: mouseclick <x> <y>. A drag: mousedown, then mousemove to ano
235
236
  clicked with click, which waits for it. help mousemove shows how to find a point. The mouse stays at the point, for a mousewheel or mousedown after
236
237
  it. The cursor (help cursor) moves there first and shows a ring at the press.`,
237
238
  },
238
- mousedown: { usage: 'mousedown [button]', summary: 'press a mouse button where the mouse is (left, right or middle)' },
239
+ mousedown: { usage: 'mousedown [button]', summary: 'press a mouse button at the mouse (left, right, middle)' },
239
240
  mouseup: { usage: 'mouseup [button]', summary: 'release a mouse button where the mouse is' },
240
241
  mousewheel: {
241
242
  usage: 'mousewheel <dx> <dy>',
@@ -252,7 +253,7 @@ scrolled or redrawn. While recording, it scrolls in eased steps over up to 1s (h
252
253
  summary: 'clear an input and fill it',
253
254
  args: [
254
255
  ['<selector>', 'the first word; quote it if it has spaces: fill "text=Your name" Ada.'],
255
- ['<value>', 'the rest of the line; quotes around all of it are removed, so fill #name "" clears it.'],
256
+ ['<value>', 'the rest of the line; quotes around all of it are removed, so fill #name "" clears it. Over send, the shell\'s quotes do it: pw-repl send fill #name "" (\'""\' types two quote marks).'],
256
257
  ],
257
258
  options: [
258
259
  ['--submit', 'at the end: presses Enter in it afterwards.'],
@@ -261,8 +262,8 @@ scrolled or redrawn. While recording, it scrolls in eased steps over up to 1s (h
261
262
  once, even while recording: type shows it being typed.`,
262
263
  },
263
264
  type: {
264
- usage: 'type [<selector>] <text> [--submit]',
265
- summary: 'type key by key after what the field holds; text as for fill',
265
+ usage: 'type [--delay=<ms>] [<selector>] <text> [--submit]',
266
+ summary: 'type key by key after what the field holds',
266
267
  args: [
267
268
  ['[<selector>]', 'as for fill. Without it (one word, or one quoted), into what has focus: type "garden hose".'],
268
269
  ['<text>', 'the rest of the line, as fill\'s value.'],
@@ -281,10 +282,10 @@ Date and time inputs: they take keys part by part, in the order they show them (
281
282
  from the first part with a selector, or from the part a click landed on. type says what the input then
282
283
  holds. Their ISO form (2026-10-02) is refused, since typed it comes out mangled: fill sets it.`,
283
284
  },
284
- press: { usage: 'press <key> | press <selector> <key>', summary: 'press a key (Enter, Escape, Control+A; any case), maybe on an element' },
285
+ press: { usage: 'press <key> | press <selector> <key>', summary: 'press a key: Enter, Escape, Control+A (any case)' },
285
286
  select: {
286
287
  usage: 'select <selector> <value>',
287
- summary: 'choose an option in a select (the value is the rest of the line)',
288
+ summary: 'choose an option; the value is the rest of the line',
288
289
  args: [
289
290
  ['<value>', 'an option\'s value or its label.'],
290
291
  ],
@@ -314,6 +315,7 @@ all.`,
314
315
  ['<selector>', 'outlines the first match, with new refs: those from before stop working.'],
315
316
  ],
316
317
  options: [
318
+ ['--all', 'the whole outline, not capped.'],
317
319
  ['--full', 'Playwright\'s snapshot unchanged.'],
318
320
  ['--grep <text>', 'only the lines containing text (role, name or a flag such as [disabled], any case), each with the named elements around it.'],
319
321
  ['--regex <pattern>', '--grep by a regular expression: snapshot --regex "Total: \\d+".'],
@@ -332,7 +334,8 @@ value attribute so too. A field a Show password button made plain text, with no
332
334
  shows its value. Every other field shows what it holds (textbox "Name": Ada), and a select its chosen
333
335
  option: inputs lists the fields without what they hold.
334
336
 
335
- Output over 60 lines ends with a line count.`,
337
+ Output over 60 lines ends with a line count. Past 12000 characters it is cut at the end of a line, saying
338
+ where: --all shows all.`,
336
339
  },
337
340
  watch: {
338
341
  usage: 'watch on [options] | off | [n|new] | save <file>',
@@ -703,6 +706,74 @@ Kept from when the REPL connects; the last 200 per tab.
703
706
 
704
707
  A page brought back from the back/forward cache (back, forward) reports its earlier messages again, at
705
708
  the time it comes back, though nothing loaded again.`,
709
+ },
710
+ profile: {
711
+ usage: 'profile on [secs] | off | reload | save <file>',
712
+ summary: 'what slows the tab: long tasks, layouts, input, frames',
713
+ args: [
714
+ ['on', 'starts a performance trace of the selected tab, until profile off (at most 120 seconds).'],
715
+ ['[secs]', 'records that long (1 to 120), then prints. Other commands wait until it ends. Over 20, give send time too: pw-repl send -t 70 profile on 60.'],
716
+ ['off', 'stops it and prints the summary.'],
717
+ ['reload', 'reloads the tab and profiles its load, until its network is idle (at most 10s after load) and half a second more. For work that starts later (a timer): profile on, reload, sleep, profile off.'],
718
+ ['save <file>', 'writes the last profile\'s trace, the tab\'s events only, as Chrome trace JSON for DevTools\' Performance panel (Load profile) or ui.perfetto.dev. It refuses a file that exists.'],
719
+ ['(none)', 'says whether one is running, or shows the last one.'],
720
+ ],
721
+ optionsFor: 'profile on',
722
+ options: [
723
+ ['--cpu', 'samples JS more often, and adds the functions that ran longest themselves. The trace gets much bigger.'],
724
+ ],
725
+ detail: `For "the page feels slow": what the tab's main thread was busy with, and when. It is a Chrome
726
+ performance trace, as DevTools' Performance panel records (Playwright's browser.startTracing), summed up.
727
+ Use it around something you do (profile on, click e5, profile off), or while the user uses the page: steps
728
+ they take in the browser are in it.
729
+
730
+ The summary: main-thread busy time by kind (scripting, style/layout, paint, GC, other); the page's
731
+ load marks, when it loaded; each long task (over 50ms), with what set it off, the script it ran, the
732
+ function it spent most time in ("most in"), each as file:line function, and a big response (100 KB or more) that finished just before
733
+ it; forced style and layout (work a script waited for, reading a size or a style), by where in the script; and the steps:
734
+ each click or key press with its time to the next paint (input delay, handlers, presentation), and each
735
+ burst of wheel or scroll events and each drag, with the main thread's busy time and the frames drawn and
736
+ dropped. Times are from when it started. --all lists every long task and step, not the first ones.
737
+
738
+ Long tasks read kind, what set it off, script: scripting, click event, app.js:120 handleClick. The kind
739
+ is what most of its time went to (GC, style/layout); what set it off is the event or callback that began
740
+ it (timer, promise callbacks); promise callbacks a click handler queued run in the click's task, so
741
+ show as click event. No script is named when none of its code came from a page URL.
742
+
743
+ A page that freezes: profile on, then do what freezes it. Your commands wait for the page meanwhile, and
744
+ may time out, but the profile records it all; profile off once the page answers again.
745
+
746
+ Size: a busy page fills Chrome's trace buffer in under a minute (one that restyles many elements as it
747
+ draws, sooner), which stops the profile there, and the summary says so. Stopping a long one takes a
748
+ while, as Chrome writes out its trace: give send time, pw-repl send -t 60 profile off. If send gives up
749
+ first, the profile still ends, and profile shows it.
750
+
751
+ One profile runs at a time, in a browser: Chrome traces one at a time, the whole browser's, so another
752
+ REPL's profile, or DevTools' Performance panel recording, stops profile on. It belongs to the tab, ends
753
+ if the tab closes (profile shows it), and modes lists it.`,
754
+ moreAbout: 'what is left out, inlined functions, frames in a background tab, and the mark it leaves in the page',
755
+ more: `The tab's own renderer only: a cross-site iframe running in a process of its own is left out, and
756
+ so is anything another tab of the same site, sharing the tab's process, does at the same time.
757
+
758
+ Frames: a tab pw-repl uses is kept drawing as if it were in front (help tab), so its frame counts are a
759
+ tab in front's; one behind another tab draws far less, as the user would see it. The summary says so.
760
+
761
+ A function the JS engine inlined into its caller counts as the caller, in "most in" and --cpu alike: a
762
+ small hot function called in a loop can show as the loop's function.
763
+
764
+ A wheel-zoom or drag the page handles in its own script (a map) shows as a step from its events; one the
765
+ browser handles alone, with no listener, may show only as frames.
766
+
767
+ A page gets scroll events for any scroll, input or not. Those next to a click or key press, with no wheel
768
+ event (an action's scroll into view, or a handler's scrollTo), are that step's: it says the page
769
+ scrolled. A burst of scroll events alone may also be the page's own script scrolling.
770
+
771
+ A step under way when the profile began (an action still running at profile on) reads before, without
772
+ its busy time: the trace has nothing from before it began.
773
+
774
+ profile on and off each run console.timeStamp in the page, which prints nothing, so the tab is found in
775
+ the trace even when its page did nothing. The trace is kept in a temporary folder until the next
776
+ profile, or until the REPL stops.`,
706
777
  },
707
778
  capture: {
708
779
  usage: 'capture on [requests|console] [secs] | off',
package/lib/inspect.js CHANGED
@@ -6,7 +6,7 @@ const out = require('./output');
6
6
  const { printOutput } = out;
7
7
  const { devices } = require('playwright-core');
8
8
  const { toSelector, REF, unquote } = require('./syntax');
9
- const { regexOf, COMMAND_TIMEOUT } = require('./util');
9
+ const { regexOf, COMMAND_TIMEOUT, BUSY_PAGE } = require('./util');
10
10
  const { tabState } = require('./tabstate');
11
11
  const { keptSession } = require('./cdp');
12
12
  const { CDP_REFUSED } = require('./dialogs');
@@ -127,6 +127,13 @@ async function waitGone(locator, what, timeout) {
127
127
 
128
128
  const SNAPSHOT_HINT_LINES = 60;
129
129
 
130
+ function evaluated(promise) {
131
+ return withTimeout(promise, 'Page evaluation', COMMAND_TIMEOUT).catch(error => {
132
+ if (error.message === 'Page evaluation timed out') error.message += ` after ${COMMAND_TIMEOUT / 1000}s; ${BUSY_PAGE}`;
133
+ throw error;
134
+ });
135
+ }
136
+
130
137
  // Each matching line (any part of it: role, name, flags such as [disabled]),
131
138
  // with the named elements it sits in, so a match can be placed without the
132
139
  // rest of the tree.
@@ -422,7 +429,7 @@ const commands = {
422
429
  return;
423
430
  }
424
431
  const lines = text.split('\n').length;
425
- printOutput(lines > SNAPSHOT_HINT_LINES ? `${text}\n[${lines} lines; snapshot --grep <text> or snapshot <eN> shows less]` : text, all);
432
+ printOutput(text, all, lines > SNAPSHOT_HINT_LINES ? `[${lines} lines; snapshot --grep <text> or snapshot <eN> shows less]` : '');
426
433
  },
427
434
 
428
435
  async wait(args) {
@@ -480,7 +487,7 @@ const commands = {
480
487
  try {
481
488
  // Made from its text by the debugger, as eval is, so a page's CSP cannot
482
489
  // refuse it, and in the element's own frame, which may be an iframe.
483
- const fn = await withTimeout((await el.ownerFrame()).evaluateHandle(unquote(onRef[1])), 'Page evaluation', COMMAND_TIMEOUT);
490
+ const fn = await evaluated((await el.ownerFrame()).evaluateHandle(unquote(onRef[1])));
484
491
  try {
485
492
  printOutput(await el.evaluate((e, f) => f(e), fn), all);
486
493
  } finally {
@@ -502,13 +509,13 @@ const commands = {
502
509
  const call = `const value = (${args}\n); return typeof value === 'function' ? value() : value;`;
503
510
  if (awaits && !single && compiles(AsyncFunction, args)) {
504
511
  const session = await keptSession(state.page);
505
- const { result, exceptionDetails } = await withTimeout(session.send('Runtime.evaluate', { expression: args, replMode: true, awaitPromise: true, returnByValue: true }), 'Page evaluation', COMMAND_TIMEOUT);
512
+ const { result, exceptionDetails } = await evaluated(session.send('Runtime.evaluate', { expression: args, replMode: true, awaitPromise: true, returnByValue: true }));
506
513
  if (exceptionDetails) throw new Error(exceptionDetails.exception?.description || exceptionDetails.text);
507
514
  printOutput(result.value !== undefined ? result.value : result.description ?? 'undefined', all);
508
515
  return;
509
516
  }
510
517
  const expression = single ? `(${awaits ? 'async ' : ''}() => { ${call} })()` : args;
511
- const value = await withTimeout(state.page.evaluate(expression), 'Page evaluation', COMMAND_TIMEOUT);
518
+ const value = await evaluated(state.page.evaluate(expression));
512
519
  // An empty string is shown as one, not as no output at all.
513
520
  printOutput(value === '' ? '""' : value, all);
514
521
  // Quotes around the whole of it, as a shell's quoting sent on, make it a string, not code to run:
package/lib/modes.js CHANGED
@@ -8,6 +8,7 @@ const { setNetwork } = require('./network');
8
8
  const { emulatedKinds, setEmulation, setViewport } = require('./emulation');
9
9
  const { recordingOf, finishRecording } = require('./record');
10
10
  const { currentCapture, endCapture } = require('./capture');
11
+ const { currentProfile, finishProfile } = require('./profile');
11
12
  const { nextTabWanted, waitingText, stopWaitingForNextTab, stopWatching } = require('./watch');
12
13
  const { hideHighlights, setLabels } = require('./elements');
13
14
  const { cursorOf, cursorOff } = require('./cursor');
@@ -31,6 +32,7 @@ function activeModes(p) {
31
32
  if (cursorOf(p)) modes.push('cursor');
32
33
  if (toastOf(p)) modes.push('toast');
33
34
  if (currentCapture()?.page === p) modes.push('capture');
35
+ if (currentProfile()?.page === p) modes.push('profile');
34
36
  if (recordingOf(p)) modes.push('record');
35
37
  return modes;
36
38
  }
@@ -87,6 +89,8 @@ function modeOwners(p) {
87
89
  if (toast) owners.push([toast.owner ?? null, 'toast']);
88
90
  const capture = currentCapture();
89
91
  if (capture?.page === p) owners.push([capture.owner ?? null, 'capture']);
92
+ const profile = currentProfile();
93
+ if (profile?.page === p) owners.push([profile.owner ?? null, 'profile']);
90
94
  const recording = recordingOf(p);
91
95
  if (recording) owners.push([recording.owner ?? null, 'record']);
92
96
  return owners;
@@ -116,6 +120,8 @@ async function allModesOff(mine = false) {
116
120
  if (watch?.on && ours(watch.owner)) { stopWatching(p, watch); done.push('watch off'); }
117
121
  const capture = currentCapture();
118
122
  if (capture?.page === p && ours(capture.owner)) { endCapture(); done.push('capture off (capture shows it)'); }
123
+ const profile = currentProfile();
124
+ if (profile?.page === p && ours(profile.owner)) { await finishProfile(); done.push('profile off (profile shows it)'); }
119
125
  const globs = [...(tabState(p).routes || [])].filter(([, route]) => ours(route.owner)).map(([glob]) => glob);
120
126
  if (globs.length) { await unroute(p, globs); done.push(`${globs.length} route${globs.length === 1 ? '' : 's'} removed`); }
121
127
  if ('network' in tabState(p) && ours(tabState(p).network.owner)) { await setNetwork(p, null); done.push('network on'); }
package/lib/output.js CHANGED
@@ -65,16 +65,25 @@ function aside(text) {
65
65
  else console.log(text);
66
66
  }
67
67
 
68
- function printOutput(value, all = false) {
68
+ // A footer is printed after the cut, so it is never cut off itself.
69
+ function printOutput(value, all = false, footer = '') {
69
70
  let text;
70
71
  if (typeof value === 'string') text = value;
71
72
  else text = JSON.stringify(value, null, 2);
72
73
  if (text === undefined) text = 'undefined';
73
74
  if (!all && text.length > OUTPUT_LIMIT) {
74
- log(`${text.slice(0, OUTPUT_LIMIT)}\n[truncated; add --all to the command to show all]`);
75
- } else {
76
- log(text);
75
+ // Cut at a line's end, unless the last one within the limit comes so early (a short line before a
76
+ // long minified one) that little would be shown.
77
+ const end = text.lastIndexOf('\n', OUTPUT_LIMIT);
78
+ if (end > OUTPUT_LIMIT / 2) {
79
+ const shown = text.slice(0, end);
80
+ const at = shown.split('\n').length;
81
+ text = `${shown}\n[truncated at line ${at} of ${text.split('\n').length}; add --all to the command to show all]`;
82
+ } else {
83
+ text = `${text.slice(0, OUTPUT_LIMIT)}\n[truncated; add --all to the command to show all]`;
84
+ }
77
85
  }
86
+ log(footer ? `${text}\n${footer}` : text);
78
87
  }
79
88
 
80
89
  module.exports = { OUTPUT_LIMIT, log, error, collect, take, notice, aside, onIdlePrint, onSelectedTab, printOutput };