@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/README.md +46 -25
- package/esm/args.d.ts +2 -1
- package/esm/args.js +2 -1
- package/esm/bin.js +52 -29
- package/esm/browser.d.ts +0 -1
- package/esm/browser.js +5 -9
- package/esm/capture.d.ts +9 -8
- package/esm/capture.js +41 -57
- package/esm/catalog.d.ts +40 -0
- package/esm/catalog.js +103 -0
- package/esm/hub.d.ts +10 -0
- package/esm/hub.js +31 -16
- package/esm/imagePath.d.ts +1 -0
- package/esm/imagePath.js +8 -0
- package/esm/index.d.ts +8 -8
- package/esm/index.js +7 -7
- package/esm/instanceVersion.d.ts +8 -0
- package/esm/instanceVersion.js +24 -0
- package/esm/poll.d.ts +13 -0
- package/esm/poll.js +29 -0
- package/esm/ready.d.ts +20 -9
- package/esm/ready.js +37 -27
- package/esm/session.d.ts +13 -5
- package/esm/session.js +31 -12
- package/esm/sessionGate.d.ts +52 -43
- package/esm/sessionGate.js +82 -83
- package/esm/sessionOverflow.d.ts +11 -0
- package/esm/sessionOverflow.js +24 -0
- package/esm/url.d.ts +16 -6
- package/esm/url.js +34 -17
- package/esm/waits.d.ts +13 -16
- package/esm/waits.js +49 -81
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -10,7 +10,10 @@ npx @jbrowse/capture --hub hg38 --loc BRCA1 \
|
|
|
10
10
|
|
|
11
11
|
`--hub` names an assembly on [genomes.jbrowse.org](https://genomes.jbrowse.org),
|
|
12
12
|
which hosts a ready-made JBrowse config per UCSC and GenArk genome, and `--loc`
|
|
13
|
-
takes a gene name because those configs ship a text index.
|
|
13
|
+
takes a gene name because those configs ship a text index. `--track` takes a
|
|
14
|
+
trackId, the id without its assembly prefix (`clinvarMain`) or the track's
|
|
15
|
+
display name; capture checks each one and the assembly against the config before
|
|
16
|
+
launching a browser, and a miss fails at once with suggestions.
|
|
14
17
|
|
|
15
18
|
## Knowing when JBrowse has finished rendering
|
|
16
19
|
|
|
@@ -38,13 +41,18 @@ decides the answer:
|
|
|
38
41
|
1. **The census.** `AppReadyMarker` publishes what is open as `data-app-views`,
|
|
39
42
|
`data-app-assemblies` and `data-app-tracks`, and the gate reads that one
|
|
40
43
|
element: at least one view, your assembly among them, every trackId you named
|
|
41
|
-
actually open. A
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
actually open. A trackId or assembly that capture could not check against the
|
|
45
|
+
config before launching (a session spec's, or a config it could not fetch)
|
|
46
|
+
fails here if it never opens. A config, session or plugin the app cannot load
|
|
47
|
+
fails here at once, with the app's own message, which it publishes as
|
|
48
|
+
`data-app-error`.
|
|
44
49
|
2. **`[data-app-phase="ready"]`, held.** The session renders it when no view is
|
|
45
50
|
resolving an assembly and no display is fetching. It has to hold for a beat,
|
|
46
51
|
not merely be true once: a display drops to `ready` in the gap between one
|
|
47
|
-
fetch finishing and the debounced next one starting.
|
|
52
|
+
fetch finishing and the debounced next one starting. The hold also waits out
|
|
53
|
+
a view body still loading its code and a display drawing a morph between two
|
|
54
|
+
layouts (`data-display-animating="true"`), neither of which the marker can
|
|
55
|
+
see.
|
|
48
56
|
|
|
49
57
|
Then one negative gate, which is meaningful only after those two and answers
|
|
50
58
|
what they do not — the marker is about WORK, and a display whose fetch failed is
|
|
@@ -52,10 +60,10 @@ not working, so it reads `ready` over an error banner. `data-display-drawn` is
|
|
|
52
60
|
the stricter question, and the census of what is still unpainted goes in the
|
|
53
61
|
error.
|
|
54
62
|
|
|
55
|
-
**
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
63
|
+
**The instance has to be JBrowse v5 or later**, the first release to publish the
|
|
64
|
+
marker. Capture reads the instance's `version.txt` before launching a browser
|
|
65
|
+
and refuses an older build there, rather than waiting out a timeout on a page
|
|
66
|
+
that will never say it has finished.
|
|
59
67
|
|
|
60
68
|
## Library
|
|
61
69
|
|
|
@@ -70,20 +78,28 @@ const { pending, unsettled } = await captureJBrowse({
|
|
|
70
78
|
out: 'brca1.png',
|
|
71
79
|
})
|
|
72
80
|
|
|
73
|
-
// or keep the page, to click things
|
|
81
|
+
// or keep the page, to click things, read state back, or shoot one element
|
|
74
82
|
const { browser, page } = await openJBrowse({ hub: 'mm39', loc: 'Sox2' })
|
|
75
83
|
const tracks = await page.evaluate(
|
|
76
84
|
() => window.JBrowseSession.views[0].tracks.length,
|
|
77
85
|
)
|
|
86
|
+
const view = await page.$('[data-testid^="view-container-"]')
|
|
87
|
+
await view.screenshot({ path: 'view-only.png' })
|
|
78
88
|
await browser.close()
|
|
79
89
|
```
|
|
80
90
|
|
|
91
|
+
The CLI covers the common capture and has no flag for the rest. A crop, a click
|
|
92
|
+
before the shot or a hidden element is a line of Puppeteer on the page
|
|
93
|
+
`openJBrowse` returns, and a view or display setting goes in a `--spec`.
|
|
94
|
+
|
|
81
95
|
Two waits, depending on what you did:
|
|
82
96
|
|
|
83
97
|
- **`waitForJBrowseReady(page)`** is the wait on its own, for a page you
|
|
84
|
-
navigated yourself.
|
|
85
|
-
|
|
86
|
-
|
|
98
|
+
navigated yourself. `waitForFrame(page)` is its second half alone, for a page
|
|
99
|
+
whose session you already gated, after a resize or a click. The individual
|
|
100
|
+
stages (`waitForSession`, `waitForLoadingComplete`, `waitForDisplaysDone`,
|
|
101
|
+
`waitForQuiescent`, ...) are exported too, and each one documents what it can
|
|
102
|
+
and cannot tell you.
|
|
87
103
|
- **`waitForAppSettled(page)`** is the wait after you CLICK something. A page
|
|
88
104
|
that is loading starts out `loading` and the transition into `ready` is it
|
|
89
105
|
finishing; a page you just clicked is already `ready` and stays that way until
|
|
@@ -101,11 +117,11 @@ way, which is the vacuous-gate problem again, one step later.
|
|
|
101
117
|
Here every stage reports its outcome, and an unsettled one throws by default,
|
|
102
118
|
naming the gate:
|
|
103
119
|
|
|
104
|
-
```
|
|
105
|
-
gave up waiting after
|
|
106
|
-
never
|
|
107
|
-
|
|
108
|
-
|
|
120
|
+
```text
|
|
121
|
+
gave up waiting after 60000ms: the app never held itself ready; display(s)
|
|
122
|
+
never painted: LinearAlignmentsDisplay (volvox_cram-LinearAlignmentsDisplay) is
|
|
123
|
+
loading. Raise the timeout, or pass allowUnsettled (--allowUnsettled) to capture
|
|
124
|
+
the frame as it stands.
|
|
109
125
|
```
|
|
110
126
|
|
|
111
127
|
`allowUnsettled` (`--allowUnsettled`) takes the frame as it stands instead, and
|
|
@@ -113,16 +129,21 @@ still tells you what did not settle.
|
|
|
113
129
|
|
|
114
130
|
## Reading the result
|
|
115
131
|
|
|
116
|
-
|
|
132
|
+
Three fields on a successful capture:
|
|
117
133
|
|
|
118
134
|
- **`unsettled`** — stages that hit their timeout. Empty unless you asked to
|
|
119
135
|
proceed anyway.
|
|
120
|
-
- **`pending`** — displays still reporting unpainted when the shutter fired
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
136
|
+
- **`pending`** — displays still reporting unpainted when the shutter fired,
|
|
137
|
+
each with its own phase: `loading` is a slow fetch, `error` a banner, `ready`
|
|
138
|
+
a display claiming it finished without drawing. Read after `settle`, not
|
|
139
|
+
before it, so the frame it describes is the frame that was captured; a display
|
|
140
|
+
that finished during the settle is not in it, and no longer fails the run
|
|
141
|
+
either. Whatever is left lands in `unsettled` too, as does a display whose
|
|
142
|
+
renderer failed (`renderError`).
|
|
143
|
+
- **`tooLarge`** — displays showing "Requested too much data" instead of their
|
|
144
|
+
features, because the region is wider than the track fetches without being
|
|
145
|
+
asked. The capture still succeeds, since that is the app working as designed,
|
|
146
|
+
and the CLI prints a warning naming them. Narrow the location to draw them.
|
|
126
147
|
|
|
127
148
|
## CLI
|
|
128
149
|
|
package/esm/args.d.ts
CHANGED
|
@@ -4,8 +4,8 @@ export interface ParsedArgs {
|
|
|
4
4
|
assembly?: string;
|
|
5
5
|
loc?: string;
|
|
6
6
|
tracks: string[];
|
|
7
|
+
spec?: string;
|
|
7
8
|
session?: string;
|
|
8
|
-
sessionName?: string;
|
|
9
9
|
instance?: string;
|
|
10
10
|
out?: string;
|
|
11
11
|
width?: number;
|
|
@@ -17,6 +17,7 @@ export interface ParsedArgs {
|
|
|
17
17
|
headed: boolean;
|
|
18
18
|
verbose: boolean;
|
|
19
19
|
help: boolean;
|
|
20
|
+
version: boolean;
|
|
20
21
|
allowUnsettled: boolean;
|
|
21
22
|
/** What `list` reads its hub and filter from. Empty for every other form. */
|
|
22
23
|
positionals: string[];
|
package/esm/args.js
CHANGED
|
@@ -7,8 +7,8 @@ const OPTIONS = {
|
|
|
7
7
|
config: { type: 'string' },
|
|
8
8
|
assembly: { type: 'string' },
|
|
9
9
|
loc: { type: 'string' },
|
|
10
|
+
spec: { type: 'string' },
|
|
10
11
|
session: { type: 'string' },
|
|
11
|
-
sessionName: { type: 'string' },
|
|
12
12
|
instance: { type: 'string' },
|
|
13
13
|
out: { type: 'string', short: 'o' },
|
|
14
14
|
track: { type: 'string', multiple: true, default: [] },
|
|
@@ -21,6 +21,7 @@ const OPTIONS = {
|
|
|
21
21
|
headed: { type: 'boolean', default: false },
|
|
22
22
|
verbose: { type: 'boolean', default: false },
|
|
23
23
|
help: { type: 'boolean', short: 'h', default: false },
|
|
24
|
+
version: { type: 'boolean', short: 'v', default: false },
|
|
24
25
|
allowUnsettled: { type: 'boolean', default: false },
|
|
25
26
|
};
|
|
26
27
|
function finite(name, raw) {
|
package/esm/bin.js
CHANGED
|
@@ -2,52 +2,50 @@
|
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
3
|
import { parseArgs } from './args.js';
|
|
4
4
|
import { captureJBrowse } from './capture.js';
|
|
5
|
-
import {
|
|
5
|
+
import { resolveAgainstConfig } from './catalog.js';
|
|
6
|
+
import { listHubAssemblies, listHubTracks } from './hub.js';
|
|
7
|
+
import { describePendingDisplays } from './sessionGate.js';
|
|
6
8
|
import { jbrowseUrl } from './url.js';
|
|
7
9
|
const HELP = `jb2capture — screenshot a live JBrowse 2 view, once it has finished drawing
|
|
8
10
|
|
|
9
11
|
USAGE
|
|
10
12
|
jb2capture [flags] --out <file.png> open a view, wait for it, screenshot it
|
|
11
13
|
jb2capture url [flags] print the URL instead of launching a browser
|
|
12
|
-
jb2capture list
|
|
14
|
+
jb2capture list [hub] [filter] list hosted assemblies, or one's trackIds
|
|
13
15
|
|
|
14
16
|
WHAT TO SHOW
|
|
15
17
|
--hub <name> a genomes.jbrowse.org assembly: a UCSC db name (hg38, mm39)
|
|
16
18
|
or a GenArk accession (GCA_.../GCF_...)
|
|
17
19
|
--config <url> a config.json URL, for data that is not hosted there
|
|
18
20
|
--assembly <name> assembly to open (defaults to --hub)
|
|
19
|
-
--loc <locstring>
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
--
|
|
24
|
-
--session <json|path> a full session spec, for several views or per-display
|
|
25
|
-
settings. Replaces --assembly/--loc/--track.
|
|
21
|
+
--loc <locstring> chr1:1,000-2,000, several space-separated, or a gene name
|
|
22
|
+
where the config has a text index (hosted ones do)
|
|
23
|
+
--track <trackId> a track to open, by trackId or name; repeat for several
|
|
24
|
+
--spec <json|path|-> a session spec, for several views or per-display settings
|
|
25
|
+
--session <path|-> a session saved with File → Export session
|
|
26
26
|
--instance <url> JBrowse Web deployment to drive
|
|
27
27
|
(default https://jbrowse.org/code/jb2/latest/)
|
|
28
|
-
--sessionName <name> name the opened session carries
|
|
29
28
|
|
|
30
29
|
THE IMAGE
|
|
31
|
-
--out, -o <file>
|
|
30
|
+
--out, -o <file> .png, .jpg or .webp to write (required unless using a
|
|
31
|
+
subcommand)
|
|
32
32
|
--width <px> viewport width (default 1400)
|
|
33
33
|
--height <px> viewport height (default 900)
|
|
34
34
|
--scale <n> device pixel ratio (default 2)
|
|
35
|
-
--fullPage
|
|
35
|
+
--fullPage grow the image until every view fits, not just the
|
|
36
|
+
--height of the viewport
|
|
36
37
|
|
|
37
38
|
WAITING
|
|
38
39
|
--timeout <ms> budget per wait stage (default 60000)
|
|
39
40
|
--settle <ms> extra pause before the census and the shot (default 0)
|
|
40
41
|
--allowUnsettled write the image anyway when a stage times out
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
you asked for, the view resolving, tracks fetching, displays painting, and any
|
|
44
|
-
leftover "Loading…" text. A stage that times out FAILS the run rather than
|
|
45
|
-
quietly handing back a half-drawn frame — raise --timeout, or pass
|
|
46
|
-
--allowUnsettled if you want the frame as it stands.
|
|
43
|
+
A stage that times out fails the run rather than writing a half-drawn frame.
|
|
47
44
|
|
|
48
45
|
OTHER
|
|
49
46
|
--headed run with a visible browser window
|
|
50
47
|
--verbose print browser console output (GPU noise filtered)
|
|
48
|
+
--version, -v the package version
|
|
51
49
|
--help, -h this text
|
|
52
50
|
|
|
53
51
|
EXAMPLES
|
|
@@ -62,16 +60,24 @@ EXAMPLES
|
|
|
62
60
|
jb2capture --config https://example.org/config.json --assembly mydata \\
|
|
63
61
|
--loc "chr3:25,325,000-25,361,000 chr10:58,716,500-58,718,500" -o two.png
|
|
64
62
|
`;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
63
|
+
// inline JSON, `-` for stdin, or a path: the forms `jb2export --spec` reads
|
|
64
|
+
function readJson(flag, value) {
|
|
65
|
+
const text = value === '-'
|
|
66
|
+
? readFileSync(0, 'utf8')
|
|
67
|
+
: value.trimStart().startsWith('{')
|
|
68
|
+
? value
|
|
69
|
+
: readFileSync(value, 'utf8');
|
|
69
70
|
const parsed = JSON.parse(text);
|
|
70
71
|
if (typeof parsed !== 'object' || parsed === null) {
|
|
71
|
-
throw new Error(
|
|
72
|
+
throw new Error(`--${flag} must be a JSON object`);
|
|
72
73
|
}
|
|
73
74
|
return parsed;
|
|
74
75
|
}
|
|
76
|
+
// a bare view object is the one-view spec it would be wrapped in
|
|
77
|
+
function readSpec(value) {
|
|
78
|
+
const spec = readJson('spec', value);
|
|
79
|
+
return 'type' in spec && !('views' in spec) ? { views: [spec] } : spec;
|
|
80
|
+
}
|
|
75
81
|
function urlOptions(args) {
|
|
76
82
|
return {
|
|
77
83
|
hub: args.hub,
|
|
@@ -79,16 +85,24 @@ function urlOptions(args) {
|
|
|
79
85
|
assembly: args.assembly,
|
|
80
86
|
loc: args.loc,
|
|
81
87
|
tracks: args.tracks.length ? args.tracks : undefined,
|
|
82
|
-
|
|
83
|
-
|
|
88
|
+
spec: args.spec ? readSpec(args.spec) : undefined,
|
|
89
|
+
session: args.session ? readJson('session', args.session) : undefined,
|
|
84
90
|
instance: args.instance,
|
|
85
91
|
};
|
|
86
92
|
}
|
|
87
93
|
async function runList(positionals) {
|
|
88
94
|
const [hub, filter] = positionals;
|
|
89
95
|
if (!hub) {
|
|
90
|
-
|
|
91
|
-
|
|
96
|
+
const assemblies = await listHubAssemblies();
|
|
97
|
+
const pad = Math.max(...assemblies.map(a => a.name.length));
|
|
98
|
+
for (const a of assemblies) {
|
|
99
|
+
const about = [a.organism, a.description].filter(Boolean).join(' — ');
|
|
100
|
+
console.log(` ${a.name.padEnd(pad)} ${about}`);
|
|
101
|
+
}
|
|
102
|
+
console.log('\nGenArk accessions (GCA_…/GCF_…) work as --hub too; browse them at ' +
|
|
103
|
+
"https://genomes.jbrowse.org. List one assembly's tracks with " +
|
|
104
|
+
'`jb2capture list <name> [filter]`.');
|
|
105
|
+
return;
|
|
92
106
|
}
|
|
93
107
|
const tracks = await listHubTracks(hub, filter);
|
|
94
108
|
if (!tracks.length) {
|
|
@@ -116,18 +130,23 @@ async function main() {
|
|
|
116
130
|
console.log(HELP);
|
|
117
131
|
return;
|
|
118
132
|
}
|
|
133
|
+
if (args.version) {
|
|
134
|
+
const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
135
|
+
console.log(version);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
119
138
|
if (first === 'list') {
|
|
120
139
|
await runList(args.positionals);
|
|
121
140
|
return;
|
|
122
141
|
}
|
|
123
142
|
if (first === 'url') {
|
|
124
|
-
console.log(jbrowseUrl(urlOptions(args)));
|
|
143
|
+
console.log(jbrowseUrl(await resolveAgainstConfig(urlOptions(args))));
|
|
125
144
|
return;
|
|
126
145
|
}
|
|
127
146
|
if (!args.out) {
|
|
128
147
|
throw new Error('--out <file.png> is required (or use `jb2capture url`)');
|
|
129
148
|
}
|
|
130
|
-
const { url, pending, unsettled } = await captureJBrowse({
|
|
149
|
+
const { url, pending, tooLarge, unsettled } = await captureJBrowse({
|
|
131
150
|
...urlOptions(args),
|
|
132
151
|
out: args.out,
|
|
133
152
|
width: args.width,
|
|
@@ -155,7 +174,11 @@ async function main() {
|
|
|
155
174
|
}
|
|
156
175
|
if (pending.length) {
|
|
157
176
|
console.error(`warning: ${pending.length} display(s) had not finished drawing at capture time ` +
|
|
158
|
-
`(${pending
|
|
177
|
+
`(${describePendingDisplays(pending)}). Raise --timeout, or --settle for a slow remote file.`);
|
|
178
|
+
}
|
|
179
|
+
if (tooLarge.length) {
|
|
180
|
+
console.error(`warning: ${tooLarge.length} display(s) show "too much data" instead of their features ` +
|
|
181
|
+
`(${tooLarge.map(d => d.name).join(', ')}). Narrow --loc to draw them.`);
|
|
159
182
|
}
|
|
160
183
|
}
|
|
161
184
|
try {
|
package/esm/browser.d.ts
CHANGED
package/esm/browser.js
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
-
// Just the sandbox flags, for the scripts that MEASURE cross-origin behaviour
|
|
3
|
-
// (COOP/COEP isolation, cancelled cross-origin fetches) and would be measuring
|
|
4
|
-
// nothing with web security off. They used to spell this
|
|
5
|
-
// `BASE_CHROME_ARGS.filter(a => a !== '--disable-web-security')`, which silently
|
|
6
|
-
// stops subtracting the day another security flag is added below.
|
|
7
|
-
export const SANDBOX_CHROME_ARGS = ['--no-sandbox', '--disable-setuid-sandbox'];
|
|
8
2
|
// Chrome flags both harnesses launch with. Backend-specific flags
|
|
9
3
|
// (--disable-gpu, --enable-unsafe-swiftshader, --disable-popup-blocking) are
|
|
10
4
|
// appended by each caller.
|
|
11
5
|
export const BASE_CHROME_ARGS = [
|
|
12
|
-
|
|
6
|
+
'--no-sandbox',
|
|
7
|
+
'--disable-setuid-sandbox',
|
|
13
8
|
'--disable-web-security',
|
|
14
9
|
];
|
|
15
10
|
const CHROME_PATHS = [
|
|
@@ -30,10 +25,11 @@ const CHROME_PATHS = [
|
|
|
30
25
|
export function findChromeExecutable() {
|
|
31
26
|
return process.env.CHROME_PATH ?? CHROME_PATHS.find(p => fs.existsSync(p));
|
|
32
27
|
}
|
|
33
|
-
//
|
|
34
|
-
//
|
|
28
|
+
// Console chatter that isn't a real error. Real GPU failures (`context LOST`,
|
|
29
|
+
// `GL error`) are deliberately NOT suppressed.
|
|
35
30
|
const NOISE_NEEDLES = [
|
|
36
31
|
'favicon',
|
|
32
|
+
'window.jb drives this app programmatically',
|
|
37
33
|
'GPU stall',
|
|
38
34
|
'[GPU] WebGPU not supported',
|
|
39
35
|
'[GPU] No compatible GPU adapter',
|
package/esm/capture.d.ts
CHANGED
|
@@ -27,21 +27,22 @@ export interface OpenResult extends ReadyReport {
|
|
|
27
27
|
* reading state back out of `window.JBrowseSession`, capturing a dialog. For a
|
|
28
28
|
* plain image, `captureJBrowse` wraps the whole thing.
|
|
29
29
|
*
|
|
30
|
-
* The assembly and
|
|
30
|
+
* The assembly and trackIds asked for in the URL become the session gate's
|
|
31
31
|
* expectations unless you override them, so a mistyped trackId fails here rather
|
|
32
32
|
* than producing an image of an empty browser.
|
|
33
33
|
*/
|
|
34
34
|
export declare function openJBrowse(options?: OpenOptions): Promise<OpenResult>;
|
|
35
35
|
export interface CaptureOptions extends OpenOptions {
|
|
36
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* Image path to write, `.png`, `.jpg`/`.jpeg` or `.webp`. Omit to get the PNG
|
|
38
|
+
* buffer back and write it yourself.
|
|
39
|
+
*/
|
|
37
40
|
out?: string;
|
|
38
41
|
/**
|
|
39
|
-
* Capture
|
|
40
|
-
*
|
|
41
|
-
* `
|
|
42
|
-
*
|
|
43
|
-
* before the content re-rasters (measured in the browser-test suites as a
|
|
44
|
-
* 10–25% image diff that moves run to run).
|
|
42
|
+
* Capture every view rather than the viewport: the viewport grows by however
|
|
43
|
+
* far the session runs past it, and the frame is waited for again. Never
|
|
44
|
+
* puppeteer's `fullPage`, which measures the document (always the window's
|
|
45
|
+
* height here) and shoots before the resized content re-rasters.
|
|
45
46
|
*/
|
|
46
47
|
fullPage?: boolean;
|
|
47
48
|
}
|
package/esm/capture.js
CHANGED
|
@@ -2,10 +2,13 @@ import { mkdirSync } from 'node:fs';
|
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
3
|
import puppeteer from 'puppeteer';
|
|
4
4
|
import { BASE_CHROME_ARGS, findChromeExecutable, isBrowserConsoleNoise, } from './browser.js';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
5
|
+
import { resolveAgainstConfig } from './catalog.js';
|
|
6
|
+
import { assertImagePath } from './imagePath.js';
|
|
7
|
+
import { assertSupportedInstance } from './instanceVersion.js';
|
|
8
|
+
import { waitForFrame, waitForJBrowseReady } from './ready.js';
|
|
9
|
+
import { assemblyFromSession, savedSnapshot, trackIdsFromSession, } from './session.js';
|
|
10
|
+
import { sessionOverflowInPage } from './sessionOverflow.js';
|
|
11
|
+
import { PUBLIC_INSTANCE, assertSessionStandsAlone, jbrowseUrl } from './url.js';
|
|
9
12
|
// Puppeteer's own `goto` default is 30s, half of what a caller passing nothing
|
|
10
13
|
// is told each wait stage gets. Defaulted here so the navigation, the chain and
|
|
11
14
|
// the fullPage re-settle all run on one budget.
|
|
@@ -18,18 +21,21 @@ const DEFAULT_TIMEOUT = 60000;
|
|
|
18
21
|
* reading state back out of `window.JBrowseSession`, capturing a dialog. For a
|
|
19
22
|
* plain image, `captureJBrowse` wraps the whole thing.
|
|
20
23
|
*
|
|
21
|
-
* The assembly and
|
|
24
|
+
* The assembly and trackIds asked for in the URL become the session gate's
|
|
22
25
|
* expectations unless you override them, so a mistyped trackId fails here rather
|
|
23
26
|
* than producing an image of an empty browser.
|
|
24
27
|
*/
|
|
25
28
|
export async function openJBrowse(options = {}) {
|
|
26
|
-
const { width = 1400, height = 900, deviceScaleFactor = 2, headless = true, executablePath = findChromeExecutable(), args = [], onConsole, timeout = DEFAULT_TIMEOUT, trackIds,
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
const url = jbrowseUrl(
|
|
29
|
+
const { width = 1400, height = 900, deviceScaleFactor = 2, headless = true, executablePath = findChromeExecutable(), args = [], onConsole, timeout = DEFAULT_TIMEOUT, trackIds, ...given } = options;
|
|
30
|
+
assertSessionStandsAlone(given);
|
|
31
|
+
await assertSupportedInstance(given.instance ?? PUBLIC_INSTANCE);
|
|
32
|
+
const urlOptions = await resolveAgainstConfig(given);
|
|
33
|
+
// a named session keeps the header free of the timestamp an unnamed one gets,
|
|
34
|
+
// which otherwise differs on every capture of the same view
|
|
35
|
+
const url = jbrowseUrl({
|
|
36
|
+
...urlOptions,
|
|
37
|
+
sessionName: urlOptions.sessionName ?? 'Screenshot',
|
|
38
|
+
});
|
|
33
39
|
const browser = await puppeteer.launch({
|
|
34
40
|
headless,
|
|
35
41
|
executablePath,
|
|
@@ -50,28 +56,16 @@ export async function openJBrowse(options = {}) {
|
|
|
50
56
|
// never go idle, and the session gate below is a far better "it is up"
|
|
51
57
|
// signal than the absence of requests.
|
|
52
58
|
await page.goto(url, { waitUntil: 'domcontentloaded', timeout });
|
|
59
|
+
const { spec, session, assembly, hub, tracks } = urlOptions;
|
|
60
|
+
const opens = spec ?? (session && savedSnapshot(session));
|
|
61
|
+
// Spread rather than listed, so every ready option arrives by construction:
|
|
62
|
+
// a hand-copied list is what once dropped `allowUnsettled`.
|
|
53
63
|
const report = await waitForJBrowseReady(page, {
|
|
54
|
-
// Spread, not a hand-copied field list. `OpenOptions extends
|
|
55
|
-
// ReadyOptions`, so every present and future ready option arrives here by
|
|
56
|
-
// construction. Listing them by hand is what dropped `allowUnsettled`:
|
|
57
|
-
// it was declared, documented, recommended by this function's own timeout
|
|
58
|
-
// message, and silently never forwarded, so `--allowUnsettled` did
|
|
59
|
-
// nothing and a timing-out stage always threw. Overridden below: the
|
|
60
|
-
// defaulted timeout, and the two options derived from the URL.
|
|
61
64
|
...options,
|
|
62
65
|
timeout,
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
// would fail a capture that is entirely correct.
|
|
67
|
-
assembly: urlOptions.session
|
|
68
|
-
? (urlOptions.assembly ?? assemblyFromSession(urlOptions.session))
|
|
69
|
-
: (urlOptions.assembly ?? urlOptions.hub),
|
|
70
|
-
trackIds: trackIds ??
|
|
71
|
-
urlOptions.tracks ??
|
|
72
|
-
(urlOptions.session
|
|
73
|
-
? trackIdsFromSession(urlOptions.session)
|
|
74
|
-
: undefined),
|
|
66
|
+
// a session may open another assembly than the hub that supplies its config
|
|
67
|
+
assembly: opens ? assemblyFromSession(opens) : (assembly ?? hub),
|
|
68
|
+
trackIds: trackIds ?? (opens ? trackIdsFromSession(opens) : tracks),
|
|
75
69
|
});
|
|
76
70
|
return { browser, page, url, ...report };
|
|
77
71
|
}
|
|
@@ -90,35 +84,25 @@ export async function openJBrowse(options = {}) {
|
|
|
90
84
|
export async function captureJBrowse(options = {}) {
|
|
91
85
|
const { out, fullPage = false, ...openOptions } = options;
|
|
92
86
|
if (out) {
|
|
93
|
-
|
|
94
|
-
// at the screenshot, after the whole launch-navigate-wait cycle.
|
|
87
|
+
assertImagePath(out);
|
|
95
88
|
mkdirSync(dirname(out), { recursive: true });
|
|
96
89
|
}
|
|
97
|
-
const { browser, page, url, ...
|
|
90
|
+
const { browser, page, url, ...opened } = await openJBrowse(openOptions);
|
|
98
91
|
try {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
report.unsettled.push('the app never re-settled after the resize');
|
|
114
|
-
if (!openOptions.allowUnsettled) {
|
|
115
|
-
throw new Error(`gave up waiting after ${openOptions.timeout ?? DEFAULT_TIMEOUT}ms: ` +
|
|
116
|
-
'the app never re-settled after the fullPage resize. Raise the ' +
|
|
117
|
-
'timeout, or pass allowUnsettled (--allowUnsettled) to capture ' +
|
|
118
|
-
'the frame as it stands.');
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
92
|
+
let report = opened;
|
|
93
|
+
const overflow = fullPage ? await page.evaluate(sessionOverflowInPage) : 0;
|
|
94
|
+
const viewport = page.viewport();
|
|
95
|
+
if (viewport && overflow > 0) {
|
|
96
|
+
await page.setViewport({
|
|
97
|
+
...viewport,
|
|
98
|
+
height: viewport.height + overflow,
|
|
99
|
+
});
|
|
100
|
+
// the resize repaints, and can start work, so the report describes the
|
|
101
|
+
// frame after it
|
|
102
|
+
report = await waitForFrame(page, {
|
|
103
|
+
...openOptions,
|
|
104
|
+
timeout: openOptions.timeout ?? DEFAULT_TIMEOUT,
|
|
105
|
+
});
|
|
122
106
|
}
|
|
123
107
|
const image = await page.screenshot({ path: out });
|
|
124
108
|
return { url, image, ...report };
|
package/esm/catalog.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { JBrowseUrlOptions } from './url.ts';
|
|
2
|
+
interface CatalogTrack {
|
|
3
|
+
trackId: string;
|
|
4
|
+
name?: unknown;
|
|
5
|
+
}
|
|
6
|
+
interface CatalogAssembly {
|
|
7
|
+
name: string;
|
|
8
|
+
aliases?: string[];
|
|
9
|
+
}
|
|
10
|
+
/** The parts of a JBrowse config.json that name what can be opened. */
|
|
11
|
+
export interface Catalog {
|
|
12
|
+
assemblies?: CatalogAssembly[];
|
|
13
|
+
assembly?: CatalogAssembly;
|
|
14
|
+
tracks?: CatalogTrack[];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The canonical name of the assembly `input` names by name or alias, ignoring
|
|
18
|
+
* case. The canonical name is the one the app's census publishes, so the
|
|
19
|
+
* session gate compares against it rather than against an alias.
|
|
20
|
+
*/
|
|
21
|
+
export declare function resolveAssemblyName(catalog: Catalog, input: string): string;
|
|
22
|
+
/**
|
|
23
|
+
* The trackId `input` names, resolved the way `jb2export --track` resolves one:
|
|
24
|
+
* an exact trackId, then the id with the assembly prefix a hosted config puts
|
|
25
|
+
* on every track (`clinvarMain` for `hg38-clinvarMain`), then a single
|
|
26
|
+
* case-insensitive match on the id, the unprefixed id or the display name. A
|
|
27
|
+
* miss names the tracks whose id or name contains what was typed.
|
|
28
|
+
*/
|
|
29
|
+
export declare function resolveTrackId(tracks: CatalogTrack[], input: string, assemblyName: string): string;
|
|
30
|
+
/**
|
|
31
|
+
* The same options with the assembly and tracks checked against the config
|
|
32
|
+
* and spelled the way the config spells them, before any browser launches: a
|
|
33
|
+
* typo fails here with suggestions rather than as a session-gate timeout.
|
|
34
|
+
*
|
|
35
|
+
* A config this cannot fetch or parse passes through unchanged, and the app
|
|
36
|
+
* reports it. A spec or saved session passes through too, since it may declare
|
|
37
|
+
* tracks of its own that no config lists.
|
|
38
|
+
*/
|
|
39
|
+
export declare function resolveAgainstConfig(options: JBrowseUrlOptions): Promise<JBrowseUrlOptions>;
|
|
40
|
+
export {};
|