pw-repl 0.8.0 → 0.8.2

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/lib/elements.js CHANGED
@@ -242,12 +242,10 @@ async function smoothWheel(dx, dy) {
242
242
  }
243
243
  }
244
244
 
245
- // While the tab records, text is typed at a pace a viewer can follow, steady rather than human-like,
246
- // and faster for long text, so it takes 8s at most: a paragraph at 60ms a key would outrun send's 20s
247
- // default and hold every client's commands meanwhile. Otherwise at once, as before.
248
- function typingPace(text) {
249
- if (!recordingOf(state.page) || !text.length) return 0;
250
- return Math.round(Math.min(60, 8000 / text.length));
245
+ // While the tab records, text is typed at a pace a viewer can follow, steady rather than human-like.
246
+ // Otherwise at once, as before.
247
+ function typingPace() {
248
+ return recordingOf(state.page) ? 60 : 0;
251
249
  }
252
250
 
253
251
  // check, uncheck and upload's button click it at its centre, where aimFor put the cursor.
@@ -399,7 +397,7 @@ const commands = {
399
397
  const paced = /^\s*--delay(?:=|\s+)(\S*)(?:\s+|$)/.exec(line || '');
400
398
  if (paced && !/^\d+$/.test(paced[1])) throw new Error('Usage: type --delay=<ms> [<selector>] <text>, the pause between keys, e.g. type --delay=80 #search garden hose');
401
399
  const { args, submit } = submitFlag(paced ? line.slice(paced[0].length) : line);
402
- const delay = text => (paced ? Number(paced[1]) : typingPace(text));
400
+ const delay = () => (paced ? Number(paced[1]) : typingPace());
403
401
  // One word, or one quoted, is typed into the focused element, as playwright-cli's type does.
404
402
  const sole = splitSelector(args);
405
403
  if (sole && sole.rest === undefined) {
@@ -414,7 +412,7 @@ const commands = {
414
412
  const usage = 'type <selector> <text> types into an element, e.g. type #search garden hose';
415
413
  if (!focused) throw new Error(`Nothing on the page has focus: ${usage}`);
416
414
  if (!focused.editable) throw new Error(`The focused element, ${focused.name}, takes no text: ${usage}`);
417
- await state.page.keyboard.type(sole.word, { delay: delay(sole.word) });
415
+ await state.page.keyboard.type(sole.word, { delay: delay() });
418
416
  if (submit) await state.page.keyboard.press('Enter');
419
417
  out.log(`Typed into the focused ${focused.name}${submit ? ', and submitted' : ''}`);
420
418
  return;
@@ -435,7 +433,7 @@ const commands = {
435
433
  getSelection().addRange(range);
436
434
  }
437
435
  }, null, { timeout }));
438
- await state.page.keyboard.type(value, { delay: delay(value) });
436
+ await state.page.keyboard.type(value, { delay: delay() });
439
437
  if (submit) await state.page.keyboard.press('Enter');
440
438
  out.log(`Typed into${submit ? ' and submitted' : ''}: ${shown}`);
441
439
  },
package/lib/emulation.js CHANGED
@@ -220,4 +220,4 @@ const commands = {
220
220
  },
221
221
  };
222
222
 
223
- module.exports = { commands, viewportText, EMULATE_KINDS, emulatedKinds, deviceMetrics, viewportMetrics, setEmulation, setViewport };
223
+ module.exports = { commands, viewportText, EMULATE_KINDS, emulatedKinds, deviceMetrics, viewportMetrics, setEmulation, setViewport, settled };
package/lib/help.js CHANGED
@@ -199,9 +199,9 @@ Examples: fill #name Ada Lovelace, fill "text=Your name" Ada, fill e7 Ada`,
199
199
  --submit at the end presses Enter afterwards.
200
200
 
201
201
  Keys are sent all at once. While the tab is recording (help record), they are 60ms apart, so the video
202
- shows the text being typed; long text goes faster, so typing never takes more than 8s, well within
203
- pw-repl send's 20s default. type --delay=<ms> <selector> <text> sets the gap between keys yourself, with no limit on the
204
- total; for long text, raise send's -t. It is a gap between keys, not a wait before starting like
202
+ shows the text being typed. type --delay=<ms> <selector> <text> sets the gap between keys yourself. The
203
+ whole text takes its length times the gap, and send waits 20s by default: about 300 characters at 60ms
204
+ need a longer send -t, or a shorter --delay. It is a gap between keys, not a wait before starting like
205
205
  screenshot's --delay. A page that reacts to each key, such as search as you type, may react to each one.`,
206
206
  },
207
207
  press: { usage: 'press <key> | press <selector> <key>', summary: 'press a key (Enter, Escape, Control+A; any case), maybe on an element' },
@@ -378,7 +378,9 @@ odd width or height loses a pixel), a phone's too under emulate mobile: a Pixel
378
378
  sharp than the phone draws it. It has no pointer (cursor on draws one) and no browser toolbars,
379
379
  and a tab behind another, hidden or headless keeps recording. Set the size first with viewport
380
380
  (1280x720 fills a 16:9 player); a page that changes size while recording is fitted into the first size.
381
- Chrome sends a frame as the page draws, and a still page keeps its real length.
381
+ Chrome sends a frame as the page draws, and a still page keeps its real length. Under viewport or
382
+ emulate, a page the tab loads while recording holds the last frame until the new page's HTML is in,
383
+ and is then recorded at the viewport's size.
382
384
 
383
385
  While a tab records, type sends keys at a pace the video can show (help type), and mousewheel scrolls in
384
386
  small eased steps over up to 1s rather than at once; fill still sets the whole
package/lib/record.js CHANGED
@@ -8,6 +8,8 @@ const out = require('./output');
8
8
  const { tabState } = require('./tabstate');
9
9
  const { clock, NO_TAB, STOPPED_RECORDING, ENDED_UNSEEN } = require('./util');
10
10
  const { shellWords, splitSelector } = require('./syntax');
11
+ const { keptSession } = require('./cdp');
12
+ const { deviceMetrics, viewportMetrics, settled } = require('./emulation');
11
13
 
12
14
  const FPS = 25;
13
15
  const MAX_SECONDS = 3600;
@@ -126,6 +128,17 @@ function recordingOf(p) {
126
128
  return tabState(p).recording || null;
127
129
  }
128
130
 
131
+ async function restoreMetrics(p, rec) {
132
+ if (rec?.finishing) return;
133
+ const tab = tabState(p);
134
+ if (!tab.viewport && !tab.emulation?.device) return;
135
+ const kept = await keptSession(p);
136
+ if (rec?.finishing) return;
137
+ if (tab.viewport) await viewportMetrics(kept, tab.viewport);
138
+ else if (tab.emulation?.device) await deviceMetrics(kept, tab.emulation.device);
139
+ await settled(p);
140
+ }
141
+
129
142
  // The tabs recording now, for the modes they count as.
130
143
  function recordingPages() {
131
144
  if (!state.browser) return [];
@@ -137,12 +150,17 @@ async function startRecording(p, file, seconds, format) {
137
150
  // As screenshot does: a tab that has not been in front since it was emulated as a phone can
138
151
  // be drawn at the window's size.
139
152
  await p.bringToFront();
153
+ // Bringing an emulated tab to the front can leave Chrome's capture surface at the window's size
154
+ // after its next navigation. Reapply the active screen metrics on the kept session before measuring
155
+ // or starting this recording, then wait for the page to draw at them.
156
+ await restoreMetrics(p);
140
157
  // Its own session: Chrome keeps one screencast per session, and the kept session's keeps the tab drawing.
141
158
  const session = await p.context().newCDPSession(p);
142
159
  const rec = {
143
160
  page: p, file, format, exe, session, owner: state.client, startedAt: Date.now(), firstFrameAt: null,
144
161
  size: null, resizedAt: null, written: 0, dropped: 0, latest: null, ff: null, timer: null, limit: null, finishing: null,
145
162
  ffmpegError: '', starting: true, scale: 1, steps: [], step: null, stepsFile: null,
163
+ holdFrames: false, restarting: false, navigationVersion: 0, restoreError: null, mainFrameId: null, screencastParams: null,
146
164
  };
147
165
  // The size is the page's as the screen shows it, in CSS pixels (a phone's too, whose frames come in
148
166
  // its device pixels), and Chrome scales each frame to fit it. Not the first frame's: just after a
@@ -150,11 +168,39 @@ async function startRecording(p, file, seconds, format) {
150
168
  // frames have them (innerWidth has them; the viewport's clientWidth does not).
151
169
  const even = n => Math.max(2, Math.round(n) - (Math.round(n) % 2));
152
170
  try {
171
+ await session.send('Page.enable');
172
+ const tree = await session.send('Page.getFrameTree');
173
+ rec.mainFrameId = tree.frameTree.frame.id;
174
+ // Full document navigations reset Chrome's capture surface; SPA route changes do not.
175
+ session.on('Page.frameNavigated', ({ frame }) => {
176
+ if (frame.id !== rec.mainFrameId || rec.finishing) return;
177
+ rec.navigationVersion += 1;
178
+ const tab = tabState(p);
179
+ rec.holdFrames = !!(tab.viewport || tab.emulation?.device);
180
+ });
181
+ session.on('Page.domContentEventFired', async () => {
182
+ if (!rec.holdFrames || rec.finishing) return;
183
+ const version = rec.navigationVersion;
184
+ try {
185
+ await restoreMetrics(p, rec);
186
+ if (rec.finishing || version !== rec.navigationVersion) return;
187
+ await session.send('Page.stopScreencast');
188
+ if (rec.finishing || version !== rec.navigationVersion) return;
189
+ rec.holdFrames = false;
190
+ rec.restarting = true;
191
+ await session.send('Page.startScreencast', rec.screencastParams);
192
+ } catch (error) {
193
+ if (rec.finishing || p.isClosed() || version !== rec.navigationVersion) return;
194
+ rec.restoreError = error.message;
195
+ out.notice(await finishRecording(rec, `Could not restore the recording size: ${error.message}`), p);
196
+ }
197
+ });
153
198
  const { cssVisualViewport: vv } = await session.send('Page.getLayoutMetrics');
154
199
  const { result } = await session.send('Runtime.evaluate', { expression: '[innerWidth, innerHeight]', returnByValue: true });
155
200
  const [width, height] = result.value;
156
201
  rec.scale = vv.scale || 1;
157
202
  rec.size = { width: even(width * rec.scale), height: even(height * rec.scale) };
203
+ rec.screencastParams = { format: 'jpeg', quality: 85, maxWidth: rec.size.width, maxHeight: rec.size.height };
158
204
  } catch (error) {
159
205
  session.detach().catch(() => {});
160
206
  throw error;
@@ -180,6 +226,11 @@ async function startRecording(p, file, seconds, format) {
180
226
  if (!size) return;
181
227
  // A frame the size of the video, to a pixel (Chrome scales it to fit, keeping its shape).
182
228
  const fits = Math.abs(size.width - rec.size.width) <= 1 && Math.abs(size.height - rec.size.height) <= 1;
229
+ if (rec.restarting && rec.latest) {
230
+ if (!fits) { tick(); return; }
231
+ rec.restarting = false;
232
+ }
233
+ if (rec.holdFrames && rec.latest) { tick(); return; }
183
234
  if (!rec.ff) {
184
235
  rec.firstFrameAt = Date.now();
185
236
  rec.ff = spawnFfmpeg(rec);
@@ -195,7 +246,7 @@ async function startRecording(p, file, seconds, format) {
195
246
  rec.latest = image;
196
247
  });
197
248
  try {
198
- await session.send('Page.startScreencast', { format: 'jpeg', quality: 85, maxWidth: rec.size.width, maxHeight: rec.size.height });
249
+ await session.send('Page.startScreencast', rec.screencastParams);
199
250
  } catch (error) {
200
251
  delete tabState(p).recording;
201
252
  session.detach().catch(() => {});
@@ -263,7 +314,7 @@ function finishRecording(rec, why = null, { shuttingDown = false } = {}) {
263
314
  const text = typeof result === 'string' ? result : result.text;
264
315
  // A recording that saved nothing, or that ffmpeg cut short, makes record off fail rather than
265
316
  // report it as done.
266
- rec.failed = typeof result !== 'string' || !!rec.exitedEarly;
317
+ rec.failed = typeof result !== 'string' || !!rec.exitedEarly || !!rec.restoreError;
267
318
  const said = why ? `${why}. ${text}` : text;
268
319
  // A failure is kept unreported until record off (or record on) has failed with it. One that
269
320
  // ended by itself is unseen until record shows it, and the REPL stopping logs it for stop.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pw-repl",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "Text REPL for driving an existing Chromium through Playwright over CDP",
5
5
  "keywords": [
6
6
  "playwright",