pw-repl 0.8.1 → 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/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
@@ -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.1",
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",