@jbrowse/capture 4.3.0
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/LICENSE +201 -0
- package/README.md +124 -0
- package/esm/args.d.ts +30 -0
- package/esm/args.js +79 -0
- package/esm/bin.d.ts +2 -0
- package/esm/bin.js +165 -0
- package/esm/browser.d.ts +4 -0
- package/esm/browser.js +53 -0
- package/esm/capture.d.ts +113 -0
- package/esm/capture.js +273 -0
- package/esm/hub.d.ts +23 -0
- package/esm/hub.js +43 -0
- package/esm/index.d.ts +10 -0
- package/esm/index.js +7 -0
- package/esm/session.d.ts +25 -0
- package/esm/session.js +61 -0
- package/esm/sessionGate.d.ts +126 -0
- package/esm/sessionGate.js +196 -0
- package/esm/url.d.ts +39 -0
- package/esm/url.js +44 -0
- package/esm/waits.d.ts +214 -0
- package/esm/waits.js +428 -0
- package/package.json +51 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { Page } from 'puppeteer';
|
|
2
|
+
export interface SessionExpectations {
|
|
3
|
+
/** Assembly that must be open on some view. Usually the one you navigated to. */
|
|
4
|
+
assembly?: string;
|
|
5
|
+
/**
|
|
6
|
+
* trackIds that must all be open. The exact ids, not a count: a hosted config
|
|
7
|
+
* usually ships a defaultSession, so `&tracks=` ADDS to tracks that are
|
|
8
|
+
* already there and a count is satisfied before your track arrives — or by
|
|
9
|
+
* the default set alone when the id you passed does not exist at all.
|
|
10
|
+
*/
|
|
11
|
+
trackIds?: string[];
|
|
12
|
+
}
|
|
13
|
+
export interface SessionSummary {
|
|
14
|
+
views: number;
|
|
15
|
+
assemblies: string[];
|
|
16
|
+
trackIds: string[];
|
|
17
|
+
}
|
|
18
|
+
/** What the page currently has open, or undefined if there is no session yet. */
|
|
19
|
+
export declare function readSessionSummary(page: Page): Promise<SessionSummary | undefined>;
|
|
20
|
+
export declare function readSessionSummaryInPage(): SessionSummary | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* Wait until the session exists, every view reports itself initialized, and the
|
|
23
|
+
* assembly and tracks that were asked for are actually open.
|
|
24
|
+
*
|
|
25
|
+
* Throws on timeout rather than proceeding. A config URL that 404s, a trackId
|
|
26
|
+
* that does not exist in the config, and an assembly name that does not match
|
|
27
|
+
* the one the config declares all fail here, which is the only place they fail
|
|
28
|
+
* at all — each of them otherwise produces a browser that loads, paints its
|
|
29
|
+
* chrome, and photographs beautifully with nothing in it.
|
|
30
|
+
*/
|
|
31
|
+
export declare function waitForSession(page: Page, { assembly, trackIds, timeout, }?: SessionExpectations & {
|
|
32
|
+
timeout?: number;
|
|
33
|
+
}): Promise<void>;
|
|
34
|
+
export declare const PAINT_CONTRACT_NOTE: string;
|
|
35
|
+
/**
|
|
36
|
+
* Whether every open display's paint state was actually measurable.
|
|
37
|
+
*
|
|
38
|
+
* The per-display paint attributes are not universal. Measured on 2026-08-07:
|
|
39
|
+
* jbrowse.org/code/jb2/main publishes `data-view-phase`, `data-display-phase`
|
|
40
|
+
* and `data-display-drawn`; jbrowse.org/code/jb2/latest — the released build,
|
|
41
|
+
* which is what every genomes.jbrowse.org link and every docs figure link opens
|
|
42
|
+
* — publishes none of them and exposes only the loading overlay. Against that
|
|
43
|
+
* instance the display-level waits are unfalsifiable rather than satisfied, so a
|
|
44
|
+
* caller has to be told which of the two it got instead of reading "0 displays
|
|
45
|
+
* pending" as good news.
|
|
46
|
+
*
|
|
47
|
+
* The absence of the attribute is NOT on its own the answer, which is the trap
|
|
48
|
+
* here: a page with no tracks open — an import form, a menu shot, a bare view —
|
|
49
|
+
* publishes none of it either, and reporting that as "this build cannot measure
|
|
50
|
+
* paint" is both wrong and alarming. So this asks the question only when there
|
|
51
|
+
* is something to measure, and answers true when there is not.
|
|
52
|
+
*
|
|
53
|
+
* Call it AFTER the session gate, so "no tracks open" means the page genuinely
|
|
54
|
+
* has none rather than not having got there yet.
|
|
55
|
+
*/
|
|
56
|
+
export declare function hasPaintContract(page: Page): Promise<boolean>;
|
|
57
|
+
/**
|
|
58
|
+
* Which readiness attributes this build publishes at all.
|
|
59
|
+
*
|
|
60
|
+
* `hasPaintContract` answers the same question for one attribute and folds in
|
|
61
|
+
* "there was nothing to measure"; this is the raw read of all three, so the
|
|
62
|
+
* chain can adapt to the build BEFORE it starts waiting rather than reporting
|
|
63
|
+
* afterwards what it could not see.
|
|
64
|
+
*
|
|
65
|
+
* Every wait keyed on one of these is NEGATIVE — it passes when the selector is
|
|
66
|
+
* absent — so on a build that publishes none of them all three are satisfied by
|
|
67
|
+
* a page that has not begun to draw. Read this after the session gate and treat
|
|
68
|
+
* `false` as "this signal is unavailable", never as "this signal says done".
|
|
69
|
+
*/
|
|
70
|
+
export interface Instrumentation {
|
|
71
|
+
viewPhase: boolean;
|
|
72
|
+
displayPhase: boolean;
|
|
73
|
+
displayDrawn: boolean;
|
|
74
|
+
}
|
|
75
|
+
/** Serialized into the page; exported so a test can call the real function. */
|
|
76
|
+
export declare function readInstrumentationInPage(): Instrumentation;
|
|
77
|
+
export declare function readInstrumentation(page: Page): Promise<Instrumentation>;
|
|
78
|
+
/**
|
|
79
|
+
* Displays that were still reporting unpainted at the moment of the call.
|
|
80
|
+
*
|
|
81
|
+
* Distinct from `unsettled`, which says a wait ran out of time. This says what
|
|
82
|
+
* the page looked like when the shutter fired, and the two do not imply each
|
|
83
|
+
* other: a display can go back to pending after its stage passed. An empty
|
|
84
|
+
* result means nothing at all on a build with no paint contract — check
|
|
85
|
+
* `paintContract` before reading it as good news.
|
|
86
|
+
*/
|
|
87
|
+
export declare function pendingDisplays(page: Page): Promise<string[]>;
|
|
88
|
+
/** One unpainted display and what it says about itself. */
|
|
89
|
+
export interface PendingDisplay {
|
|
90
|
+
/** the display TYPE, shared by every instance of it */
|
|
91
|
+
name: string;
|
|
92
|
+
/** which instance, where the display publishes `data-display-id` */
|
|
93
|
+
id?: string;
|
|
94
|
+
/**
|
|
95
|
+
* Its own phase, or undefined on a build too old to publish one. `loading` is
|
|
96
|
+
* a display still fetching; `ready` is the one that earns this census, since
|
|
97
|
+
* a display that says it has finished while reporting no paint is a bug in
|
|
98
|
+
* the display rather than a slow page.
|
|
99
|
+
*/
|
|
100
|
+
phase?: string;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The same census, with each display's own account of itself.
|
|
104
|
+
*
|
|
105
|
+
* `pendingDisplays` above answers WHICH, and that was the whole report: every
|
|
106
|
+
* timeout read the same however it was caused. The phase separates the cases a
|
|
107
|
+
* bare name runs together —
|
|
108
|
+
*
|
|
109
|
+
* `loading` still fetching. A slow page or a fetch that never returns.
|
|
110
|
+
* `error` finished, badly. Nothing is coming; the picture is a banner.
|
|
111
|
+
* `ready` it says it is done and reports no paint. That is the display's
|
|
112
|
+
* bug, not the wait's, and it is the one a longer timeout will
|
|
113
|
+
* never fix.
|
|
114
|
+
* absent the element publishes no phase: a build older than the
|
|
115
|
+
* attribute, or a surface that never grew one.
|
|
116
|
+
*
|
|
117
|
+
* Serialized into the page, so a test can call the real function. Read fresh
|
|
118
|
+
* from the DOM at report time rather than from handles the waits held. A handle to an element that has since re-rendered throws
|
|
119
|
+
* `Node is detached from document`, which is how the previous attempt at this
|
|
120
|
+
* turned four diagnosable timeouts into nine opaque puppeteer errors (reverted
|
|
121
|
+
* in 28c6ee6d90).
|
|
122
|
+
*/
|
|
123
|
+
export declare function pendingDisplayStatesInPage(): PendingDisplay[];
|
|
124
|
+
export declare function pendingDisplayStates(page: Page): Promise<PendingDisplay[]>;
|
|
125
|
+
/** `pendingDisplayStates` as one line for an error message. */
|
|
126
|
+
export declare function describePendingDisplays(pending: PendingDisplay[]): string;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/** What the page currently has open, or undefined if there is no session yet. */
|
|
2
|
+
export function readSessionSummary(page) {
|
|
3
|
+
return page.evaluate(readSessionSummaryInPage);
|
|
4
|
+
}
|
|
5
|
+
// Serialized into the page, so it can only call what it declares — hence the
|
|
6
|
+
// inlined copy of readViews rather than a shared import.
|
|
7
|
+
//
|
|
8
|
+
// Exported for its test: it reads `globalThis.JBrowseSession` and nothing else,
|
|
9
|
+
// so calling it in node against a stubbed global exercises the very function
|
|
10
|
+
// puppeteer serializes, rather than a copy of it that can drift.
|
|
11
|
+
export function readSessionSummaryInPage() {
|
|
12
|
+
const session = globalThis.JBrowseSession;
|
|
13
|
+
const views = session?.views;
|
|
14
|
+
if (!views) {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
// Inlined rather than shared with the gate below for the same reason as the
|
|
18
|
+
// readViews copy: both are serialized into the page.
|
|
19
|
+
function collect(v) {
|
|
20
|
+
return [
|
|
21
|
+
...(v.tracks ?? []),
|
|
22
|
+
...(v.levels ?? []).flatMap(l => l.tracks ?? []),
|
|
23
|
+
...(v.views ?? []).flatMap(collect),
|
|
24
|
+
];
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
views: views.length,
|
|
28
|
+
assemblies: [
|
|
29
|
+
...new Set(views.flatMap(function asm(v) {
|
|
30
|
+
return [...(v.assemblyNames ?? []), ...(v.views ?? []).flatMap(asm)];
|
|
31
|
+
})),
|
|
32
|
+
],
|
|
33
|
+
trackIds: views.flatMap(v => collect(v).map(t => t.configuration?.trackId ?? '(unnamed)')),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Wait until the session exists, every view reports itself initialized, and the
|
|
38
|
+
* assembly and tracks that were asked for are actually open.
|
|
39
|
+
*
|
|
40
|
+
* Throws on timeout rather than proceeding. A config URL that 404s, a trackId
|
|
41
|
+
* that does not exist in the config, and an assembly name that does not match
|
|
42
|
+
* the one the config declares all fail here, which is the only place they fail
|
|
43
|
+
* at all — each of them otherwise produces a browser that loads, paints its
|
|
44
|
+
* chrome, and photographs beautifully with nothing in it.
|
|
45
|
+
*/
|
|
46
|
+
export async function waitForSession(page, { assembly, trackIds = [], timeout = 60000, } = {}) {
|
|
47
|
+
try {
|
|
48
|
+
// #region session-gate
|
|
49
|
+
await page.waitForFunction((wantAssembly, wantTracks) => {
|
|
50
|
+
const session = globalThis.JBrowseSession;
|
|
51
|
+
const views = session?.views;
|
|
52
|
+
if (!views?.length) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
// `initialized` is an LGV getter; a view type without one is mounted
|
|
56
|
+
// content the moment it exists, so absent counts as initialized and
|
|
57
|
+
// only an explicit false is pending.
|
|
58
|
+
if (views.some(v => v.initialized === false)) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
// A container view (synteny, dotplot) keeps its assemblies on the rows
|
|
62
|
+
// and its tracks on the levels, so both walks descend into sub-views
|
|
63
|
+
// and levels rather than reading the top view only.
|
|
64
|
+
const asmOf = (v) => [
|
|
65
|
+
...(v.assemblyNames ?? []),
|
|
66
|
+
...(v.views ?? []).flatMap(asmOf),
|
|
67
|
+
];
|
|
68
|
+
if (wantAssembly !== null &&
|
|
69
|
+
!views.some(v => asmOf(v).includes(wantAssembly))) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
const tracksOf = (v) => [
|
|
73
|
+
...(v.tracks ?? []),
|
|
74
|
+
...(v.levels ?? []).flatMap(l => l.tracks ?? []),
|
|
75
|
+
...(v.views ?? []).flatMap(tracksOf),
|
|
76
|
+
];
|
|
77
|
+
const open = new Set(views.flatMap(v => tracksOf(v).map(t => t.configuration?.trackId)));
|
|
78
|
+
return wantTracks.every(id => open.has(id));
|
|
79
|
+
}, { timeout, polling: 250 }, assembly ?? null, trackIds);
|
|
80
|
+
// #endregion session-gate
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
const summary = await readSessionSummary(page);
|
|
84
|
+
const found = summary
|
|
85
|
+
? `${summary.views} view(s), assemblies [${summary.assemblies.join(', ')}], tracks [${summary.trackIds.join(', ')}]`
|
|
86
|
+
: 'no session on the page at all (is this a jbrowse-web instance?)';
|
|
87
|
+
const missing = summary
|
|
88
|
+
? trackIds.filter(id => !summary.trackIds.includes(id))
|
|
89
|
+
: trackIds;
|
|
90
|
+
const wanted = [
|
|
91
|
+
assembly ? `assembly "${assembly}"` : undefined,
|
|
92
|
+
missing.length ? `track(s) [${missing.join(', ')}]` : undefined,
|
|
93
|
+
]
|
|
94
|
+
.filter(Boolean)
|
|
95
|
+
.join(' and ');
|
|
96
|
+
throw new Error(`the session never reached the requested state after ${timeout}ms. ` +
|
|
97
|
+
`Wanted ${wanted || 'an initialized view'}; found ${found}. ` +
|
|
98
|
+
'A config URL that 404s, a trackId the config does not define, or an ' +
|
|
99
|
+
'assembly name that does not match the config all look like this.');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
export const PAINT_CONTRACT_NOTE = 'this JBrowse build publishes no data-display-drawn attributes, so "every ' +
|
|
103
|
+
'display has painted" cannot be checked — only that nothing is still ' +
|
|
104
|
+
'loading. Raise --settle if the image looks half-drawn.';
|
|
105
|
+
/**
|
|
106
|
+
* Whether every open display's paint state was actually measurable.
|
|
107
|
+
*
|
|
108
|
+
* The per-display paint attributes are not universal. Measured on 2026-08-07:
|
|
109
|
+
* jbrowse.org/code/jb2/main publishes `data-view-phase`, `data-display-phase`
|
|
110
|
+
* and `data-display-drawn`; jbrowse.org/code/jb2/latest — the released build,
|
|
111
|
+
* which is what every genomes.jbrowse.org link and every docs figure link opens
|
|
112
|
+
* — publishes none of them and exposes only the loading overlay. Against that
|
|
113
|
+
* instance the display-level waits are unfalsifiable rather than satisfied, so a
|
|
114
|
+
* caller has to be told which of the two it got instead of reading "0 displays
|
|
115
|
+
* pending" as good news.
|
|
116
|
+
*
|
|
117
|
+
* The absence of the attribute is NOT on its own the answer, which is the trap
|
|
118
|
+
* here: a page with no tracks open — an import form, a menu shot, a bare view —
|
|
119
|
+
* publishes none of it either, and reporting that as "this build cannot measure
|
|
120
|
+
* paint" is both wrong and alarming. So this asks the question only when there
|
|
121
|
+
* is something to measure, and answers true when there is not.
|
|
122
|
+
*
|
|
123
|
+
* Call it AFTER the session gate, so "no tracks open" means the page genuinely
|
|
124
|
+
* has none rather than not having got there yet.
|
|
125
|
+
*/
|
|
126
|
+
export async function hasPaintContract(page) {
|
|
127
|
+
const summary = await readSessionSummary(page);
|
|
128
|
+
if (summary && summary.trackIds.length === 0) {
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
return page.evaluate(() => document.querySelector('[data-display-drawn]') !== null);
|
|
132
|
+
}
|
|
133
|
+
/** Serialized into the page; exported so a test can call the real function. */
|
|
134
|
+
export function readInstrumentationInPage() {
|
|
135
|
+
return {
|
|
136
|
+
viewPhase: document.querySelector('[data-view-phase]') !== null,
|
|
137
|
+
displayPhase: document.querySelector('[data-display-phase]') !== null,
|
|
138
|
+
displayDrawn: document.querySelector('[data-display-drawn]') !== null,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
export function readInstrumentation(page) {
|
|
142
|
+
return page.evaluate(readInstrumentationInPage);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Displays that were still reporting unpainted at the moment of the call.
|
|
146
|
+
*
|
|
147
|
+
* Distinct from `unsettled`, which says a wait ran out of time. This says what
|
|
148
|
+
* the page looked like when the shutter fired, and the two do not imply each
|
|
149
|
+
* other: a display can go back to pending after its stage passed. An empty
|
|
150
|
+
* result means nothing at all on a build with no paint contract — check
|
|
151
|
+
* `paintContract` before reading it as good news.
|
|
152
|
+
*/
|
|
153
|
+
export function pendingDisplays(page) {
|
|
154
|
+
return page.evaluate(() => [
|
|
155
|
+
...document.querySelectorAll('[data-display-drawn="false"]'),
|
|
156
|
+
].map(el => el.dataset.testid ?? (el.id || 'unnamed display')));
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* The same census, with each display's own account of itself.
|
|
160
|
+
*
|
|
161
|
+
* `pendingDisplays` above answers WHICH, and that was the whole report: every
|
|
162
|
+
* timeout read the same however it was caused. The phase separates the cases a
|
|
163
|
+
* bare name runs together —
|
|
164
|
+
*
|
|
165
|
+
* `loading` still fetching. A slow page or a fetch that never returns.
|
|
166
|
+
* `error` finished, badly. Nothing is coming; the picture is a banner.
|
|
167
|
+
* `ready` it says it is done and reports no paint. That is the display's
|
|
168
|
+
* bug, not the wait's, and it is the one a longer timeout will
|
|
169
|
+
* never fix.
|
|
170
|
+
* absent the element publishes no phase: a build older than the
|
|
171
|
+
* attribute, or a surface that never grew one.
|
|
172
|
+
*
|
|
173
|
+
* Serialized into the page, so a test can call the real function. Read fresh
|
|
174
|
+
* from the DOM at report time rather than from handles the waits held. A handle to an element that has since re-rendered throws
|
|
175
|
+
* `Node is detached from document`, which is how the previous attempt at this
|
|
176
|
+
* turned four diagnosable timeouts into nine opaque puppeteer errors (reverted
|
|
177
|
+
* in 28c6ee6d90).
|
|
178
|
+
*/
|
|
179
|
+
export function pendingDisplayStatesInPage() {
|
|
180
|
+
return [
|
|
181
|
+
...document.querySelectorAll('[data-display-drawn="false"]'),
|
|
182
|
+
].map(el => ({
|
|
183
|
+
name: el.dataset.testid ?? (el.id || 'unnamed display'),
|
|
184
|
+
id: el.dataset.displayId,
|
|
185
|
+
phase: el.dataset.displayPhase,
|
|
186
|
+
}));
|
|
187
|
+
}
|
|
188
|
+
export function pendingDisplayStates(page) {
|
|
189
|
+
return page.evaluate(pendingDisplayStatesInPage);
|
|
190
|
+
}
|
|
191
|
+
/** `pendingDisplayStates` as one line for an error message. */
|
|
192
|
+
export function describePendingDisplays(pending) {
|
|
193
|
+
return pending
|
|
194
|
+
.map(d => `${d.name}${d.id ? ` (${d.id})` : ''} is ${d.phase ?? 'in an unpublished phase'}`)
|
|
195
|
+
.join('; ');
|
|
196
|
+
}
|
package/esm/url.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export declare const PUBLIC_INSTANCE = "https://jbrowse.org/code/jb2/latest/";
|
|
2
|
+
export interface JBrowseUrlOptions {
|
|
3
|
+
/** A genomes.jbrowse.org assembly: a UCSC db name (`hg38`) or GenArk accession. */
|
|
4
|
+
hub?: string;
|
|
5
|
+
/** A config.json URL, for data that is not on genomes.jbrowse.org. Wins over `hub`. */
|
|
6
|
+
config?: string;
|
|
7
|
+
/** Assembly to open. Defaults to `hub`, whose config names its assembly after it. */
|
|
8
|
+
assembly?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Where to open. A locstring (`chr1:1,000-2,000`, several space-separated for
|
|
11
|
+
* a discontinuous view), or — on a config that ships a text index, which every
|
|
12
|
+
* UCSC assembly on genomes.jbrowse.org does — a feature name such as `BRCA1`.
|
|
13
|
+
*/
|
|
14
|
+
loc?: string;
|
|
15
|
+
/** trackIds from the config to open. */
|
|
16
|
+
tracks?: string[];
|
|
17
|
+
/** The full session spec, for anything the three fields above cannot say. */
|
|
18
|
+
session?: object;
|
|
19
|
+
/** Name the session carries once opened. */
|
|
20
|
+
sessionName?: string;
|
|
21
|
+
/** JBrowse Web deployment to load. Defaults to the public one. */
|
|
22
|
+
instance?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The URL that opens a JBrowse Web instance onto a described view. Useful on its
|
|
26
|
+
* own: an agent can hand this to a human, or paste it into a report, without
|
|
27
|
+
* launching a browser at all.
|
|
28
|
+
*
|
|
29
|
+
* Two ways to say where to go, and they are not interchangeable. `assembly` /
|
|
30
|
+
* `loc` / `tracks` become the [URL parameters](https://jbrowse.org/jb2/docs/urlparams/),
|
|
31
|
+
* which route `loc` through the config's text-search index, so a **gene name**
|
|
32
|
+
* works there. `session` becomes a
|
|
33
|
+
* [session spec](https://jbrowse.org/jb2/docs/urlparams/#session-spec), which
|
|
34
|
+
* can describe several views and per-display settings but whose `init.loc` is
|
|
35
|
+
* parsed as a locstring only and throws on a gene name. Pass one or the other;
|
|
36
|
+
* `session` wins, and the URL params are dropped rather than silently starting
|
|
37
|
+
* the fresh session that `&loc=` otherwise forces.
|
|
38
|
+
*/
|
|
39
|
+
export declare function jbrowseUrl({ hub, config, assembly, loc, tracks, session, sessionName, instance, }: JBrowseUrlOptions): string;
|
package/esm/url.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { hubUrl } from './hub.js';
|
|
2
|
+
import { sessionSpecParam } from './session.js';
|
|
3
|
+
// The public JBrowse Web build the docs' own figure links point at. Any other
|
|
4
|
+
// deployment works the same way, including a local `npx serve` of a build.
|
|
5
|
+
export const PUBLIC_INSTANCE = 'https://jbrowse.org/code/jb2/latest/';
|
|
6
|
+
/**
|
|
7
|
+
* The URL that opens a JBrowse Web instance onto a described view. Useful on its
|
|
8
|
+
* own: an agent can hand this to a human, or paste it into a report, without
|
|
9
|
+
* launching a browser at all.
|
|
10
|
+
*
|
|
11
|
+
* Two ways to say where to go, and they are not interchangeable. `assembly` /
|
|
12
|
+
* `loc` / `tracks` become the [URL parameters](https://jbrowse.org/jb2/docs/urlparams/),
|
|
13
|
+
* which route `loc` through the config's text-search index, so a **gene name**
|
|
14
|
+
* works there. `session` becomes a
|
|
15
|
+
* [session spec](https://jbrowse.org/jb2/docs/urlparams/#session-spec), which
|
|
16
|
+
* can describe several views and per-display settings but whose `init.loc` is
|
|
17
|
+
* parsed as a locstring only and throws on a gene name. Pass one or the other;
|
|
18
|
+
* `session` wins, and the URL params are dropped rather than silently starting
|
|
19
|
+
* the fresh session that `&loc=` otherwise forces.
|
|
20
|
+
*/
|
|
21
|
+
export function jbrowseUrl({ hub, config, assembly = hub, loc, tracks, session, sessionName, instance = PUBLIC_INSTANCE, }) {
|
|
22
|
+
const configUrl = config ?? (hub ? hubUrl(hub) : undefined);
|
|
23
|
+
const url = new URL(instance);
|
|
24
|
+
// URL's own encoder, not a hand-built query string: a config URL contains
|
|
25
|
+
// `://` and `?`, and a session spec is a whole JSON document, so appending
|
|
26
|
+
// either raw produces a link that loads a truncated config and reports
|
|
27
|
+
// nothing.
|
|
28
|
+
const set = (key, value) => {
|
|
29
|
+
if (value) {
|
|
30
|
+
url.searchParams.set(key, value);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
set('config', configUrl);
|
|
34
|
+
if (session) {
|
|
35
|
+
set('session', sessionSpecParam(session));
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
set('assembly', assembly);
|
|
39
|
+
set('loc', loc);
|
|
40
|
+
set('tracks', tracks?.join(','));
|
|
41
|
+
}
|
|
42
|
+
set('sessionName', sessionName);
|
|
43
|
+
return url.href;
|
|
44
|
+
}
|
package/esm/waits.d.ts
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import type { ElementHandle, Page } from 'puppeteer';
|
|
2
|
+
export declare const delay: (ms: number) => Promise<void>;
|
|
3
|
+
export declare const LOADING_OVERLAY = "[data-testid=\"loading-overlay\"]";
|
|
4
|
+
/**
|
|
5
|
+
* The whole readiness contract, in one selector.
|
|
6
|
+
*
|
|
7
|
+
* `AppReadyMarker` renders it from the session: it reads `ready` when no view
|
|
8
|
+
* is resolving an assembly and no display is fetching, and `loading` whenever
|
|
9
|
+
* one is. Waiting for it is the entire job — everything else in this module is
|
|
10
|
+
* either a narrower question (has THIS display painted) or a fallback for a
|
|
11
|
+
* deployment too old to publish it.
|
|
12
|
+
*
|
|
13
|
+
* It is POSITIVE, which is the property that matters. Every other signal here
|
|
14
|
+
* is an absence, and an absence is equally true of an app that has not started:
|
|
15
|
+
* measured on a two-track session, the gap between "the session holds the
|
|
16
|
+
* tracks" and "the first loading indicator appears" is about a second, and a
|
|
17
|
+
* capture taken in it is a picture of an empty browser.
|
|
18
|
+
*/
|
|
19
|
+
export declare const APP_READY = "[data-app-phase=\"ready\"]";
|
|
20
|
+
export declare function waitForLoadingComplete(page: Page, { timeout, waitForDownloads, }?: {
|
|
21
|
+
timeout?: number;
|
|
22
|
+
waitForDownloads?: boolean;
|
|
23
|
+
}): Promise<boolean>;
|
|
24
|
+
/**
|
|
25
|
+
* Wait until nothing on the page is in flight.
|
|
26
|
+
*
|
|
27
|
+
* Every signal it reads is one the app publishes deliberately — the loading
|
|
28
|
+
* overlay, `data-busy`, the display and view phases, and each display's own
|
|
29
|
+
* status message on the session model. See `isPageBusyInPage`.
|
|
30
|
+
*
|
|
31
|
+
* It used to scan rendered text for /^(loading|rendering|computing)/ and resolve
|
|
32
|
+
* `getComputedStyle` on each match to decide whether it was on screen. That is a
|
|
33
|
+
* heuristic over a rendering: it broke on a reworded or translated message, it
|
|
34
|
+
* needed the visibility check only because the loading overlay keeps the word
|
|
35
|
+
* "Loading" in the DOM at `opacity: 0`, and an unset opacity parses to zero
|
|
36
|
+
* outside a full layout engine, which reads every ordinary element as hidden. A
|
|
37
|
+
* component that wants to be waited for now says so with an attribute.
|
|
38
|
+
*
|
|
39
|
+
* Best-effort: a view genuinely stuck loading would otherwise burn the whole
|
|
40
|
+
* timeout, so this reports false rather than throwing and the caller decides.
|
|
41
|
+
*/
|
|
42
|
+
export declare function waitForQuiescent(page: Page, { timeout }?: {
|
|
43
|
+
timeout?: number;
|
|
44
|
+
}): Promise<boolean>;
|
|
45
|
+
export declare const PENDING_DISPLAYS = "[data-display-drawn=\"false\"]";
|
|
46
|
+
/**
|
|
47
|
+
* `page.waitForSelector`, with the display census in the timeout.
|
|
48
|
+
*
|
|
49
|
+
* The waits below are best-effort and hand their outcome back as a boolean, so
|
|
50
|
+
* a caller can say what was still unsettled. A *selector* wait is the opposite
|
|
51
|
+
* shape — it throws, and it runs FIRST, before any of them: a capture whose
|
|
52
|
+
* display never paints dies here on puppeteer's `TimeoutError`, which names the
|
|
53
|
+
* selector and nothing else, and the census that would have named the display
|
|
54
|
+
* runs later or not at all.
|
|
55
|
+
*
|
|
56
|
+
* So the census runs on the way out. It is re-read from the DOM here rather
|
|
57
|
+
* than taken off a handle the wait held: a handle to an element that has since
|
|
58
|
+
* re-rendered throws `Node is detached from document`, which is how the first
|
|
59
|
+
* attempt at this turned four diagnosable timeouts into nine opaque puppeteer
|
|
60
|
+
* errors (reverted in 28c6ee6d90).
|
|
61
|
+
*
|
|
62
|
+
* What it reports is the phase, and that is the part a longer timeout cannot
|
|
63
|
+
* substitute for: `loading` is a slow fetch, `error` is a display that has
|
|
64
|
+
* finished badly and is showing a banner, and `ready` is a display claiming it
|
|
65
|
+
* finished without drawing — a bug in the display rather than in the wait.
|
|
66
|
+
*
|
|
67
|
+
* An EMPTY census is the other answer, and `tooLarge` / `renderError` are why it
|
|
68
|
+
* is not good news. Those two replace the display's subtree outright, so they
|
|
69
|
+
* publish neither attribute and appear here as nothing at all: a wait on such a
|
|
70
|
+
* display can never resolve and can never be reported. Nothing pending means
|
|
71
|
+
* either that, or a selector that was never a display's.
|
|
72
|
+
*/
|
|
73
|
+
export declare function waitForSelectorAttributed(page: Page, selector: string, timeoutMs: number): Promise<ElementHandle>;
|
|
74
|
+
/**
|
|
75
|
+
* The census as one clause, for a message. Best-effort in its own right: a page
|
|
76
|
+
* whose execution context is gone cannot be asked, and saying so is the answer.
|
|
77
|
+
*/
|
|
78
|
+
export declare function describePendingDisplaysNow(page: Page): Promise<string>;
|
|
79
|
+
export declare const displayPainted: (testid: string) => string;
|
|
80
|
+
/**
|
|
81
|
+
* The stronger one. `drawn` flips on FIRST paint — an empty canvas with the
|
|
82
|
+
* fetch still in flight satisfies it — so a figure that must show *data* waits
|
|
83
|
+
* on the phase instead.
|
|
84
|
+
*/
|
|
85
|
+
export declare const displaySettled: (testid: string) => string;
|
|
86
|
+
/** One display, by its config's `displayId` rather than by type. */
|
|
87
|
+
export declare const displayById: (displayId: string) => string;
|
|
88
|
+
export declare function waitForDisplaysDone(page: Page, timeoutMs: number): Promise<boolean>;
|
|
89
|
+
export declare function waitForDisplayPhases(page: Page, timeoutMs: number): Promise<boolean>;
|
|
90
|
+
export declare function waitForViewPhases(page: Page, timeoutMs: number): Promise<import("puppeteer").JSHandle<false> | import("puppeteer").JSHandle<true>>;
|
|
91
|
+
/**
|
|
92
|
+
* Everything the app publishes to say it is working, as one selector.
|
|
93
|
+
*
|
|
94
|
+
* All four are attributes a component sets deliberately, which is the whole
|
|
95
|
+
* point: the alternative — scanning rendered text for /^loading/ and resolving
|
|
96
|
+
* `getComputedStyle` to decide whether each match is on screen — makes a
|
|
97
|
+
* reworded message, a translation or an opacity animation change the answer.
|
|
98
|
+
*
|
|
99
|
+
* `loading-overlay` the view-level scrim, present in every build
|
|
100
|
+
* `data-busy` LoadingEllipses, which is what the app renders
|
|
101
|
+
* wherever it tells a user it is working
|
|
102
|
+
* `data-display-phase` one display's own fetch, newer builds
|
|
103
|
+
* `data-view-phase` a view still resolving its assembly, newer builds
|
|
104
|
+
*/
|
|
105
|
+
export declare const BUSY_SELECTOR: string;
|
|
106
|
+
/**
|
|
107
|
+
* Is the app doing anything right now?
|
|
108
|
+
*
|
|
109
|
+
* Serialized into the page, so it declares everything it uses. Two sources: the
|
|
110
|
+
* selector above, and the live session model's own per-display status, which is
|
|
111
|
+
* the only PER-DISPLAY signal a build with no readiness attributes has left.
|
|
112
|
+
* Both are contracts rather than renderings — a model field and a set of data
|
|
113
|
+
* attributes — so neither moves when the UI is restyled.
|
|
114
|
+
*
|
|
115
|
+
* Exported so a test can call the real function rather than a copy of it.
|
|
116
|
+
*/
|
|
117
|
+
export declare function isPageBusyInPage(): boolean;
|
|
118
|
+
/**
|
|
119
|
+
* Wait until the app has been idle for an unbroken stretch.
|
|
120
|
+
*
|
|
121
|
+
* The gate for a build that publishes no readiness attributes, where every
|
|
122
|
+
* other wait in this module is an assertion about an absent selector and so
|
|
123
|
+
* cannot fail. Absence answers "is it working NOW"; a capture needs "has it
|
|
124
|
+
* finished", and the two differ in both directions:
|
|
125
|
+
*
|
|
126
|
+
* BEFORE the work starts. Measured on jbrowse.org/code/jb2/latest with two
|
|
127
|
+
* remote tracks: the session reports both tracks open at ~2.5s and the
|
|
128
|
+
* loading overlay does not go up until ~3.5s. Every absence-based gate passes
|
|
129
|
+
* during that second, over an app that has drawn nothing.
|
|
130
|
+
*
|
|
131
|
+
* BETWEEN two pieces of work. A track that finishes one fetch and starts the
|
|
132
|
+
* next is momentarily idle, and a single-sample gate takes it.
|
|
133
|
+
*
|
|
134
|
+
* Requiring the idle to HOLD closes both without needing to know which signals
|
|
135
|
+
* a given build has. Polled from node rather than in-page: chrome throttles
|
|
136
|
+
* in-page timers and rAF once the tab is not visible, which is the state a
|
|
137
|
+
* headless capture sits in.
|
|
138
|
+
*
|
|
139
|
+
* Returns false on timeout rather than throwing, like its neighbours — the
|
|
140
|
+
* caller decides whether a page that never went quiet is a failure or a slow
|
|
141
|
+
* page worth capturing anyway.
|
|
142
|
+
*/
|
|
143
|
+
export declare function waitForQuietPeriod(page: Page, { quietMs, timeout, pollMs, busyWindowMs, }?: {
|
|
144
|
+
quietMs?: number;
|
|
145
|
+
timeout?: number;
|
|
146
|
+
pollMs?: number;
|
|
147
|
+
/**
|
|
148
|
+
* Wait for the app to be seen BUSY before any idle counts, giving up on
|
|
149
|
+
* that after this long. Zero (the default) accepts idle immediately.
|
|
150
|
+
*
|
|
151
|
+
* This is what turns the wait positive. Idle is still an absence, so an app
|
|
152
|
+
* that has not begun looks exactly like one that has finished — measured on
|
|
153
|
+
* jbrowse.org/code/jb2/latest, where a chain starting at the session gate
|
|
154
|
+
* saw an idle page at 1.6s and the tracks did not draw until past 10s.
|
|
155
|
+
* Seeing the transition INTO work and back out of it is an observation of
|
|
156
|
+
* the work itself.
|
|
157
|
+
*
|
|
158
|
+
* The window is bounded because a page with nothing to fetch never goes
|
|
159
|
+
* busy at all, and hanging on that would be worse than the race it closes.
|
|
160
|
+
* Size it above the gap between the session gate passing and the first
|
|
161
|
+
* indicator appearing, measured at ~1s on that instance.
|
|
162
|
+
*/
|
|
163
|
+
busyWindowMs?: number;
|
|
164
|
+
}): Promise<boolean>;
|
|
165
|
+
/**
|
|
166
|
+
* Wait for the app to say it has finished.
|
|
167
|
+
*
|
|
168
|
+
* One selector, no chain: `[data-app-phase="ready"]` is rendered by the session
|
|
169
|
+
* itself, so it cannot be satisfied before the app exists. Returns false if the
|
|
170
|
+
* page never publishes it, which is how a caller tells "not ready yet" from "a
|
|
171
|
+
* build too old to have the marker" and falls back.
|
|
172
|
+
*/
|
|
173
|
+
export declare function waitForAppReady(page: Page, { timeout }?: {
|
|
174
|
+
timeout?: number;
|
|
175
|
+
}): Promise<boolean>;
|
|
176
|
+
/** Whether this build publishes the app-level readiness marker at all. */
|
|
177
|
+
export declare function hasAppReadyMarker(page: Page): Promise<boolean>;
|
|
178
|
+
/**
|
|
179
|
+
* Wait out work an INTERACTION started: `ready`, and still ready a beat later.
|
|
180
|
+
*
|
|
181
|
+
* The gate for after a click, a keystroke or a resize, where `waitForAppReady`
|
|
182
|
+
* alone is not one. A page that is still LOADING starts at `loading` and the
|
|
183
|
+
* transition into `ready` is the app finishing, so one read of the selector
|
|
184
|
+
* answers it. After an interaction the app is already `ready` — it was finished a
|
|
185
|
+
* moment ago — and stays that way until the click's work registers, so the same
|
|
186
|
+
* read returns instantly, on the pre-click frame.
|
|
187
|
+
*
|
|
188
|
+
* What it replaces is a fixed sleep, which is wrong in both directions and only
|
|
189
|
+
* ever caught in one: too short captures the work in progress, and the figure it
|
|
190
|
+
* produces looks finished, while too long is dead time on every run.
|
|
191
|
+
*
|
|
192
|
+
* **Not "seen busy, then ready"**, which is the obvious shape and was built and
|
|
193
|
+
* measured first. Waiting for `loading` to appear before accepting `ready` needs
|
|
194
|
+
* a cap, since an interaction that only opened a menu never makes the app busy at
|
|
195
|
+
* all — and on the figure spec it was measured against
|
|
196
|
+
* (`search_feature_highlight`) the app was never seen busy either, because that
|
|
197
|
+
* spec's own selector wait had already outlasted the redraw. The busy window then
|
|
198
|
+
* ran to its 2s cap having watched nothing, costing more than the 1.2s sleep it
|
|
199
|
+
* replaced. Requiring the idle to HOLD costs the hold and no more, and catches
|
|
200
|
+
* the same late-starting work.
|
|
201
|
+
*
|
|
202
|
+
* Polled from Node rather than by `page.waitForSelector`, for the reason
|
|
203
|
+
* `waitForQuietPeriod` is: chrome throttles in-page timers and rAF once the tab
|
|
204
|
+
* is not visible, which is exactly the state a headless capture sits in.
|
|
205
|
+
*
|
|
206
|
+
* Falls back to the quiet period on a build too old for the marker, rather than
|
|
207
|
+
* passing instantly. A no-op that reports success is how a spec that dropped its
|
|
208
|
+
* sleep for this ends up capturing the frame the sleep was there to avoid.
|
|
209
|
+
*/
|
|
210
|
+
export declare function waitForAppSettled(page: Page, { timeout, holdMs, pollMs, }?: {
|
|
211
|
+
timeout?: number;
|
|
212
|
+
holdMs?: number;
|
|
213
|
+
pollMs?: number;
|
|
214
|
+
}): Promise<boolean>;
|