@jbrowse/capture 5.0.0-beta.8 → 5.0.0-beta.9

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/esm/waits.js CHANGED
@@ -1,9 +1,5 @@
1
- import { describePendingDisplays, pendingDisplayStatesInPage, } from './sessionGate.js';
2
- // Fixed-duration sleep. Shared by the browser-test suites and the website
3
- // screenshot generator so the helper isn't redefined per consumer.
4
- export const delay = (ms) => new Promise(resolve => {
5
- setTimeout(resolve, ms);
6
- });
1
+ import { holdTrue } from './poll.js';
2
+ import { describePendingDisplays, pendingDisplayStatesInPage, tooLargeDisplaysInPage, } from './sessionGate.js';
7
3
  // Every best-effort wait below runs through this. They swallow their own timeout
8
4
  // on purpose — a slow-but-finishing page should not be failed for being slow,
9
5
  // and a display in a terminal state publishes no attribute to wait on at all —
@@ -37,6 +33,14 @@ export const LOADING_OVERLAY = '[data-testid="loading-overlay"]';
37
33
  * capture taken in it is a picture of an empty browser.
38
34
  */
39
35
  export const APP_READY = '[data-app-phase="ready"]';
36
+ /**
37
+ * A display drawing a transition between two settled pictures: canvas's row
38
+ * morph, a multi-way lane re-aligning. The app is finished working and every
39
+ * display reads `ready`, so neither the marker nor the phase can say so; the
40
+ * display publishes it itself, and a frame taken while it reads `true` is a
41
+ * picture no settled state ever shows.
42
+ */
43
+ export const ANIMATING_DISPLAYS = '[data-display-animating="true"]';
40
44
  /**
41
45
  * How long `ready` has to HOLD before an interaction's work counts as finished.
42
46
  *
@@ -123,11 +127,8 @@ export const PENDING_DISPLAYS = '[data-display-drawn="false"]';
123
127
  * finished badly and is showing a banner, and `ready` is a display claiming it
124
128
  * finished without drawing — a bug in the display rather than in the wait.
125
129
  *
126
- * An EMPTY census is the other answer, and `tooLarge` / `renderError` are why it
127
- * is not good news. Those two replace the display's subtree outright, so they
128
- * publish neither attribute and appear here as nothing at all: a wait on such a
129
- * display can never resolve and can never be reported. Nothing pending means
130
- * either that, or a selector that was never a display's.
130
+ * A display showing "too much data" is not pending, and a wait on its body
131
+ * can never resolve, so the census names those too.
131
132
  */
132
133
  export async function waitForSelectorAttributed(page, selector, timeoutMs) {
133
134
  try {
@@ -148,9 +149,18 @@ export async function waitForSelectorAttributed(page, selector, timeoutMs) {
148
149
  export async function describePendingDisplaysNow(page) {
149
150
  try {
150
151
  const pending = await page.evaluate(pendingDisplayStatesInPage);
151
- return pending.length
152
- ? `${pending.length} display(s) had not painted: ${describePendingDisplays(pending)}`
153
- : 'no display reported itself unpainted, so either the selector was not on a display or the display is in a terminal phase that publishes nothing (tooLarge, renderError)';
152
+ const tooLarge = await page.evaluate(tooLargeDisplaysInPage);
153
+ const found = [
154
+ pending.length
155
+ ? `${pending.length} display(s) had not painted: ${describePendingDisplays(pending)}`
156
+ : undefined,
157
+ tooLarge.length
158
+ ? `${tooLarge.length} display(s) show "too much data" in place of their features: ${describePendingDisplays(tooLarge)}`
159
+ : undefined,
160
+ ].filter(Boolean);
161
+ return found.length
162
+ ? found.join('; ')
163
+ : 'no display reported itself unpainted, so the selector was not on a display';
154
164
  }
155
165
  catch {
156
166
  return 'the page could not be queried afterwards (context gone)';
@@ -176,8 +186,6 @@ export const displayPainted = (testid) => `[data-testid="${testid}"][data-displa
176
186
  * on the phase instead.
177
187
  */
178
188
  export const displaySettled = (testid) => `[data-testid="${testid}"][data-display-phase="ready"]`;
179
- /** One display, by its config's `displayId` rather than by type. */
180
- export const displayById = (displayId) => `[data-display-id="${displayId}"]`;
181
189
  // Wait until no display is still pending its first paint AND could still reach
182
190
  // it: a pending display publishing `loading`, or publishing no phase at all (a
183
191
  // build older than the attribute), is still coming; a terminal phase is not. The
@@ -210,10 +218,8 @@ export function waitForDisplaysDone(page, timeoutMs) {
210
218
  // DisplayPhase, whose `loading` term covers the entire fetch, so "nothing is
211
219
  // loading" is a direct read rather than an inference.
212
220
  //
213
- // Terminal phases (`tooLarge`, `renderError`) replace the display subtree and
214
- // publish no attribute, so they resolve immediately here — correct, since they
215
- // are finished, not pending. They're caught as *content* by the caller's own
216
- // settled check, not by this wait.
221
+ // Terminal phases (`tooLarge`, `renderError`) resolve immediately here, since
222
+ // they are finished, not pending. The caller's census reports them.
217
223
  //
218
224
  // Best-effort like its neighbours: a display that never leaves `loading` should
219
225
  // fail loudly through that settled check, with the frame to look at, rather than
@@ -221,26 +227,25 @@ export function waitForDisplaysDone(page, timeoutMs) {
221
227
  export function waitForDisplayPhases(page, timeoutMs) {
222
228
  return settled(page.waitForFunction(() => document.querySelector('[data-display-phase="loading"]') === null, { timeout: timeoutMs, polling: 'mutation' }));
223
229
  }
230
+ /**
231
+ * A view whose MODEL is initialized but whose lazily-imported React component
232
+ * has not arrived: ViewWrapper's Suspense spinner. The app marker cannot see
233
+ * it, since the model it reads is finished, so a view clicked into existence
234
+ * reads `ready` for as long as its chunk takes to load.
235
+ */
236
+ const VIEW_COMPONENT_PENDING = '[data-view-component-pending]';
224
237
  // The view-level counterpart: ViewContainer publishes `data-view-phase` from the
225
238
  // view model's own phase, and `loading` means the view is still waiting on its
226
239
  // assembly (or on init's navigation) and has mounted no displays at all. Every
227
240
  // display-level wait above is silent in that state — there is nothing to be
228
241
  // loading yet — so a capture taken then lands on a bare spinner.
229
242
  //
230
- // The `data-view-component-pending` half is the case the phase attribute cannot
231
- // report: a view whose MODEL is initialized but whose lazily-imported React
232
- // component has not arrived, so ViewContainer publishes a non-loading phase over
233
- // a body that is still ViewWrapper's Suspense spinner. A session loaded at page
234
- // load has usually won that race by the time anything else settles; a view
235
- // CLICKED into existence fetches its chunk only then, and its frame is the one a
236
- // launch-dialog figure is about.
237
- //
238
243
  // NOT best-effort, unlike its neighbours: a view that never leaves `loading` has
239
244
  // no content to fall through to, so the timeout IS the diagnosis and the caller
240
245
  // should surface it.
241
246
  export function waitForViewPhases(page, timeoutMs) {
242
- return page.waitForFunction(() => document.querySelector('[data-view-phase="loading"]') === null &&
243
- document.querySelector('[data-view-component-pending]') === null, { timeout: timeoutMs, polling: 'mutation' });
247
+ return page.waitForFunction((pending) => document.querySelector('[data-view-phase="loading"]') === null &&
248
+ document.querySelector(pending) === null, { timeout: timeoutMs, polling: 'mutation' }, VIEW_COMPONENT_PENDING);
244
249
  }
245
250
  /**
246
251
  * Everything the app publishes to say it is working, as one selector.
@@ -255,12 +260,14 @@ export function waitForViewPhases(page, timeoutMs) {
255
260
  * wherever it tells a user it is working
256
261
  * `data-display-phase` one display's own fetch, newer builds
257
262
  * `data-view-phase` a view still resolving its assembly, newer builds
263
+ * `data-display-animating` a display drawing a morph, newer builds
258
264
  */
259
265
  export const BUSY_SELECTOR = [
260
266
  LOADING_OVERLAY,
261
267
  '[data-busy="true"]',
262
268
  '[data-display-phase="loading"]',
263
269
  '[data-view-phase="loading"]',
270
+ ANIMATING_DISPLAYS,
264
271
  ].join(', ');
265
272
  /**
266
273
  * Is the app doing anything right now?
@@ -294,33 +301,12 @@ export function isPageBusyInPage(busySelector) {
294
301
  * "nothing is in flight right now", which is what a probe or a benchmark wants
295
302
  * between two measured actions.
296
303
  *
297
- * Polled from node rather than in-page: chrome throttles in-page timers and rAF
298
- * once the tab is not visible, which is the state a headless capture sits in.
299
- *
300
304
  * Returns false on timeout rather than throwing, like its neighbours.
301
305
  */
302
- export async function waitForQuietPeriod(page, { quietMs = 1500, timeout = 30000, pollMs = 250, } = {}) {
303
- const deadline = Date.now() + timeout;
304
- let quietSince;
305
- while (Date.now() < deadline) {
306
- // A page that navigates or closes under us fails the evaluate; treat that
307
- // as busy and let the deadline decide, rather than reporting quiet.
308
- const busy = await page
309
- .evaluate(isPageBusyInPage, BUSY_SELECTOR)
310
- .catch(() => true);
311
- const now = Date.now();
312
- if (busy) {
313
- quietSince = undefined;
314
- }
315
- else {
316
- quietSince ??= now;
317
- if (now - quietSince >= quietMs) {
318
- return true;
319
- }
320
- }
321
- await delay(pollMs);
322
- }
323
- return false;
306
+ export function waitForQuietPeriod(page, { quietMs = 1500, timeout = 30000, pollMs = 250, } = {}) {
307
+ // a page that navigates or closes under us fails the evaluate, which reads
308
+ // as busy and leaves the deadline to decide
309
+ return holdTrue(() => page.evaluate(isPageBusyInPage, BUSY_SELECTOR).then(busy => !busy, () => false), { holdMs: quietMs, timeout, pollMs });
324
310
  }
325
311
  /**
326
312
  * Wait for the FIRST frame the app says it has finished.
@@ -336,8 +322,7 @@ export async function waitForQuietPeriod(page, { quietMs = 1500, timeout = 30000
336
322
  export function waitForAppReady(page, { timeout = 30000 } = {}) {
337
323
  return settled(page.waitForSelector(APP_READY, { timeout, visible: false }));
338
324
  }
339
- /** Whether this build publishes the app-level readiness marker at all. */
340
- export function hasAppReadyMarker(page) {
325
+ function hasAppReadyMarker(page) {
341
326
  return page.evaluate(() => document.querySelector('[data-app-phase]') !== null);
342
327
  }
343
328
  /**
@@ -364,9 +349,8 @@ export function hasAppReadyMarker(page) {
364
349
  * replaced. Requiring the idle to HOLD costs the hold and no more, and catches
365
350
  * the same late-starting work.
366
351
  *
367
- * Polled from Node rather than by `page.waitForSelector`, for the reason
368
- * `waitForQuietPeriod` is: chrome throttles in-page timers and rAF once the tab
369
- * is not visible, which is exactly the state a headless capture sits in.
352
+ * The hold also requires no view body to be waiting on its lazy component and
353
+ * no display to be animating, neither of which the marker can see.
370
354
  *
371
355
  * Throws on a build too old for the marker rather than falling back — see the
372
356
  * body.
@@ -382,25 +366,9 @@ export async function waitForAppSettled(page, { timeout = 30000, holdMs = APP_SE
382
366
  'to wait for — every other readiness attribute is an absence an app ' +
383
367
  'that has not started also satisfies.');
384
368
  }
385
- const deadline = Date.now() + timeout;
386
- let readySince;
387
- while (Date.now() < deadline) {
388
- // a page that navigates or closes under us fails the evaluate; treat that as
389
- // not-ready and let the deadline decide
390
- const ready = await page
391
- .evaluate(selector => document.querySelector(selector) !== null, APP_READY)
392
- .catch(() => false);
393
- const now = Date.now();
394
- if (ready) {
395
- readySince ??= now;
396
- if (now - readySince >= holdMs) {
397
- return true;
398
- }
399
- }
400
- else {
401
- readySince = undefined;
402
- }
403
- await delay(pollMs);
404
- }
405
- return false;
369
+ return holdTrue(() => page
370
+ .evaluate((ready, pending, animating) => document.querySelector(ready) !== null &&
371
+ document.querySelector(pending) === null &&
372
+ document.querySelector(animating) === null, APP_READY, VIEW_COMPONENT_PENDING, ANIMATING_DISPLAYS)
373
+ .catch(() => false), { holdMs, timeout, pollMs });
406
374
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jbrowse/capture",
3
- "version": "5.0.0-beta.8",
3
+ "version": "5.0.0-beta.9",
4
4
  "description": "Drive a JBrowse 2 instance with Puppeteer and screenshot it once it has actually finished rendering",
5
5
  "keywords": [
6
6
  "jbrowse",
@@ -31,10 +31,10 @@
31
31
  ],
32
32
  "sideEffects": false,
33
33
  "dependencies": {
34
- "puppeteer": "^25.10.0"
34
+ "puppeteer": "^25.11.0"
35
35
  },
36
36
  "devDependencies": {
37
- "@jbrowse/core": "^5.0.0-beta.8"
37
+ "@jbrowse/core": "^5.0.0-beta.9"
38
38
  },
39
39
  "publishConfig": {
40
40
  "access": "public"