react-msaview 6.1.0 → 6.2.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.
Files changed (57) hide show
  1. package/bundle/index.js +92 -92
  2. package/bundle/index.js.map +4 -4
  3. package/dist/components/dialogs/ExportSVGDialog.js +48 -20
  4. package/dist/components/dialogs/ExportSVGDialog.js.map +1 -1
  5. package/dist/components/getVisibleLeaves.d.ts +8 -0
  6. package/dist/components/getVisibleLeaves.js +13 -6
  7. package/dist/components/getVisibleLeaves.js.map +1 -1
  8. package/dist/components/header/settingsMenuItems.js +1 -1
  9. package/dist/components/header/settingsMenuItems.js.map +1 -1
  10. package/dist/components/minimap/Minimap.js +1 -1
  11. package/dist/components/minimap/Minimap.js.map +1 -1
  12. package/dist/components/minimap/MinimapSVG.d.ts +4 -4
  13. package/dist/components/minimap/MinimapSVG.js +16 -10
  14. package/dist/components/minimap/MinimapSVG.js.map +1 -1
  15. package/dist/components/minimap/minimapLayout.d.ts +5 -8
  16. package/dist/components/minimap/minimapLayout.js +10 -11
  17. package/dist/components/minimap/minimapLayout.js.map +1 -1
  18. package/dist/components/msa/msaRaster.d.ts +32 -2
  19. package/dist/components/msa/msaRaster.js +93 -15
  20. package/dist/components/msa/msaRaster.js.map +1 -1
  21. package/dist/components/tree/renderTreeCanvas.js +22 -19
  22. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  23. package/dist/{renderTestEnv.d.ts → headlessRenderEnv.d.ts} +15 -13
  24. package/dist/{renderTestEnv.js → headlessRenderEnv.js} +22 -18
  25. package/dist/headlessRenderEnv.js.map +1 -0
  26. package/dist/index.d.ts +1 -0
  27. package/dist/index.js +3 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/model/treeModel.d.ts +1 -1
  30. package/dist/model/treeModel.js +1 -1
  31. package/dist/renderToSvg.js +115 -36
  32. package/dist/renderToSvg.js.map +1 -1
  33. package/dist/version.d.ts +1 -1
  34. package/dist/version.js +1 -1
  35. package/package.json +3 -3
  36. package/src/components/dialogs/ExportSVGDialog.tsx +55 -20
  37. package/src/components/getVisibleLeaves.ts +25 -9
  38. package/src/components/header/settingsMenuItems.ts +1 -1
  39. package/src/components/minimap/Minimap.tsx +1 -4
  40. package/src/components/minimap/MinimapSVG.tsx +50 -35
  41. package/src/components/minimap/minimapLayout.test.ts +16 -5
  42. package/src/components/minimap/minimapLayout.ts +10 -11
  43. package/src/components/msa/msaRaster.ts +124 -17
  44. package/src/components/tree/renderTreeCanvas.test.ts +55 -0
  45. package/src/components/tree/renderTreeCanvas.ts +23 -18
  46. package/src/{renderTestEnv.ts → headlessRenderEnv.ts} +23 -19
  47. package/src/impgRender.test.tsx +2 -2
  48. package/src/index.ts +6 -0
  49. package/src/model/treeModel.ts +1 -1
  50. package/src/renderDomainsSvg.test.tsx +2 -2
  51. package/src/renderMinimapSvg.test.tsx +29 -15
  52. package/src/renderRasterSvg.test.tsx +216 -0
  53. package/src/renderSequenceLogoSvg.test.tsx +6 -4
  54. package/src/renderToSvg.test.tsx +79 -0
  55. package/src/renderToSvg.tsx +201 -65
  56. package/src/version.ts +1 -1
  57. package/dist/renderTestEnv.js.map +0 -1
@@ -1,6 +1,6 @@
1
1
  /**
2
- * jsdom shims for the SVG-export tests. Test-only -- nothing in the viewer
3
- * imports this.
2
+ * DOM shims for driving `renderToSvg` outside a browser -- the SVG-export
3
+ * tests, the README figure generator, and react-msaview-cli all need them.
4
4
  *
5
5
  * jsdom implements neither DOMMatrix nor a canvas 2D context, and
6
6
  * `@jbrowse/svgcanvas` needs both: it tracks the current transform as a real
@@ -12,7 +12,7 @@
12
12
  * leaves the rest undefined -- every emitted transform then carries `NaN` in its
13
13
  * x components, which no assertion on a drawn x-position can survive.
14
14
  */
15
- export class TestDOMMatrix {
15
+ export class HeadlessDOMMatrix {
16
16
  a: number
17
17
  b: number
18
18
  c: number
@@ -30,8 +30,8 @@ export class TestDOMMatrix {
30
30
  this.f = f
31
31
  }
32
32
 
33
- multiply(o: TestDOMMatrix) {
34
- return new TestDOMMatrix([
33
+ multiply(o: HeadlessDOMMatrix) {
34
+ return new HeadlessDOMMatrix([
35
35
  this.a * o.a + this.c * o.b,
36
36
  this.b * o.a + this.d * o.b,
37
37
  this.a * o.c + this.c * o.d,
@@ -42,22 +42,22 @@ export class TestDOMMatrix {
42
42
  }
43
43
 
44
44
  translate(x: number, y = 0) {
45
- return this.multiply(new TestDOMMatrix([1, 0, 0, 1, x, y]))
45
+ return this.multiply(new HeadlessDOMMatrix([1, 0, 0, 1, x, y]))
46
46
  }
47
47
 
48
48
  scale(x: number, y = x) {
49
- return this.multiply(new TestDOMMatrix([x, 0, 0, y, 0, 0]))
49
+ return this.multiply(new HeadlessDOMMatrix([x, 0, 0, y, 0, 0]))
50
50
  }
51
51
  }
52
52
 
53
- export class TestDOMPoint {
53
+ export class HeadlessDOMPoint {
54
54
  constructor(
55
55
  public x = 0,
56
56
  public y = 0,
57
57
  ) {}
58
58
 
59
- matrixTransform(m: TestDOMMatrix) {
60
- return new TestDOMPoint(
59
+ matrixTransform(m: HeadlessDOMMatrix) {
60
+ return new HeadlessDOMPoint(
61
61
  m.a * this.x + m.c * this.y + m.e,
62
62
  m.b * this.x + m.d * this.y + m.f,
63
63
  )
@@ -65,18 +65,22 @@ export class TestDOMPoint {
65
65
  }
66
66
 
67
67
  /**
68
- * Install the shims. Call from `beforeAll` in any test that exports SVG.
68
+ * Install the shims, into `globalThis` by default. A Node caller that builds
69
+ * its own jsdom passes that window instead, because Node has no global
70
+ * HTMLCanvasElement to patch.
69
71
  *
70
72
  * `measureText` reports 0.6em per character, close enough to a real sans-serif
71
- * average for layout assertions and, more usefully, exactly predictable: a test
72
- * can compute the width a renderer will see rather than hardcoding a number
73
+ * average for layout and, more usefully, exactly predictable: a caller can
74
+ * compute the width a renderer will see rather than hardcoding a number
73
75
  * measured from one machine's fonts.
74
76
  */
75
- export function installRenderTestEnv() {
77
+ export function installHeadlessRenderEnv(win: unknown = globalThis) {
76
78
  const g = globalThis as Record<string, unknown>
77
- g.DOMMatrix = TestDOMMatrix
78
- g.DOMPoint = TestDOMPoint
79
- HTMLCanvasElement.prototype.getContext = function () {
79
+ g.DOMMatrix = HeadlessDOMMatrix
80
+ g.DOMPoint = HeadlessDOMPoint
81
+ const canvas = (win as { HTMLCanvasElement: typeof HTMLCanvasElement })
82
+ .HTMLCanvasElement
83
+ canvas.prototype.getContext = function () {
80
84
  let font = '10px sans-serif'
81
85
  return {
82
86
  get font() {
@@ -86,11 +90,11 @@ export function installRenderTestEnv() {
86
90
  font = v
87
91
  },
88
92
  measureText: (t: string) => ({
89
- width: t.length * (Number.parseFloat(font) || 10) * 0.6,
93
+ width: t.length * (Number.parseFloat(font) || 10) * CHAR_WIDTH_RATIO,
90
94
  }),
91
95
  } as unknown as CanvasRenderingContext2D
92
96
  } as unknown as typeof HTMLCanvasElement.prototype.getContext
93
97
  }
94
98
 
95
99
  /** the per-character width factor `measureText` above reports */
96
- export const TEST_CHAR_WIDTH_RATIO = 0.6
100
+ export const CHAR_WIDTH_RATIO = 0.6
@@ -11,13 +11,13 @@ import { createJBrowseTheme } from '@jbrowse/core/ui/theme'
11
11
  import { enableStaticRendering } from 'mobx-react'
12
12
  import { beforeAll, expect, test } from 'vitest'
13
13
 
14
+ import { installHeadlessRenderEnv } from './headlessRenderEnv.ts'
14
15
  import MSAModelF from './model.ts'
15
- import { installRenderTestEnv } from './renderTestEnv.ts'
16
16
  import { renderToSvg } from './renderToSvg.tsx'
17
17
 
18
18
  beforeAll(() => {
19
19
  enableStaticRendering(true)
20
- installRenderTestEnv()
20
+ installHeadlessRenderEnv()
21
21
  })
22
22
 
23
23
  // representative `impg query -o fasta-aln` block: PanSN names with region
package/src/index.ts CHANGED
@@ -6,6 +6,12 @@
6
6
  // members they rely on (e.g. mouseCol/setMousePos, setHighlightedColumns,
7
7
  // seqPosToVisibleCol/visibleColToSeqPos) are flagged inline in model.ts.
8
8
  export { renderToSvg } from './renderToSvg.tsx'
9
+ // renderToSvg outside a browser needs DOM bits jsdom omits; react-msaview-cli
10
+ // and the README figure generator both drive it that way
11
+ export {
12
+ CHAR_WIDTH_RATIO,
13
+ installHeadlessRenderEnv,
14
+ } from './headlessRenderEnv.ts'
9
15
  export { default as MSAView } from './components/Loading.tsx'
10
16
  export { default as MSAViewer } from './components/MSAViewer.tsx'
11
17
  export { type MsaViewModel, default as MSAModelF } from './model.ts'
@@ -56,7 +56,7 @@ export function TreeModelF() {
56
56
 
57
57
  /**
58
58
  * #property
59
- * draw clickable node bubbles on the tree
59
+ * draw node bubbles on the tree; the branches stay clickable either way
60
60
  */
61
61
  drawNodeBubbles: stripDefault(types.boolean, defaultDrawNodeBubbles),
62
62
 
@@ -8,13 +8,13 @@ import { createJBrowseTheme } from '@jbrowse/core/ui/theme'
8
8
  import { enableStaticRendering } from 'mobx-react'
9
9
  import { beforeAll, expect, test } from 'vitest'
10
10
 
11
+ import { installHeadlessRenderEnv } from './headlessRenderEnv.ts'
11
12
  import MSAModelF from './model.ts'
12
- import { installRenderTestEnv } from './renderTestEnv.ts'
13
13
  import { renderToSvg } from './renderToSvg.tsx'
14
14
 
15
15
  beforeAll(() => {
16
16
  enableStaticRendering(true)
17
- installRenderTestEnv()
17
+ installHeadlessRenderEnv()
18
18
  })
19
19
 
20
20
  const msa = `>seq1
@@ -1,26 +1,26 @@
1
1
  // @vitest-environment jsdom
2
2
  //
3
- // The viewport export draws a minimap by default, and nothing covered it. The
4
- // export lays the figure out on its own grid rather than the on-screen one, so
5
- // the minimap has to be told how wide to draw -- reading a width off the model
6
- // puts its trapezoid short of the alignment column beside it.
3
+ // The viewport export draws a minimap by default, and nothing covered it. It
4
+ // spans the same alignment canvas the live minimap does, so its trapezoid has
5
+ // to reach the right edge of the exported alignment column -- and it is drawn
6
+ // only when the live view shows one at all.
7
7
  import { createJBrowseTheme } from '@jbrowse/core/ui/theme'
8
8
  import { enableStaticRendering } from 'mobx-react'
9
9
  import { beforeAll, expect, test } from 'vitest'
10
10
 
11
+ import { installHeadlessRenderEnv } from './headlessRenderEnv.ts'
11
12
  import MSAModelF from './model.ts'
12
- import { installRenderTestEnv } from './renderTestEnv.ts'
13
13
  import { renderToSvg } from './renderToSvg.tsx'
14
14
 
15
15
  beforeAll(() => {
16
16
  enableStaticRendering(true)
17
- installRenderTestEnv()
17
+ installHeadlessRenderEnv()
18
18
  })
19
19
 
20
20
  const width = 1000
21
21
 
22
- function makeModel() {
23
- const row = 'ACDEFGHIKL'.repeat(40)
22
+ function makeModel(cols = 400) {
23
+ const row = 'ACDEFGHIKL'.repeat(cols / 10)
24
24
  const model = MSAModelF().create({
25
25
  type: 'MsaView',
26
26
  msaFormat: 'fasta',
@@ -31,19 +31,24 @@ function makeModel() {
31
31
  return model
32
32
  }
33
33
 
34
- test('the exported minimap spans the exported alignment column', async () => {
35
- const model = makeModel()
36
- expect(model.showHorizontalScrollbar).toBe(true)
37
-
38
- const svg = await renderToSvg(model, {
34
+ function exportViewport(model: ReturnType<typeof makeModel>) {
35
+ return renderToSvg(model, {
39
36
  theme: createJBrowseTheme(),
40
37
  exportType: 'viewport',
41
38
  includeMinimap: true,
42
39
  includeTracks: false,
43
40
  })
41
+ }
44
42
 
45
- // the minimap column runs from the tree area to the right edge of the figure
46
- const minimapWidth = width - model.treeAreaWidth
43
+ test('the exported minimap spans the exported alignment column', async () => {
44
+ const model = makeModel()
45
+ expect(model.showHorizontalScrollbar).toBe(true)
46
+
47
+ const svg = await exportViewport(model)
48
+
49
+ // the minimap column is the alignment canvas, which is what the export lays
50
+ // out beside the tree
51
+ const minimapWidth = model.msaCanvasWidth
47
52
  const polygon = /<polygon[^>]*points="([^"]*)"/.exec(svg)?.[1]
48
53
  expect(polygon).toBeDefined()
49
54
  // bottom edge of the trapezoid reaches the full width it was given
@@ -53,3 +58,12 @@ test('the exported minimap spans the exported alignment column', async () => {
53
58
  // and the bar outline is drawn to the same width
54
59
  expect(svg).toContain(`width="${minimapWidth}"`)
55
60
  })
61
+
62
+ test('no minimap when the alignment already fits across', async () => {
63
+ const model = makeModel(10)
64
+ expect(model.showHorizontalScrollbar).toBe(false)
65
+
66
+ // a minimap here would mark a viewport wider than the bar it sits in, and the
67
+ // live view draws none either
68
+ expect(await exportViewport(model)).not.toContain('<polygon')
69
+ })
@@ -0,0 +1,216 @@
1
+ // @vitest-environment jsdom
2
+ //
3
+ // The SVG export's raster background. jsdom has no canvas, so the export falls
4
+ // back to a <rect> per cell there and every other export test exercises that
5
+ // path; this file supplies the canvas the raster needs and checks the export
6
+ // takes it instead.
7
+ import { createJBrowseTheme } from '@jbrowse/core/ui/theme'
8
+ import { enableStaticRendering } from 'mobx-react'
9
+ import { beforeAll, expect, test } from 'vitest'
10
+
11
+ import MSAModelF from './model.ts'
12
+ import { renderToSvg } from './renderToSvg.tsx'
13
+
14
+ const dataUrl = 'data:image/png;base64,STUB'
15
+
16
+ // enough of a 2d context for the raster: a color parser, an ImageData that is
17
+ // really backed by bytes, and a canvas that reads back
18
+ beforeAll(() => {
19
+ enableStaticRendering(true)
20
+ globalThis.DOMMatrix = class {
21
+ a = 1
22
+ b = 0
23
+ c = 0
24
+ d = 1
25
+ e = 0
26
+ f = 0
27
+ constructor(init?: number[]) {
28
+ const [a = 1, b = 0, c = 0, d = 1, e = 0, f = 0] = init ?? []
29
+ Object.assign(this, { a, b, c, d, e, f })
30
+ }
31
+ multiply(o: any) {
32
+ return new (globalThis.DOMMatrix as any)([
33
+ this.a * o.a + this.c * o.b,
34
+ this.b * o.a + this.d * o.b,
35
+ this.a * o.c + this.c * o.d,
36
+ this.b * o.c + this.d * o.d,
37
+ this.a * o.e + this.c * o.f + this.e,
38
+ this.b * o.e + this.d * o.f + this.f,
39
+ ])
40
+ }
41
+ translate(x: number, y = 0) {
42
+ return this.multiply(
43
+ new (globalThis.DOMMatrix as any)([1, 0, 0, 1, x, y]),
44
+ )
45
+ }
46
+ scale(x: number, y = x) {
47
+ return this.multiply(
48
+ new (globalThis.DOMMatrix as any)([x, 0, 0, y, 0, 0]),
49
+ )
50
+ }
51
+ } as any
52
+ globalThis.DOMPoint = class {
53
+ constructor(
54
+ public x = 0,
55
+ public y = 0,
56
+ ) {}
57
+ matrixTransform(m: any) {
58
+ return new (globalThis.DOMPoint as any)(
59
+ m.a * this.x + m.c * this.y + m.e,
60
+ m.b * this.x + m.d * this.y + m.f,
61
+ )
62
+ }
63
+ } as any
64
+
65
+ HTMLCanvasElement.prototype.getContext = function () {
66
+ let font = '10px sans-serif'
67
+ let fillStyle = '#000000'
68
+ return {
69
+ get font() {
70
+ return font
71
+ },
72
+ set font(v: string) {
73
+ font = v
74
+ },
75
+ get fillStyle() {
76
+ return fillStyle
77
+ },
78
+ set fillStyle(v: string) {
79
+ fillStyle = v
80
+ },
81
+ measureText: (t: string) => ({
82
+ width: t.length * (Number.parseFloat(font) || 10) * 0.6,
83
+ }),
84
+ clearRect: () => {},
85
+ fillRect: () => {},
86
+ createImageData: (width: number, height: number) => ({
87
+ data: new Uint8ClampedArray(width * height * 4),
88
+ width,
89
+ height,
90
+ }),
91
+ putImageData: () => {},
92
+ getImageData: () => ({ data: Uint8ClampedArray.from([1, 2, 3, 255]) }),
93
+ } as unknown as CanvasRenderingContext2D
94
+ } as unknown as typeof HTMLCanvasElement.prototype.getContext
95
+ HTMLCanvasElement.prototype.toDataURL = () => dataUrl
96
+ })
97
+
98
+ function makeModel(rows: number, cols: number) {
99
+ const letters = 'ACDEFGHIKLMNPQRSTVWY'
100
+ const msa = Array.from({ length: rows }, (_, r) => {
101
+ let s = ''
102
+ for (let c = 0; c < cols; c++) {
103
+ s += letters[(r * 7 + c * 3) % letters.length]
104
+ }
105
+ return `>seq${r}\n${s}`
106
+ }).join('\n')
107
+ const model = MSAModelF().create({
108
+ id: 'raster',
109
+ type: 'MsaView',
110
+ height: 400,
111
+ msaFormat: 'fasta',
112
+ data: { msa },
113
+ })
114
+ model.setWidth(800)
115
+ return model
116
+ }
117
+
118
+ function exportEntire(model: ReturnType<typeof makeModel>) {
119
+ return renderToSvg(model, {
120
+ theme: createJBrowseTheme(),
121
+ exportType: 'entire',
122
+ })
123
+ }
124
+
125
+ test('the background is one image, not a rect per cell', async () => {
126
+ const model = makeModel(40, 300)
127
+ expect(model.bgColor).toBe(true)
128
+ expect(model.actuallyShowDomains).toBe(false)
129
+
130
+ const svg = await exportEntire(model)
131
+ const images = [...svg.matchAll(/<image[^>]*>/g)].map(m => m[0])
132
+
133
+ expect(images).toHaveLength(1)
134
+ expect(images[0]).toContain(dataUrl)
135
+ // the alignment's own rectangle, one pixel per cell scaled up by whole cells
136
+ expect(images[0]).toContain(`width="${300 * model.colWidth}"`)
137
+ expect(images[0]).toContain(`height="${40 * model.rowHeight}"`)
138
+ expect(images[0]).toContain('image-rendering="pixelated"')
139
+
140
+ // 12000 cells would have been 12000 rects
141
+ expect((svg.match(/<rect/g) ?? []).length).toBeLessThan(10)
142
+ })
143
+
144
+ test('letters still draw on top of the raster', async () => {
145
+ const model = makeModel(4, 10)
146
+ expect(model.showMsaLetters).toBe(true)
147
+
148
+ const svg = await exportEntire(model)
149
+
150
+ expect(svg).toContain('<image')
151
+ expect((svg.match(/<text/g) ?? []).length).toBeGreaterThanOrEqual(40)
152
+ })
153
+
154
+ test('the domain overlay keeps the vector path, which paints its own boxes', async () => {
155
+ const model = makeModel(4, 10)
156
+ model.setDomains({
157
+ seq0: {
158
+ xref: [{ id: 'seq0' }],
159
+ matches: [
160
+ {
161
+ signature: {
162
+ entry: { name: 'Kinase', accession: 'PF00069', description: '' },
163
+ },
164
+ locations: [{ start: 1, end: 5 }],
165
+ },
166
+ ],
167
+ },
168
+ })
169
+ expect(model.actuallyShowDomains).toBe(true)
170
+
171
+ expect(await exportEntire(model)).not.toContain('<image')
172
+ })
173
+
174
+ test('turning background color off leaves nothing for a raster to draw', async () => {
175
+ const model = makeModel(4, 10)
176
+ model.setBgColor(false)
177
+
178
+ expect(await exportEntire(model)).not.toContain('<image')
179
+ })
180
+
181
+ test('the exported minimap carries the alignment thumbnail', async () => {
182
+ const model = makeModel(20, 2000)
183
+ expect(model.showHorizontalScrollbar).toBe(true)
184
+
185
+ const svg = await renderToSvg(model, {
186
+ theme: createJBrowseTheme(),
187
+ exportType: 'viewport',
188
+ includeMinimap: true,
189
+ })
190
+
191
+ // the bar is the same downsampled alignment the live minimap draws, rather
192
+ // than the empty outline the export used to leave
193
+ const bar = [...svg.matchAll(/<image[^>]*>/g)]
194
+ .map(m => m[0])
195
+ .find(i => i.includes('height="12"'))
196
+ expect(bar).toBeDefined()
197
+ expect(bar).toContain(dataUrl)
198
+ expect(bar).toContain(`width="${model.msaCanvasWidth}"`)
199
+ })
200
+
201
+ test('each track is named in the tree column beside it', async () => {
202
+ const model = makeModel(4, 40)
203
+ model.toggleTrack('sequence-logo')
204
+ const names = model.turnedOnTracks.map(t => t.model.name)
205
+ expect(names.length).toBeGreaterThan(1)
206
+
207
+ const svg = await renderToSvg(model, {
208
+ theme: createJBrowseTheme(),
209
+ exportType: 'entire',
210
+ includeTracks: true,
211
+ })
212
+
213
+ for (const name of names) {
214
+ expect(svg).toContain(`>${name}</text>`)
215
+ }
216
+ })
@@ -10,13 +10,16 @@ import { createJBrowseTheme } from '@jbrowse/core/ui/theme'
10
10
  import { enableStaticRendering } from 'mobx-react'
11
11
  import { beforeAll, expect, test } from 'vitest'
12
12
 
13
+ import {
14
+ CHAR_WIDTH_RATIO,
15
+ installHeadlessRenderEnv,
16
+ } from './headlessRenderEnv.ts'
13
17
  import MSAModelF from './model.ts'
14
- import { TEST_CHAR_WIDTH_RATIO, installRenderTestEnv } from './renderTestEnv.ts'
15
18
  import { renderToSvg } from './renderToSvg.tsx'
16
19
 
17
20
  beforeAll(() => {
18
21
  enableStaticRendering(true)
19
- installRenderTestEnv()
22
+ installHeadlessRenderEnv()
20
23
  })
21
24
 
22
25
  // column 0 is fully conserved A, column 1 is a 3:1 split of C over G, and
@@ -159,8 +162,7 @@ test('letters are stretched to fill their column', async () => {
159
162
  model.toggleTrack('sequence-logo')
160
163
  const glyphs = await glyphsOf(model)
161
164
 
162
- const expected =
163
- colWidth / (model.sequenceLogoTrackHeight * TEST_CHAR_WIDTH_RATIO)
165
+ const expected = colWidth / (model.sequenceLogoTrackHeight * CHAR_WIDTH_RATIO)
164
166
  for (const glyph of glyphs) {
165
167
  expect(glyph.sx).toBeCloseTo(expected, 5)
166
168
  }
@@ -0,0 +1,79 @@
1
+ // @vitest-environment jsdom
2
+ //
3
+ // Layout and palette of the exported figure itself, as opposed to what any one
4
+ // layer draws inside it.
5
+ import { createJBrowseTheme } from '@jbrowse/core/ui/theme'
6
+ import { enableStaticRendering } from 'mobx-react'
7
+ import { beforeAll, expect, test } from 'vitest'
8
+
9
+ import { installHeadlessRenderEnv } from './headlessRenderEnv.ts'
10
+ import MSAModelF from './model.ts'
11
+ import { renderToSvg } from './renderToSvg.tsx'
12
+
13
+ beforeAll(() => {
14
+ enableStaticRendering(true)
15
+ installHeadlessRenderEnv()
16
+ })
17
+
18
+ function makeModel({ rows = 60, cols = 400 } = {}) {
19
+ const letters = 'ACDEFGHIKLMNPQRSTVWY'
20
+ const msa = Array.from({ length: rows }, (_, r) => {
21
+ const seq = Array.from(
22
+ { length: cols },
23
+ (_, c) => letters[(r * 7 + c * 3) % letters.length],
24
+ ).join('')
25
+ return `>seq${r}\n${seq}`
26
+ }).join('\n')
27
+ const model = MSAModelF().create({
28
+ type: 'MsaView',
29
+ msaFormat: 'fasta',
30
+ height: 400,
31
+ data: { msa },
32
+ })
33
+ model.setWidth(800)
34
+ return model
35
+ }
36
+
37
+ const rootRect = (svg: string) => /<rect[^>]*height="100%"[^>]*>/.exec(svg)?.[0]
38
+
39
+ test('the page takes the theme background, not a hardcoded white', async () => {
40
+ const model = makeModel()
41
+ const dark = createJBrowseTheme({ palette: { mode: 'dark' } })
42
+ expect(dark.palette.background.default).not.toBe('white')
43
+
44
+ const svg = await renderToSvg(model, { theme: dark, exportType: 'entire' })
45
+
46
+ // the layers below draw in theme colors: text.primary is near-white in a dark
47
+ // theme, so a white page would export white on white
48
+ expect(rootRect(svg)).toContain(`fill="${dark.palette.background.default}"`)
49
+ })
50
+
51
+ test('a viewport export is the alignment canvas, not the whole widget', async () => {
52
+ const model = makeModel()
53
+ expect(model.showHorizontalScrollbar).toBe(true)
54
+ expect(model.showVerticalScrollbar).toBe(true)
55
+
56
+ const svg = await renderToSvg(model, {
57
+ theme: createJBrowseTheme(),
58
+ exportType: 'viewport',
59
+ includeMinimap: true,
60
+ })
61
+
62
+ // the widget box also covers the resize handle and the scrollbars; exporting
63
+ // it drew the rows and columns those hide
64
+ const { treeAreaWidth, msaCanvasWidth, msaAreaHeight, minimapHeight } = model
65
+ expect(svg).toContain(`width="${treeAreaWidth + msaCanvasWidth}"`)
66
+ expect(svg).toContain(`height="${msaAreaHeight + minimapHeight}"`)
67
+ expect(msaAreaHeight + minimapHeight).toBe(model.height)
68
+ })
69
+
70
+ test('no attribute is serialized as the string "undefined"', async () => {
71
+ const model = makeModel({ rows: 4, cols: 20 })
72
+ const svg = await renderToSvg(model, {
73
+ theme: createJBrowseTheme(),
74
+ exportType: 'entire',
75
+ })
76
+
77
+ expect(svg).toContain('<text')
78
+ expect(svg).not.toContain('="undefined"')
79
+ })