@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 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 config URL that 404s, a trackId the config does not define
42
- and an assembly name that does not match the config all fail here, loudly,
43
- and this is the only place they fail at all.
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
- **A build that publishes no `[data-app-phase]` is an error, not a slower path.**
56
- The fallback chain that used to run there was those negative signals plus a
57
- seen-busy-then-quiet heuristic to compensate for them, and it existed for builds
58
- older than the marker. Point `--instance` at a build that has it.
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 and read state back
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. The individual stages (`waitForSession`,
85
- `waitForLoadingComplete`, `waitForDisplaysDone`, `waitForQuiescent`, ...) are
86
- exported too, and each one documents what it can and cannot tell you.
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 2000ms: the loading overlay never cleared (a track fetch
106
- never finished). Raise the timeout if the page is merely slow; if it never
107
- finishes, open the same URL in a browser — this gate has no content to fall
108
- through to.
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
- Two fields on a successful capture:
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
- Read after `settle`, not before it, so the frame it describes is the frame
122
- that was captured; a display that finished during the settle is not in it, and
123
- no longer fails the run either. Whatever is left lands in `unsettled` too,
124
- carrying each display's own phase — `loading` is a slow fetch, `error` a
125
- banner, `ready` a display claiming it finished without drawing.
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 { listHubTracks } from './hub.js';
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 <hub> [filter] list a hosted assembly's trackIds
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> where to go: chr1:1,000-2,000, or several space-separated
20
- for a discontinuous view. On a config with a text index —
21
- every UCSC assembly on genomes.jbrowse.org has one — a
22
- gene name such as BRCA1 also works.
23
- --track <trackId> a track to open; repeat for several
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> PNG to write (required unless using a subcommand)
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 capture the whole scrollable page, not just the viewport
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
- Every stage is waited on for you: the session holding the assembly and tracks
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
- function readSession(value) {
66
- const text = value.trimStart().startsWith('{')
67
- ? value
68
- : readFileSync(value, 'utf8');
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('--session must be a JSON object');
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
- session: args.session ? readSession(args.session) : undefined,
83
- sessionName: args.sessionName,
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
- throw new Error('list needs an assembly, e.g. `jb2capture list hg38`. ' +
91
- 'Browse them at https://genomes.jbrowse.org.');
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.join(', ')}). Raise --timeout, or --settle for a slow remote file.`);
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
@@ -1,4 +1,3 @@
1
- export declare const SANDBOX_CHROME_ARGS: string[];
2
1
  export declare const BASE_CHROME_ARGS: string[];
3
2
  export declare function findChromeExecutable(): string | undefined;
4
3
  export declare function isBrowserConsoleNoise(text: string): boolean;
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
- ...SANDBOX_CHROME_ARGS,
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
- // GPU/WebGL lifecycle chatter that isn't a real error. Real GPU failures
34
- // (`context LOST`, `GL error`) are deliberately NOT suppressed.
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 track count asked for in the URL become the session gate's
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
- /** PNG path to write. Omit to get the buffer back and write it yourself. */
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 the whole scrollable page rather than the viewport. Implemented by
40
- * growing the viewport to the page height and re-settling, never by
41
- * `page.screenshot({ fullPage: true })` — puppeteer implements that flag with
42
- * the same viewport resize but shoots immediately, and the capture can return
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 { waitForJBrowseReady } from './ready.js';
6
- import { assemblyFromSession, trackIdsFromSession } from './session.js';
7
- import { jbrowseUrl } from './url.js';
8
- import { waitForAppSettled } from './waits.js';
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 track count asked for in the URL become the session gate's
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
- // `assembly` is deliberately NOT pulled out here: it is both a URL option
28
- // (which assembly to open) and the session gate's expectation (which
29
- // assembly must end up open), and destructuring it for the second use would
30
- // have dropped it from the first, silently.
31
- ...urlOptions } = options;
32
- const url = jbrowseUrl(urlOptions);
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
- // A session spec's own assembly wins over the hub name. `--hub hg38
64
- // --session spec.json` where the spec opens something else is legitimate
65
- // (the hub is just supplying the config), and expecting the hub name there
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
- // Before the browser launches: a missing parent directory otherwise fails
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, ...report } = await openJBrowse(openOptions);
90
+ const { browser, page, url, ...opened } = await openJBrowse(openOptions);
98
91
  try {
99
- if (fullPage) {
100
- const viewport = page.viewport();
101
- const pageHeight = await page.evaluate(() => Math.ceil(Math.max(document.documentElement.scrollHeight, document.body.scrollHeight)));
102
- if (viewport && pageHeight > viewport.height) {
103
- await page.setViewport({ ...viewport, height: pageHeight });
104
- // The resize invalidates the raster and can start work (a display that
105
- // grew gained rows to draw), so the frame has to settle again — on the
106
- // same budget the chain used, and with its outcome reported. Dropping
107
- // the outcome made this the one stage that could give up and still
108
- // report `unsettled: []`, over the frame most likely to be half-drawn.
109
- const settled = await waitForAppSettled(page, {
110
- timeout: openOptions.timeout ?? DEFAULT_TIMEOUT,
111
- });
112
- if (!settled) {
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 };
@@ -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 {};