pw-repl 0.12.5 → 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/bin/pw-repl.js +3 -1
- package/lib/elements.js +7 -2
- package/lib/help.js +17 -1
- package/lib/inspect.js +11 -4
- package/lib/profile.js +15 -6
- package/lib/trace.js +24 -3
- package/lib/util.js +4 -1
- package/package.json +1 -1
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/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
|
|
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,7 +69,8 @@ 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: {
|
|
@@ -734,6 +735,14 @@ each click or key press with its time to the next paint (input delay, handlers,
|
|
|
734
735
|
burst of wheel or scroll events and each drag, with the main thread's busy time and the frames drawn and
|
|
735
736
|
dropped. Times are from when it started. --all lists every long task and step, not the first ones.
|
|
736
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
|
+
|
|
737
746
|
Size: a busy page fills Chrome's trace buffer in under a minute (one that restyles many elements as it
|
|
738
747
|
draws, sooner), which stops the profile there, and the summary says so. Stopping a long one takes a
|
|
739
748
|
while, as Chrome writes out its trace: give send time, pw-repl send -t 60 profile off. If send gives up
|
|
@@ -755,6 +764,13 @@ small hot function called in a loop can show as the loop's function.
|
|
|
755
764
|
A wheel-zoom or drag the page handles in its own script (a map) shows as a step from its events; one the
|
|
756
765
|
browser handles alone, with no listener, may show only as frames.
|
|
757
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
|
+
|
|
758
774
|
profile on and off each run console.timeStamp in the page, which prints nothing, so the tab is found in
|
|
759
775
|
the trace even when its page did nothing. The trace is kept in a temporary folder until the next
|
|
760
776
|
profile, or until the REPL stops.`,
|
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.
|
|
@@ -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
|
|
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
|
|
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
|
|
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/profile.js
CHANGED
|
@@ -35,9 +35,15 @@ function workDir() {
|
|
|
35
35
|
|
|
36
36
|
// A trace event naming the tab's frame, from its renderer, so an idle tab is still found in the trace.
|
|
37
37
|
// console.timeStamp prints nothing in the console. A page held by a dialog does not answer: no wait.
|
|
38
|
-
|
|
38
|
+
// At the end it waits two frames and 100ms more first: a click's timing reaches the trace only after the
|
|
39
|
+
// paint that follows it, and its step takes in what the page did for 100ms after it.
|
|
40
|
+
async function mark(page, { last = false } = {}) {
|
|
39
41
|
if (page.isClosed()) return;
|
|
40
|
-
|
|
42
|
+
const marked = page.evaluate(async settle => {
|
|
43
|
+
if (settle) await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(() => setTimeout(resolve, 100))));
|
|
44
|
+
console.timeStamp('pw-repl profile');
|
|
45
|
+
}, last).catch(() => {});
|
|
46
|
+
await Promise.race([marked, new Promise(resolve => setTimeout(resolve, 1000))]);
|
|
41
47
|
}
|
|
42
48
|
|
|
43
49
|
function forget(profile) {
|
|
@@ -121,7 +127,7 @@ function finishProfile() {
|
|
|
121
127
|
clearTimeout(profile.cap);
|
|
122
128
|
profile.unlisten();
|
|
123
129
|
profile.finished = (async () => {
|
|
124
|
-
await mark(profile.page);
|
|
130
|
+
await mark(profile.page, { last: true });
|
|
125
131
|
const endedAt = Date.now();
|
|
126
132
|
const raw = path.join(workDir(), `raw-${process.pid}-${endedAt}.json`);
|
|
127
133
|
try {
|
|
@@ -212,10 +218,13 @@ function printProfile(profile, all = false) {
|
|
|
212
218
|
const steps = all ? s.steps : s.steps.slice(0, SHOWN * 2);
|
|
213
219
|
for (const step of steps) {
|
|
214
220
|
const span = step.latency != null
|
|
215
|
-
? `${Math.round(step.latency)}ms to next paint (input delay ${Math.round(step.delay)}ms, handlers ${Math.round(step.handlers)}ms, presentation ${Math.round(step.presentation)}ms)`
|
|
221
|
+
? `${Math.round(step.latency)}ms to next paint (input delay ${Math.round(step.delay)}ms, handlers ${Math.round(step.handlers)}ms, presentation ${Math.round(step.presentation)}ms)${step.scrolled ? ', the page scrolled' : ''}`
|
|
216
222
|
: `${secs(step.end - step.start)}, ${step.events} event${step.events === 1 ? '' : 's'}`;
|
|
217
223
|
const frames = step.frames ? `, frames ${step.frames} drawn, ${step.dropped} dropped` : '';
|
|
218
|
-
|
|
224
|
+
// One under way at profile on (an action still running): what ran before it began is not in the
|
|
225
|
+
// trace, so its busy time would be only the end of it.
|
|
226
|
+
const busy = step.start < s.t0 ? 'began before the profile: its busy time was not recorded' : `busy ${ms(step.busy)}${step.longest ? `, longest task ${ms(step.longest)}` : ''}`;
|
|
227
|
+
lines.push(` ${(step.start < s.t0 ? 'before' : at(step.start)).padStart(8)} ${step.kind} ${span}; ${busy}${frames}`);
|
|
219
228
|
}
|
|
220
229
|
if (steps.length < s.steps.length) lines.push(` (the first ${steps.length} of ${s.steps.length}; profile --all lists them all)`);
|
|
221
230
|
if (profile.drawing && s.steps.some(step => step.frames)) {
|
|
@@ -307,4 +316,4 @@ onShutdown(async () => {
|
|
|
307
316
|
if (dir) fs.rmSync(dir, { recursive: true, force: true });
|
|
308
317
|
});
|
|
309
318
|
|
|
310
|
-
module.exports = { commands, currentProfile, finishProfile };
|
|
319
|
+
module.exports = { commands, currentProfile, finishProfile, printProfile };
|
package/lib/trace.js
CHANGED
|
@@ -10,6 +10,9 @@ const BIG_REQUEST = 100 * 1024;
|
|
|
10
10
|
const STEP_GAP = 300000;
|
|
11
11
|
// A step's frames and busy time include this long after its last input, for what it drew.
|
|
12
12
|
const STEP_TAIL = 100000;
|
|
13
|
+
// How long before its scroll events a scroll's frame can be drawn: the compositor scrolls and draws, and
|
|
14
|
+
// the page gets the scroll event in its next frame, later still when frames are being dropped.
|
|
15
|
+
const SCROLL_LEAD = 50000;
|
|
13
16
|
|
|
14
17
|
const BUCKETS = ['scripting', 'style/layout', 'paint', 'GC', 'other'];
|
|
15
18
|
|
|
@@ -433,8 +436,9 @@ function steps(tracks, events, { taskOf, tasks }) {
|
|
|
433
436
|
const began = e => (e.task >= 0 ? events.ts[e.task] : e.ts);
|
|
434
437
|
let burst = null;
|
|
435
438
|
for (const e of dispatches.filter(d => /^(?:wheel|mousewheel|scroll)$/.test(d.type))) {
|
|
436
|
-
|
|
437
|
-
burst
|
|
439
|
+
const wheel = e.type !== 'scroll';
|
|
440
|
+
if (burst && e.ts - burst.end <= STEP_GAP) { burst.end = e.ts + e.dur; burst.events += 1; burst.wheel ||= wheel; continue; }
|
|
441
|
+
burst = { kind: 'wheel/scroll', start: began(e), end: e.ts + e.dur, events: 1, wheel };
|
|
438
442
|
found.push(burst);
|
|
439
443
|
}
|
|
440
444
|
let down = null;
|
|
@@ -470,6 +474,16 @@ function steps(tracks, events, { taskOf, tasks }) {
|
|
|
470
474
|
presentation: Math.max(0, latency - (processingEnd - startAt)),
|
|
471
475
|
});
|
|
472
476
|
}
|
|
477
|
+
// A page gets scroll events for any scroll, not only input: a click's scroll into view (dispatched in
|
|
478
|
+
// the frame after it, or before it when the click waited a frame) or its handler's scrollTo. Scroll
|
|
479
|
+
// events with no wheel event next to a click or key press are that step's.
|
|
480
|
+
for (const burst of found.filter(s => s.kind === 'wheel/scroll' && !s.wheel)) {
|
|
481
|
+
const step = found.find(s => s.latency != null && burst.start <= s.end + STEP_TAIL && burst.end >= s.start - STEP_TAIL);
|
|
482
|
+
if (!step) continue;
|
|
483
|
+
step.scrolled = true;
|
|
484
|
+
step.end = Math.max(step.end, burst.end);
|
|
485
|
+
found.splice(found.indexOf(burst), 1);
|
|
486
|
+
}
|
|
473
487
|
found.sort((a, b) => a.start - b.start);
|
|
474
488
|
const taskStarts = tasks.map(t => t.ts);
|
|
475
489
|
const frames = tracks.frames.sort((a, b) => a.ts - b.ts);
|
|
@@ -488,7 +502,14 @@ function steps(tracks, events, { taskOf, tasks }) {
|
|
|
488
502
|
}
|
|
489
503
|
step.frames = 0;
|
|
490
504
|
step.dropped = 0;
|
|
491
|
-
|
|
505
|
+
let first = lowerBound(frameTimes, step.start);
|
|
506
|
+
// A scroll's own frame comes before its scroll event: the last frame before it, if close and not
|
|
507
|
+
// the step before's.
|
|
508
|
+
if (step.kind === 'wheel/scroll' && first > 0) {
|
|
509
|
+
const before = frames[first - 1].ts;
|
|
510
|
+
if (step.start - before <= SCROLL_LEAD && before > (found[i - 1]?.start ?? -Infinity)) first -= 1;
|
|
511
|
+
}
|
|
512
|
+
for (let k = first; k < frames.length && frames[k].ts < end; k++) {
|
|
492
513
|
step.frames += 1;
|
|
493
514
|
if (frames[k].dropped) step.dropped += 1;
|
|
494
515
|
}
|
package/lib/util.js
CHANGED
|
@@ -7,6 +7,9 @@ const COMMAND_TIMEOUT = 15000;
|
|
|
7
7
|
|
|
8
8
|
const MAX_EVENT_TEXT = 4000;
|
|
9
9
|
|
|
10
|
+
// For a command that timed out waiting on the page.
|
|
11
|
+
const BUSY_PAGE = 'a page busy with a long task does not answer meanwhile: profile on, then try again, shows what it is busy with';
|
|
12
|
+
|
|
10
13
|
// For a command that needs a tab, when none is selected.
|
|
11
14
|
const NO_TAB = 'No tab is selected; select one with tab <index|url-part>, or open one with tab new';
|
|
12
15
|
|
|
@@ -66,4 +69,4 @@ function playwrightBrowsersDir() {
|
|
|
66
69
|
return path.join(cache, 'ms-playwright');
|
|
67
70
|
}
|
|
68
71
|
|
|
69
|
-
module.exports = { STOPPED_RECORDING, ENDED_UNSEEN, COMMAND_TIMEOUT, MAX_EVENT_TEXT, NO_TAB, hints, clipText, numbers, regexOf, patternText, clock, playwrightBrowsersDir };
|
|
72
|
+
module.exports = { BUSY_PAGE, STOPPED_RECORDING, ENDED_UNSEEN, COMMAND_TIMEOUT, MAX_EVENT_TEXT, NO_TAB, hints, clipText, numbers, regexOf, patternText, clock, playwrightBrowsersDir };
|