@jbrowse/capture 5.0.0-beta.10 → 5.0.0-beta.13
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/README.md +52 -2
- package/esm/annotationOverlay.d.ts +77 -0
- package/esm/annotationOverlay.js +766 -0
- package/esm/annotationSpec.d.ts +7 -0
- package/esm/annotationSpec.js +70 -0
- package/esm/annotations.d.ts +14 -0
- package/esm/annotations.js +68 -0
- package/esm/args.d.ts +2 -1
- package/esm/args.js +17 -5
- package/esm/bin.js +10 -21
- package/esm/capture.d.ts +9 -2
- package/esm/capture.js +41 -11
- package/esm/catalog.d.ts +11 -6
- package/esm/catalog.js +37 -14
- package/esm/dotplotAnchor.d.ts +4 -0
- package/esm/dotplotAnchor.js +104 -0
- package/esm/graphAnchor.d.ts +47 -0
- package/esm/graphAnchor.js +120 -0
- package/esm/index.d.ts +2 -0
- package/esm/index.js +1 -0
- package/esm/jsonArgs.d.ts +4 -0
- package/esm/jsonArgs.js +34 -0
- package/esm/poll.d.ts +7 -0
- package/esm/poll.js +20 -0
- package/esm/ready.d.ts +1 -1
- package/esm/ready.js +6 -2
- package/esm/sessionGate.d.ts +8 -2
- package/esm/sessionGate.js +15 -5
- package/esm/url.d.ts +6 -5
- package/esm/url.js +20 -5
- package/esm/version.d.ts +1 -1
- package/esm/version.js +1 -1
- package/esm/waits.js +3 -5
- package/package.json +5 -3
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { AnnotationAnchor } from './annotationOverlay.ts';
|
|
2
|
+
import type { JSHandle, Page } from 'puppeteer';
|
|
3
|
+
export interface ViewportRect {
|
|
4
|
+
left: number;
|
|
5
|
+
top: number;
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
}
|
|
9
|
+
export interface GraphNodeGeometry extends ViewportRect {
|
|
10
|
+
midX: number;
|
|
11
|
+
midY: number;
|
|
12
|
+
}
|
|
13
|
+
export interface GraphPane {
|
|
14
|
+
scale?: number;
|
|
15
|
+
scaleX?: number;
|
|
16
|
+
scaleY?: number;
|
|
17
|
+
translateX?: number;
|
|
18
|
+
translateY?: number;
|
|
19
|
+
nodePositions?: Record<string, {
|
|
20
|
+
x: number;
|
|
21
|
+
y: number;
|
|
22
|
+
}[]>;
|
|
23
|
+
graph?: {
|
|
24
|
+
nodes: {
|
|
25
|
+
id: string;
|
|
26
|
+
length?: number;
|
|
27
|
+
stable?: {
|
|
28
|
+
rank?: number;
|
|
29
|
+
refName?: string;
|
|
30
|
+
start?: number;
|
|
31
|
+
end?: number;
|
|
32
|
+
};
|
|
33
|
+
}[];
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export declare function locateGraphPaneInPage(viewPath: number[], trackId: string | undefined): {
|
|
37
|
+
pane: GraphPane;
|
|
38
|
+
canvas: Element;
|
|
39
|
+
} | undefined;
|
|
40
|
+
export type LocatedGraphPane = NonNullable<ReturnType<typeof locateGraphPaneInPage>>;
|
|
41
|
+
export declare function nodeGeometryInPage(located: LocatedGraphPane | undefined, nodeId: string): GraphNodeGeometry | undefined;
|
|
42
|
+
export declare function locateGraphPane(page: Page, anchor: Pick<AnnotationAnchor, 'view' | 'trackId'>): Promise<JSHandle<LocatedGraphPane | undefined>>;
|
|
43
|
+
export declare function graphNodePoint(page: Page, anchor: AnnotationAnchor): Promise<{
|
|
44
|
+
x: number;
|
|
45
|
+
y: number;
|
|
46
|
+
} | undefined>;
|
|
47
|
+
export declare function graphNodeRect(page: Page, anchor: AnnotationAnchor, wantBounds: boolean): Promise<ViewportRect | undefined>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// The graph pane an anchor addresses, and the canvas it draws on. The graph is
|
|
2
|
+
// one canvas with no element per node, so the pane is asked where it put one
|
|
3
|
+
// instead, the way a locus anchor asks the LGV. The pane is the addressed view
|
|
4
|
+
// itself when that is a GraphGenomeView, and otherwise a LinearGraphDisplay
|
|
5
|
+
// among the view's tracks, the one `trackId` names or the first: the display
|
|
6
|
+
// itself from plugin 4.0.7, its nested `pane` before that.
|
|
7
|
+
//
|
|
8
|
+
// Puppeteer serializes this into the page, so it closes over nothing.
|
|
9
|
+
export function locateGraphPaneInPage(viewPath, trackId) {
|
|
10
|
+
let view = window.JBrowseSession;
|
|
11
|
+
for (const i of viewPath) {
|
|
12
|
+
view = view?.views?.[i];
|
|
13
|
+
}
|
|
14
|
+
if (!view) {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
const container = `[data-testid="view-container-${CSS.escape(view.id)}"]`;
|
|
18
|
+
let pane = view;
|
|
19
|
+
let scope = container;
|
|
20
|
+
if (view.tracks) {
|
|
21
|
+
const paneOf = (d) => d.pane ?? (d.type === 'LinearGraphDisplay' ? d : undefined);
|
|
22
|
+
const track = view.tracks.find(t => t.displays?.some(paneOf) &&
|
|
23
|
+
(trackId === undefined || t.configuration?.trackId === trackId));
|
|
24
|
+
const display = track?.displays?.find(paneOf);
|
|
25
|
+
pane = display ? paneOf(display) : undefined;
|
|
26
|
+
const id = track?.configuration?.trackId ?? '';
|
|
27
|
+
scope = `${container} [data-testid="trackRenderingContainer-${CSS.escape(view.id)}-${CSS.escape(id)}"] [data-testid="linear-graph-display"]`;
|
|
28
|
+
}
|
|
29
|
+
const canvas = document.querySelector(`${scope} [data-testid="graph-genome-canvas"]`);
|
|
30
|
+
return pane && canvas ? { pane, canvas } : undefined;
|
|
31
|
+
}
|
|
32
|
+
// Where the located pane drew one GFA segment, in viewport CSS px: its
|
|
33
|
+
// `nodePositions` in graph world units through the transform the renderer
|
|
34
|
+
// draws with. Undefined when the node isn't there, so the caller can fail the
|
|
35
|
+
// spec by name rather than acting on (0,0). Serialized into the page too.
|
|
36
|
+
export function nodeGeometryInPage(located, nodeId) {
|
|
37
|
+
if (!located) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
const { pane, canvas } = located;
|
|
41
|
+
// nodePositions is keyed by the node's drawn id, which carries the
|
|
42
|
+
// orientation the cut walked the segment in ('s2037+'), so a spec may name
|
|
43
|
+
// either that or the bare GFA segment id
|
|
44
|
+
const positions = pane.nodePositions;
|
|
45
|
+
const pts = positions?.[nodeId] ??
|
|
46
|
+
positions?.[`${nodeId}+`] ??
|
|
47
|
+
positions?.[`${nodeId}-`];
|
|
48
|
+
if (!pts?.length) {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
const r = canvas.getBoundingClientRect();
|
|
52
|
+
const scaleX = pane.scaleX ?? pane.scale ?? 1;
|
|
53
|
+
const scaleY = pane.scaleY ?? pane.scale ?? 1;
|
|
54
|
+
const tx = pane.translateX ?? 0;
|
|
55
|
+
const ty = pane.translateY ?? 0;
|
|
56
|
+
const screen = pts.map(p => ({ x: p.x * scaleX + tx, y: p.y * scaleY + ty }));
|
|
57
|
+
const xs = screen.map(p => p.x);
|
|
58
|
+
const ys = screen.map(p => p.y);
|
|
59
|
+
const left = Math.min(...xs);
|
|
60
|
+
const top = Math.min(...ys);
|
|
61
|
+
// Halfway along the drawn line rather than its middle vertex: an anchored
|
|
62
|
+
// node is a two-point line, whose "middle" vertex is its left end, where it
|
|
63
|
+
// meets its neighbour, and the app's hit test there answers the neighbour.
|
|
64
|
+
const legs = screen
|
|
65
|
+
.slice(1)
|
|
66
|
+
.map((p, i) => Math.hypot(p.x - screen[i].x, p.y - screen[i].y));
|
|
67
|
+
let rest = legs.reduce((sum, leg) => sum + leg, 0) / 2;
|
|
68
|
+
let mid = screen[0];
|
|
69
|
+
for (const [i, leg] of legs.entries()) {
|
|
70
|
+
const from = screen[i];
|
|
71
|
+
const to = screen[i + 1];
|
|
72
|
+
if (rest <= leg) {
|
|
73
|
+
const t = leg > 0 ? rest / leg : 0;
|
|
74
|
+
mid = { x: from.x + (to.x - from.x) * t, y: from.y + (to.y - from.y) * t };
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
rest -= leg;
|
|
78
|
+
mid = to;
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
left: r.left + left,
|
|
82
|
+
top: r.top + top,
|
|
83
|
+
width: Math.max(...xs) - left,
|
|
84
|
+
height: Math.max(...ys) - top,
|
|
85
|
+
midX: r.left + mid.x,
|
|
86
|
+
midY: r.top + mid.y,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
export function locateGraphPane(page, anchor) {
|
|
90
|
+
const path = Array.isArray(anchor.view) ? anchor.view : [anchor.view ?? 0];
|
|
91
|
+
return page.evaluateHandle(locateGraphPaneInPage, path, anchor.trackId);
|
|
92
|
+
}
|
|
93
|
+
async function graphNodeGeometry(page, anchor) {
|
|
94
|
+
const located = await locateGraphPane(page, anchor);
|
|
95
|
+
try {
|
|
96
|
+
return await located.evaluate(nodeGeometryInPage, anchor.graphNode ?? '');
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
await located.dispose();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// What a click/hover/ring acts on: a point on the node itself.
|
|
103
|
+
export async function graphNodePoint(page, anchor) {
|
|
104
|
+
const geom = await graphNodeGeometry(page, anchor);
|
|
105
|
+
return geom
|
|
106
|
+
? { x: geom.midX + (anchor.dx ?? 0), y: geom.midY + (anchor.dy ?? 0) }
|
|
107
|
+
: undefined;
|
|
108
|
+
}
|
|
109
|
+
// What an annotation resolves to: the drawn bounds for a box, a zero-size rect
|
|
110
|
+
// on the node for anything else (so its centre lands on the node, not in the
|
|
111
|
+
// space a bent node encloses).
|
|
112
|
+
export async function graphNodeRect(page, anchor, wantBounds) {
|
|
113
|
+
const geom = await graphNodeGeometry(page, anchor);
|
|
114
|
+
if (!geom) {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
return wantBounds
|
|
118
|
+
? { left: geom.left, top: geom.top, width: geom.width, height: geom.height }
|
|
119
|
+
: { left: geom.midX, top: geom.midY, width: 0, height: 0 };
|
|
120
|
+
}
|
package/esm/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { captureJBrowse, openJBrowse } from './capture.ts';
|
|
2
|
+
export { clearAnnotations, drawAnnotations } from './annotations.ts';
|
|
2
3
|
export { delay } from './poll.ts';
|
|
3
4
|
export { waitForFrame, waitForJBrowseReady } from './ready.ts';
|
|
4
5
|
export { describeDisplays, displayCensusInPage, waitForSession, } from './sessionGate.ts';
|
|
@@ -6,6 +7,7 @@ export { jbrowseUrl } from './url.ts';
|
|
|
6
7
|
export { assemblyFromSession, encodeSessionSpec, sessionSpecQuery, trackIdsFromSession, } from './session.ts';
|
|
7
8
|
export { PENDING_DISPLAYS, displayPainted, displaySettled, waitForAppSettled, waitForSelectorAttributed, } from './waits.ts';
|
|
8
9
|
export { BASE_CHROME_ARGS, findChromeExecutable, isBrowserConsoleNoise, launchBrowser, } from './browser.ts';
|
|
10
|
+
export type { Annotation, AnnotationAnchor } from './annotationOverlay.ts';
|
|
9
11
|
export type { LaunchOptions } from './browser.ts';
|
|
10
12
|
export type { CaptureOptions, CaptureResult, OpenOptions, OpenResult, } from './capture.ts';
|
|
11
13
|
export type { ReadyOptions, ReadyReport } from './ready.ts';
|
package/esm/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { captureJBrowse, openJBrowse } from './capture.js';
|
|
2
|
+
export { clearAnnotations, drawAnnotations } from './annotations.js';
|
|
2
3
|
export { delay } from './poll.js';
|
|
3
4
|
export { waitForFrame, waitForJBrowseReady } from './ready.js';
|
|
4
5
|
export { describeDisplays, displayCensusInPage, waitForSession, } from './sessionGate.js';
|
package/esm/jsonArgs.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
// inline JSON, `-` for stdin, or a path
|
|
3
|
+
function parseJson(flag, value) {
|
|
4
|
+
try {
|
|
5
|
+
const inline = /^\s*[[{]/.test(value);
|
|
6
|
+
return JSON.parse(value === '-'
|
|
7
|
+
? readFileSync(0, 'utf8')
|
|
8
|
+
: inline
|
|
9
|
+
? value
|
|
10
|
+
: readFileSync(value, 'utf8'));
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
throw new Error(`--${flag}: ${error instanceof Error ? error.message : error}`, { cause: error });
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function readJson(flag, value) {
|
|
17
|
+
const parsed = parseJson(flag, value);
|
|
18
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
19
|
+
throw new Error(`--${flag} must be a JSON object`);
|
|
20
|
+
}
|
|
21
|
+
return parsed;
|
|
22
|
+
}
|
|
23
|
+
export function readAnnotations(value) {
|
|
24
|
+
const parsed = parseJson('annotations', value);
|
|
25
|
+
if (!Array.isArray(parsed)) {
|
|
26
|
+
throw new Error('--annotations must be a JSON array of callouts');
|
|
27
|
+
}
|
|
28
|
+
return parsed;
|
|
29
|
+
}
|
|
30
|
+
// a bare view object is the one-view spec it would be wrapped in
|
|
31
|
+
export function readSpec(value) {
|
|
32
|
+
const spec = readJson('spec', value);
|
|
33
|
+
return 'type' in spec && !('views' in spec) ? { views: [spec] } : spec;
|
|
34
|
+
}
|
package/esm/poll.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Page } from 'puppeteer';
|
|
1
2
|
/** The budget each stage of the ready chain gets unless told otherwise. */
|
|
2
3
|
export declare const DEFAULT_TIMEOUT = 60000;
|
|
3
4
|
export declare const delay: (ms: number) => Promise<void>;
|
|
@@ -12,3 +13,9 @@ export declare function holdTrue(read: () => Promise<boolean>, { holdMs, timeout
|
|
|
12
13
|
timeout: number;
|
|
13
14
|
pollMs: number;
|
|
14
15
|
}): Promise<boolean>;
|
|
16
|
+
/**
|
|
17
|
+
* Await a page query, answering `fallback` when it failed for a reason a later
|
|
18
|
+
* poll can outlive, such as a navigation destroying the execution context. A
|
|
19
|
+
* closed or crashed page throws, since no poll will recover it.
|
|
20
|
+
*/
|
|
21
|
+
export declare function queryWhileOpen<T, F>(page: Page, query: Promise<T>, fallback: F): Promise<T | F>;
|
package/esm/poll.js
CHANGED
|
@@ -28,3 +28,23 @@ export async function holdTrue(read, { holdMs, timeout, pollMs, }) {
|
|
|
28
28
|
}
|
|
29
29
|
return false;
|
|
30
30
|
}
|
|
31
|
+
const PAGE_GONE = /crashed|Target closed|Session closed|Connection closed/i;
|
|
32
|
+
/**
|
|
33
|
+
* Await a page query, answering `fallback` when it failed for a reason a later
|
|
34
|
+
* poll can outlive, such as a navigation destroying the execution context. A
|
|
35
|
+
* closed or crashed page throws, since no poll will recover it.
|
|
36
|
+
*/
|
|
37
|
+
export async function queryWhileOpen(page, query, fallback) {
|
|
38
|
+
try {
|
|
39
|
+
return await query;
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
43
|
+
if (PAGE_GONE.test(message) ||
|
|
44
|
+
page.isClosed() ||
|
|
45
|
+
!page.browser().connected) {
|
|
46
|
+
throw new Error(`the page closed or crashed before it finished rendering (${message})`, { cause: error });
|
|
47
|
+
}
|
|
48
|
+
return fallback;
|
|
49
|
+
}
|
|
50
|
+
}
|
package/esm/ready.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ export interface ReadyReport {
|
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
21
21
|
* Wait until the session holds the assembly and tracks asked for, the app has
|
|
22
|
-
* held itself ready,
|
|
22
|
+
* held itself ready, every display has painted, and no error is on screen.
|
|
23
23
|
*/
|
|
24
24
|
export declare function waitForJBrowseReady(page: Page, options?: ReadyOptions): Promise<ReadyReport>;
|
|
25
25
|
/**
|
package/esm/ready.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { DEFAULT_TIMEOUT } from './poll.js';
|
|
2
|
-
import { describeDisplays, displayCensusInPage, waitForSession, } from './sessionGate.js';
|
|
2
|
+
import { describeDisplays, displayCensusInPage, errorsShownInPage, waitForSession, } from './sessionGate.js';
|
|
3
3
|
import { appSettledBlocker } from './waits.js';
|
|
4
4
|
/**
|
|
5
5
|
* Wait until the session holds the assembly and tracks asked for, the app has
|
|
6
|
-
* held itself ready,
|
|
6
|
+
* held itself ready, every display has painted, and no error is on screen.
|
|
7
7
|
*/
|
|
8
8
|
export async function waitForJBrowseReady(page, options = {}) {
|
|
9
9
|
await waitForSession(page, options);
|
|
@@ -33,6 +33,10 @@ export async function waitForFrame(page, { timeout = DEFAULT_TIMEOUT, allowUnset
|
|
|
33
33
|
if (canceled.length > 0) {
|
|
34
34
|
unsettled.push(`display(s) canceled, which lasts until Retry: ${describeDisplays(canceled)}`);
|
|
35
35
|
}
|
|
36
|
+
const errors = await page.evaluate(errorsShownInPage);
|
|
37
|
+
if (errors.length > 0) {
|
|
38
|
+
unsettled.push(`error(s) on screen: ${errors.join('; ')}`);
|
|
39
|
+
}
|
|
36
40
|
if (!allowUnsettled && unsettled.length > 0) {
|
|
37
41
|
const stillLoading = pending.some(d => d.phase === undefined || d.phase === 'loading');
|
|
38
42
|
const advice = timedOut || stillLoading
|
package/esm/sessionGate.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export interface SessionExpectations {
|
|
|
8
8
|
*/
|
|
9
9
|
trackIds?: string[];
|
|
10
10
|
/** The fewest open views that count. Default 1; 0 accepts none. */
|
|
11
|
-
|
|
11
|
+
minViews?: number;
|
|
12
12
|
}
|
|
13
13
|
export interface SessionCensus {
|
|
14
14
|
views: number;
|
|
@@ -30,7 +30,7 @@ export declare function readSessionInPage(): {
|
|
|
30
30
|
* Throws at once with the app's message if it could not load, and on timeout
|
|
31
31
|
* with what the census held instead.
|
|
32
32
|
*/
|
|
33
|
-
export declare function waitForSession(page: Page, { assembly, trackIds,
|
|
33
|
+
export declare function waitForSession(page: Page, { assembly, trackIds, minViews, timeout, }?: SessionExpectations & {
|
|
34
34
|
timeout?: number;
|
|
35
35
|
}): Promise<void>;
|
|
36
36
|
/** One display and the phase it publishes. */
|
|
@@ -55,5 +55,11 @@ export declare function displayCensusInPage(): {
|
|
|
55
55
|
pending: DisplayState[];
|
|
56
56
|
tooLarge: DisplayState[];
|
|
57
57
|
};
|
|
58
|
+
/**
|
|
59
|
+
* The error surfaces no display carries: the toast `notifyError` raises, as a
|
|
60
|
+
* track whose config fails to load does, and the box `ErrorMessage` renders.
|
|
61
|
+
* A track that never opened leaves nothing in {@link displayCensusInPage}.
|
|
62
|
+
*/
|
|
63
|
+
export declare function errorsShownInPage(): string[];
|
|
58
64
|
/** Displays as one clause for a message. */
|
|
59
65
|
export declare function describeDisplays(displays: DisplayState[]): string;
|
package/esm/sessionGate.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DEFAULT_TIMEOUT, holdTrue } from './poll.js';
|
|
1
|
+
import { DEFAULT_TIMEOUT, holdTrue, queryWhileOpen } from './poll.js';
|
|
2
2
|
/**
|
|
3
3
|
* The load failure the app shows in place of itself, the census
|
|
4
4
|
* `AppReadyMarker` publishes beside its phase, and any snackbar up — which is
|
|
@@ -33,11 +33,11 @@ export function readSessionInPage() {
|
|
|
33
33
|
* Throws at once with the app's message if it could not load, and on timeout
|
|
34
34
|
* with what the census held instead.
|
|
35
35
|
*/
|
|
36
|
-
export async function waitForSession(page, { assembly, trackIds = [],
|
|
36
|
+
export async function waitForSession(page, { assembly, trackIds = [], minViews = 1, timeout = DEFAULT_TIMEOUT, } = {}) {
|
|
37
37
|
let last;
|
|
38
38
|
const notices = new Set();
|
|
39
39
|
const reached = await holdTrue(async () => {
|
|
40
|
-
const read = await page.evaluate(readSessionInPage)
|
|
40
|
+
const read = await queryWhileOpen(page, page.evaluate(readSessionInPage), undefined);
|
|
41
41
|
if (read?.failure) {
|
|
42
42
|
throw new Error(`JBrowse could not load: ${read.failure}`);
|
|
43
43
|
}
|
|
@@ -47,7 +47,7 @@ export async function waitForSession(page, { assembly, trackIds = [], views = 1,
|
|
|
47
47
|
const census = read?.census;
|
|
48
48
|
last = census;
|
|
49
49
|
return (!!census &&
|
|
50
|
-
census.views >=
|
|
50
|
+
census.views >= minViews &&
|
|
51
51
|
(assembly === undefined || census.assemblies.includes(assembly)) &&
|
|
52
52
|
trackIds.every(id => census.trackIds.includes(id)));
|
|
53
53
|
}, { holdMs: 0, timeout, pollMs: 250 });
|
|
@@ -67,7 +67,7 @@ export async function waitForSession(page, { assembly, trackIds = [], views = 1,
|
|
|
67
67
|
const said = notices.size
|
|
68
68
|
? `The app said: ${[...notices].join(' | ')}.`
|
|
69
69
|
: 'A trackId the config does not define, or an assembly name that does not match the config, looks like this.';
|
|
70
|
-
throw new Error(`the session never reached the requested state after ${timeout}ms. Wanted ${wanted || (
|
|
70
|
+
throw new Error(`the session never reached the requested state after ${timeout}ms. Wanted ${wanted || (minViews > 0 ? 'an open view' : 'a session')}; found ${found}. ${said}`);
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
/**
|
|
@@ -94,6 +94,16 @@ export function displayCensusInPage() {
|
|
|
94
94
|
}
|
|
95
95
|
return { pending, tooLarge };
|
|
96
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* The error surfaces no display carries: the toast `notifyError` raises, as a
|
|
99
|
+
* track whose config fails to load does, and the box `ErrorMessage` renders.
|
|
100
|
+
* A track that never opened leaves nothing in {@link displayCensusInPage}.
|
|
101
|
+
*/
|
|
102
|
+
export function errorsShownInPage() {
|
|
103
|
+
return [
|
|
104
|
+
...document.querySelectorAll('[data-testid="snackbar-error"], [data-testid="error-message-box"]'),
|
|
105
|
+
].map(el => (el.innerText || el.textContent || '').trim().slice(0, 300));
|
|
106
|
+
}
|
|
97
107
|
/** Displays as one clause for a message. */
|
|
98
108
|
export function describeDisplays(displays) {
|
|
99
109
|
return displays
|
package/esm/url.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const PUBLIC_INSTANCE = "https://jbrowse.org/code/jb2/v5.0.0-beta.
|
|
1
|
+
export declare const PUBLIC_INSTANCE = "https://jbrowse.org/code/jb2/v5.0.0-beta.13/";
|
|
2
2
|
/**
|
|
3
3
|
* The instance as the page that loads it sees it: a directory given without
|
|
4
4
|
* its slash, which the host redirects, resolves relative URLs one level up.
|
|
@@ -7,7 +7,7 @@ export declare function instanceUrl(instance?: string): URL;
|
|
|
7
7
|
export interface JBrowseUrlOptions {
|
|
8
8
|
/** A genomes.jbrowse.org assembly: a UCSC db name (`hg38`) or GenArk accession. */
|
|
9
9
|
hub?: string;
|
|
10
|
-
/** A config.json URL, for data that is not on genomes.jbrowse.org.
|
|
10
|
+
/** A config.json URL, for data that is not on genomes.jbrowse.org. Not combined with `hub`. */
|
|
11
11
|
config?: string;
|
|
12
12
|
/** Assembly to open. Defaults to `hub`, whose config names its assembly after it. */
|
|
13
13
|
assembly?: string;
|
|
@@ -32,10 +32,11 @@ export interface JBrowseUrlOptions {
|
|
|
32
32
|
instance?: string;
|
|
33
33
|
}
|
|
34
34
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
35
|
+
* One source for the config, never a hub and a config URL. A spec or a saved
|
|
36
|
+
* session says which assembly, locations and tracks to open, so the URL carries
|
|
37
|
+
* one of the three ways of saying so, never two.
|
|
37
38
|
*/
|
|
38
|
-
export declare function
|
|
39
|
+
export declare function assertCoherentOptions({ hub, config, spec, session, assembly, loc, tracks, }: JBrowseUrlOptions): void;
|
|
39
40
|
/**
|
|
40
41
|
* The URL that opens a JBrowse Web instance onto a described view, for a
|
|
41
42
|
* person to follow without launching a browser here.
|
package/esm/url.js
CHANGED
|
@@ -9,17 +9,24 @@ export const PUBLIC_INSTANCE = `https://jbrowse.org/code/jb2/v${version}/`;
|
|
|
9
9
|
* its slash, which the host redirects, resolves relative URLs one level up.
|
|
10
10
|
*/
|
|
11
11
|
export function instanceUrl(instance = PUBLIC_INSTANCE) {
|
|
12
|
-
const url =
|
|
12
|
+
const url = URL.parse(instance);
|
|
13
|
+
if (!url || !['http:', 'https:', 'file:'].includes(url.protocol)) {
|
|
14
|
+
throw new Error(`instance "${instance}" is not an http(s) or file URL`);
|
|
15
|
+
}
|
|
13
16
|
if (!url.pathname.endsWith('/') && !/\.html?$/.test(url.pathname)) {
|
|
14
17
|
url.pathname += '/';
|
|
15
18
|
}
|
|
16
19
|
return url;
|
|
17
20
|
}
|
|
18
21
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
22
|
+
* One source for the config, never a hub and a config URL. A spec or a saved
|
|
23
|
+
* session says which assembly, locations and tracks to open, so the URL carries
|
|
24
|
+
* one of the three ways of saying so, never two.
|
|
21
25
|
*/
|
|
22
|
-
export function
|
|
26
|
+
export function assertCoherentOptions({ hub, config, spec, session, assembly, loc, tracks, }) {
|
|
27
|
+
if (hub && config) {
|
|
28
|
+
throw new Error('pass a hosted hub or a config URL, not both (--hub, --config); name the assembly with --assembly');
|
|
29
|
+
}
|
|
23
30
|
if (spec && session) {
|
|
24
31
|
throw new Error('pass a session spec or a saved session, not both');
|
|
25
32
|
}
|
|
@@ -41,7 +48,15 @@ export function assertSessionStandsAlone({ spec, session, assembly, loc, tracks,
|
|
|
41
48
|
* takes a locstring only. `session` opens a saved session as exported.
|
|
42
49
|
*/
|
|
43
50
|
export function jbrowseUrl({ hub, config, assembly, loc, tracks, spec, session, sessionName, instance, }) {
|
|
44
|
-
|
|
51
|
+
assertCoherentOptions({
|
|
52
|
+
hub,
|
|
53
|
+
config,
|
|
54
|
+
spec,
|
|
55
|
+
session,
|
|
56
|
+
assembly,
|
|
57
|
+
loc,
|
|
58
|
+
tracks,
|
|
59
|
+
});
|
|
45
60
|
const params = new URLSearchParams();
|
|
46
61
|
const set = (key, value) => {
|
|
47
62
|
if (value) {
|
package/esm/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const version = "5.0.0-beta.
|
|
1
|
+
export declare const version = "5.0.0-beta.13";
|
package/esm/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '5.0.0-beta.
|
|
1
|
+
export const version = '5.0.0-beta.13';
|
package/esm/waits.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DEFAULT_TIMEOUT, holdTrue } from './poll.js';
|
|
1
|
+
import { DEFAULT_TIMEOUT, holdTrue, queryWhileOpen } from './poll.js';
|
|
2
2
|
import { describeDisplays, displayCensusInPage } from './sessionGate.js';
|
|
3
3
|
/** The app's own verdict: no view is resolving an assembly, no display is fetching. */
|
|
4
4
|
export const APP_READY = '[data-app-phase="ready"]';
|
|
@@ -64,14 +64,12 @@ const NO_MARKER = 'no [data-app-phase] on the page';
|
|
|
64
64
|
* or undefined once the hold passed.
|
|
65
65
|
*/
|
|
66
66
|
export async function appSettledBlocker(page, { timeout = DEFAULT_TIMEOUT, holdMs = APP_SETTLED_HOLD_MS, pollMs = 250, } = {}) {
|
|
67
|
-
const blocker = () => page
|
|
68
|
-
.evaluate((ready, blockers, noMarker) => document.querySelector('[data-app-phase]') === null
|
|
67
|
+
const blocker = () => queryWhileOpen(page, page.evaluate((ready, blockers, noMarker) => document.querySelector('[data-app-phase]') === null
|
|
69
68
|
? noMarker
|
|
70
69
|
: (blockers.find(([selector]) => document.querySelector(selector))?.[1] ??
|
|
71
70
|
(document.querySelector(ready) === null
|
|
72
71
|
? 'the marker not reading ready'
|
|
73
|
-
: undefined)), APP_READY, SETTLE_BLOCKERS, NO_MARKER)
|
|
74
|
-
.catch(() => 'the page could not be queried');
|
|
72
|
+
: undefined)), APP_READY, SETTLE_BLOCKERS, NO_MARKER), 'the page could not be queried');
|
|
75
73
|
const held = await holdTrue(async () => (await blocker()) === undefined, {
|
|
76
74
|
holdMs,
|
|
77
75
|
timeout,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jbrowse/capture",
|
|
3
|
-
"version": "5.0.0-beta.
|
|
3
|
+
"version": "5.0.0-beta.13",
|
|
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",
|
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
"type": "module",
|
|
17
17
|
"main": "esm/index.js",
|
|
18
18
|
"exports": {
|
|
19
|
-
".": "./esm/index.js"
|
|
19
|
+
".": "./esm/index.js",
|
|
20
|
+
"./annotationOverlay": "./esm/annotationOverlay.js",
|
|
21
|
+
"./graphAnchor": "./esm/graphAnchor.js"
|
|
20
22
|
},
|
|
21
23
|
"author": "JBrowse Team",
|
|
22
24
|
"license": "Apache-2.0",
|
|
@@ -34,7 +36,7 @@
|
|
|
34
36
|
"puppeteer": "^25.12.0"
|
|
35
37
|
},
|
|
36
38
|
"devDependencies": {
|
|
37
|
-
"@jbrowse/core": "^5.0.0-beta.
|
|
39
|
+
"@jbrowse/core": "^5.0.0-beta.13"
|
|
38
40
|
},
|
|
39
41
|
"publishConfig": {
|
|
40
42
|
"access": "public"
|