react-msaview 6.4.2 → 7.0.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 (212) hide show
  1. package/README.md +6 -5
  2. package/bundle/index.js +100 -101
  3. package/bundle/index.js.map +4 -4
  4. package/dist/components/Loading.js +2 -2
  5. package/dist/components/Loading.js.map +1 -1
  6. package/dist/components/MSAViewer.d.ts +10 -2
  7. package/dist/components/MSAViewer.js +90 -2
  8. package/dist/components/MSAViewer.js.map +1 -1
  9. package/dist/components/ResizeHandles.d.ts +3 -3
  10. package/dist/components/ResizeHandles.js +28 -21
  11. package/dist/components/ResizeHandles.js.map +1 -1
  12. package/dist/components/dialogs/{InterProScanDialog.d.ts → AnnotationFileDialog.d.ts} +2 -2
  13. package/dist/components/dialogs/AnnotationFileDialog.js +92 -0
  14. package/dist/components/dialogs/AnnotationFileDialog.js.map +1 -0
  15. package/dist/components/dialogs/FeatureDialog.js +13 -9
  16. package/dist/components/dialogs/FeatureDialog.js.map +1 -1
  17. package/dist/components/header/Header.js +4 -0
  18. package/dist/components/header/Header.js.map +1 -1
  19. package/dist/components/header/HeaderStatusArea.js +0 -2
  20. package/dist/components/header/HeaderStatusArea.js.map +1 -1
  21. package/dist/components/header/LoadWarnings.d.ts +6 -0
  22. package/dist/components/header/LoadWarnings.js +36 -0
  23. package/dist/components/header/LoadWarnings.js.map +1 -0
  24. package/dist/components/header/MultiAlignmentSelector.js +0 -0
  25. package/dist/components/header/MultiAlignmentSelector.js.map +1 -1
  26. package/dist/components/header/TreeSettingsMenu.js +9 -1
  27. package/dist/components/header/TreeSettingsMenu.js.map +1 -1
  28. package/dist/components/header/UnshareableDataWarning.d.ts +6 -0
  29. package/dist/components/header/UnshareableDataWarning.js +42 -0
  30. package/dist/components/header/UnshareableDataWarning.js.map +1 -0
  31. package/dist/components/header/getAnnotationsMenu.js +14 -14
  32. package/dist/components/header/getAnnotationsMenu.js.map +1 -1
  33. package/dist/components/import/ImportForm.js +1 -1
  34. package/dist/components/import/ImportForm.js.map +1 -1
  35. package/dist/components/import/ImportFormExamples.js +2 -6
  36. package/dist/components/import/ImportFormExamples.js.map +1 -1
  37. package/dist/components/import/util.js +3 -0
  38. package/dist/components/import/util.js.map +1 -1
  39. package/dist/components/msa/MSACanvasBlock.js +4 -5
  40. package/dist/components/msa/MSACanvasBlock.js.map +1 -1
  41. package/dist/components/msa/msaRaster.d.ts +13 -3
  42. package/dist/components/msa/msaRaster.js +173 -61
  43. package/dist/components/msa/msaRaster.js.map +1 -1
  44. package/dist/components/msa/renderHighlights.d.ts +39 -1
  45. package/dist/components/msa/renderHighlights.js +71 -6
  46. package/dist/components/msa/renderHighlights.js.map +1 -1
  47. package/dist/components/msa/renderMSABlock.d.ts +1 -2
  48. package/dist/components/msa/renderMSABlock.js +24 -28
  49. package/dist/components/msa/renderMSABlock.js.map +1 -1
  50. package/dist/components/msa/renderMSAMouseover.js +9 -30
  51. package/dist/components/msa/renderMSAMouseover.js.map +1 -1
  52. package/dist/components/renderCtx.d.ts +1 -1
  53. package/dist/components/tracks/TrackBlocks.js +10 -11
  54. package/dist/components/tracks/TrackBlocks.js.map +1 -1
  55. package/dist/components/tracks/drawTracks.d.ts +26 -8
  56. package/dist/components/tracks/drawTracks.js +123 -13
  57. package/dist/components/tracks/drawTracks.js.map +1 -1
  58. package/dist/components/tree/TreeCanvas.js +2 -3
  59. package/dist/components/tree/TreeCanvas.js.map +1 -1
  60. package/dist/components/tree/TreeCanvasBlock.js +4 -2
  61. package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
  62. package/dist/components/tree/TreeRuler.js +13 -3
  63. package/dist/components/tree/TreeRuler.js.map +1 -1
  64. package/dist/components/tree/renderTreeCanvas.d.ts +0 -1
  65. package/dist/components/tree/renderTreeCanvas.js +70 -30
  66. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  67. package/dist/components/tree/scaleBar.d.ts +10 -0
  68. package/dist/components/tree/scaleBar.js +24 -0
  69. package/dist/components/tree/scaleBar.js.map +1 -0
  70. package/dist/constants.d.ts +3 -1
  71. package/dist/constants.js +18 -1
  72. package/dist/constants.js.map +1 -1
  73. package/dist/fetchUtils.d.ts +7 -7
  74. package/dist/fetchUtils.js +4 -32
  75. package/dist/fetchUtils.js.map +1 -1
  76. package/dist/hierarchy.d.ts +2 -1
  77. package/dist/hierarchy.js +16 -10
  78. package/dist/hierarchy.js.map +1 -1
  79. package/dist/index.d.ts +2 -1
  80. package/dist/index.js +8 -2
  81. package/dist/index.js.map +1 -1
  82. package/dist/model/DataModel.js +5 -2
  83. package/dist/model/DataModel.js.map +1 -1
  84. package/dist/model.d.ts +454 -177
  85. package/dist/model.js +1180 -511
  86. package/dist/model.js.map +1 -1
  87. package/dist/renderToStaticMarkup.d.ts +11 -0
  88. package/dist/renderToStaticMarkup.js +30 -7
  89. package/dist/renderToStaticMarkup.js.map +1 -1
  90. package/dist/renderToSvg.js +25 -19
  91. package/dist/renderToSvg.js.map +1 -1
  92. package/dist/rowCoordinateCalculations.d.ts +25 -25
  93. package/dist/rowCoordinateCalculations.js +12 -36
  94. package/dist/rowCoordinateCalculations.js.map +1 -1
  95. package/dist/seqPosToGlobalCol.js +3 -1
  96. package/dist/seqPosToGlobalCol.js.map +1 -1
  97. package/dist/svgTestUtil.d.ts +144 -82
  98. package/dist/types.d.ts +97 -3
  99. package/dist/util.d.ts +11 -4
  100. package/dist/util.js +30 -10
  101. package/dist/util.js.map +1 -1
  102. package/dist/version.d.ts +1 -1
  103. package/dist/version.js +1 -1
  104. package/dist/wuss.d.ts +14 -0
  105. package/dist/wuss.js +55 -0
  106. package/dist/wuss.js.map +1 -0
  107. package/package.json +10 -8
  108. package/src/components/Loading.tsx +12 -2
  109. package/src/components/MSAViewer.tsx +106 -2
  110. package/src/components/ResizeHandles.tsx +31 -31
  111. package/src/components/dialogs/AnnotationFileDialog.tsx +162 -0
  112. package/src/components/dialogs/FeatureDialog.tsx +13 -9
  113. package/src/components/header/Header.tsx +4 -0
  114. package/src/components/header/HeaderStatusArea.tsx +0 -5
  115. package/src/components/header/LoadWarnings.tsx +49 -0
  116. package/src/components/header/MultiAlignmentSelector.tsx +0 -0
  117. package/src/components/header/TreeSettingsMenu.tsx +9 -1
  118. package/src/components/header/UnshareableDataWarning.tsx +58 -0
  119. package/src/components/header/getAnnotationsMenu.ts +16 -17
  120. package/src/components/import/ImportForm.tsx +2 -2
  121. package/src/components/import/ImportFormExamples.tsx +2 -6
  122. package/src/components/import/util.ts +3 -0
  123. package/src/components/msa/MSACanvasBlock.tsx +4 -5
  124. package/src/components/msa/msaRaster.ts +216 -59
  125. package/src/components/msa/renderHighlights.ts +103 -5
  126. package/src/components/msa/renderMSABlock.ts +25 -36
  127. package/src/components/msa/renderMSAMouseover.ts +9 -39
  128. package/src/components/renderCtx.ts +1 -0
  129. package/src/components/tracks/TrackBlocks.tsx +10 -13
  130. package/src/components/tracks/drawTracks.ts +165 -17
  131. package/src/components/tree/TreeCanvas.tsx +2 -3
  132. package/src/components/tree/TreeCanvasBlock.tsx +4 -2
  133. package/src/components/tree/TreeRuler.tsx +38 -3
  134. package/src/components/tree/renderTreeCanvas.ts +79 -35
  135. package/src/components/tree/scaleBar.ts +23 -0
  136. package/src/constants.ts +20 -1
  137. package/src/fetchUtils.ts +7 -40
  138. package/src/hierarchy.ts +20 -11
  139. package/src/index.ts +18 -2
  140. package/src/model/DataModel.ts +6 -2
  141. package/src/model.ts +1352 -616
  142. package/src/rawImports.d.ts +9 -0
  143. package/src/renderToStaticMarkup.ts +34 -7
  144. package/src/renderToSvg.tsx +33 -27
  145. package/src/rowCoordinateCalculations.ts +15 -59
  146. package/src/seqPosToGlobalCol.ts +3 -1
  147. package/src/types.ts +131 -4
  148. package/src/util.ts +31 -14
  149. package/src/version.ts +1 -1
  150. package/src/wuss.ts +62 -0
  151. package/dist/components/dialogs/InterProScanDialog.js +0 -137
  152. package/dist/components/dialogs/InterProScanDialog.js.map +0 -1
  153. package/dist/components/dialogs/InterProScanFileDialog.d.ts +0 -7
  154. package/dist/components/dialogs/InterProScanFileDialog.js +0 -64
  155. package/dist/components/dialogs/InterProScanFileDialog.js.map +0 -1
  156. package/dist/launchInterProScan.d.ts +0 -13
  157. package/dist/launchInterProScan.js +0 -110
  158. package/dist/launchInterProScan.js.map +0 -1
  159. package/dist/useColorContrast.d.ts +0 -10
  160. package/dist/useColorContrast.js +0 -13
  161. package/dist/useColorContrast.js.map +0 -1
  162. package/src/clustalX.test.ts +0 -74
  163. package/src/colSpace.test.ts +0 -67
  164. package/src/collapseLogic.test.ts +0 -115
  165. package/src/colorSchemes.docs.test.ts +0 -57
  166. package/src/columnCounts.test.ts +0 -73
  167. package/src/columnStats.test.ts +0 -94
  168. package/src/columnTracks.test.ts +0 -97
  169. package/src/components/dialogs/InterProScanDialog.tsx +0 -214
  170. package/src/components/dialogs/InterProScanFileDialog.tsx +0 -138
  171. package/src/components/minimap/minimapLayout.test.ts +0 -71
  172. package/src/components/msa/msaRaster.test.ts +0 -145
  173. package/src/components/msa/renderBoxFeatureCanvasBlock.test.ts +0 -51
  174. package/src/components/tree/renderTreeCanvas.test.ts +0 -296
  175. package/src/dataInitialized.test.ts +0 -42
  176. package/src/domainBands.test.ts +0 -122
  177. package/src/domainLetterColor.test.ts +0 -80
  178. package/src/exportFileName.test.ts +0 -40
  179. package/src/fetchUtils.test.ts +0 -83
  180. package/src/flatToTree.test.ts +0 -51
  181. package/src/hierarchy.test.ts +0 -228
  182. package/src/highlights.test.ts +0 -70
  183. package/src/impgRender.test.tsx +0 -68
  184. package/src/largeTree.test.ts +0 -67
  185. package/src/launchInterProScan.ts +0 -167
  186. package/src/loadErrorDisplay.test.tsx +0 -102
  187. package/src/modelFilehandleLoaders.test.ts +0 -137
  188. package/src/modelReset.test.ts +0 -120
  189. package/src/neighborJoining.test.ts +0 -164
  190. package/src/parseAsn1.test.ts +0 -36
  191. package/src/propertyConservation.test.ts +0 -36
  192. package/src/renderColumnTracksSvg.test.tsx +0 -108
  193. package/src/renderDomainsSvg.test.tsx +0 -118
  194. package/src/renderHighlightsSvg.test.tsx +0 -85
  195. package/src/renderMinimapSvg.test.tsx +0 -62
  196. package/src/renderRasterSvg.test.tsx +0 -144
  197. package/src/renderSequenceLogoSvg.test.tsx +0 -165
  198. package/src/renderToSvg.test.tsx +0 -65
  199. package/src/reset.test.ts +0 -61
  200. package/src/rowCoordinateCalculations.test.ts +0 -211
  201. package/src/rowSpace.test.ts +0 -46
  202. package/src/segmentOverlay.test.ts +0 -70
  203. package/src/seqPosToGlobalCol.test.ts +0 -59
  204. package/src/sequenceLogo.test.ts +0 -88
  205. package/src/setFontSize.test.ts +0 -33
  206. package/src/stripDefault.test.ts +0 -48
  207. package/src/stripDefaultSnapshot.test.ts +0 -87
  208. package/src/treeMetadata.test.ts +0 -39
  209. package/src/useCanvasAutorun.test.tsx +0 -98
  210. package/src/useColorContrast.ts +0 -18
  211. package/src/viewChrome.test.tsx +0 -134
  212. package/src/wheelZoom.test.ts +0 -46
@@ -1,68 +0,0 @@
1
- // @vitest-environment jsdom
2
- //
3
- // End-to-end check of the impg (https://github.com/pangenome/impg) integration:
4
- // impg's scripts/faln2html.py drives the viewer exactly as below --
5
- // MSAModelF().create({ type:'MsaView', data:{ msa } })
6
- // model.setColorSchemeName('percent_identity_dynamic')
7
- // model.setBgColor(true); model.setWidth(1300)
8
- // then renders <MSAView model/>. This exercises that path headlessly via the
9
- // public renderToSvg export. Polyfills mirror scripts/generateFigures.tsx.
10
- import { createJBrowseTheme } from '@jbrowse/core/ui/theme'
11
- import { beforeAll, expect, test } from 'vitest'
12
-
13
- import MSAModelF from './model.ts'
14
- import { renderToSvg } from './renderToSvg.tsx'
15
- import { installSvgTestEnv } from './svgTestUtil.ts'
16
-
17
- beforeAll(() => {
18
- installSvgTestEnv()
19
- })
20
-
21
- // representative `impg query -o fasta-aln` block: PanSN names with region
22
- // colons, dash gaps, mixed-case (soft-masked) bases
23
- const impgFastaAln = `>HG002#1#chr1:1000-1040
24
- ACGTACGT--ACGTNNNNacgtACGTACGTACGTACGT
25
- >HG003#1#chr1:1000-1040
26
- ACGTAC-TGGACGTNNNNACGTAC--ACGTacgtACGT
27
- >CHM13#0#chr1:1000-1040
28
- ACGTACGTGGACGTNNNNACGT----ACGTACGTACGT`
29
-
30
- test('renders an impg fasta-aln block with declarative impg config', async () => {
31
- // everything except width is a declarative MST property, so it can be set in
32
- // the create() snapshot -- no setColorSchemeName/setBgColor/setMSAFormat
33
- // action calls needed. msaFormat:'fasta' forces the parser and skips the
34
- // heuristic fasta-vs-a3m sniffing. width is volatile (tracks container size)
35
- // so it remains an action.
36
- const model = MSAModelF().create({
37
- id: 'impg-test',
38
- type: 'MsaView',
39
- height: 400,
40
- colorSchemeName: 'percent_identity_dynamic',
41
- bgColor: true,
42
- msaFormat: 'fasta',
43
- data: { msa: impgFastaAln },
44
- })
45
- model.setWidth(1300)
46
-
47
- // the declarative config took effect
48
- expect(model.colorSchemeName).toBe('percent_identity_dynamic')
49
- expect(model.bgColor).toBe(true)
50
- expect(model.msaFormat).toBe('fasta')
51
-
52
- // the alignment parsed and all 3 rows are present at equal width
53
- expect(model.rowNames).toEqual([
54
- 'HG002#1#chr1:1000-1040',
55
- 'HG003#1#chr1:1000-1040',
56
- 'CHM13#0#chr1:1000-1040',
57
- ])
58
- expect(model.numColumns).toBe(38)
59
-
60
- const svg = await renderToSvg(model, {
61
- theme: createJBrowseTheme(),
62
- exportType: 'entire',
63
- includeMinimap: false,
64
- includeTracks: false,
65
- })
66
- expect(svg).toContain('<svg')
67
- expect(svg).toContain('HG002#1#chr1:1000-1040')
68
- })
@@ -1,67 +0,0 @@
1
- // @vitest-environment jsdom
2
- import { expect, test } from 'vitest'
3
-
4
- import MSAModelF from './model.ts'
5
-
6
- // jsdom has no 2d context, so measureTextCanvas falls back to an estimate. Only
7
- // the width matters here, so a stub keeps the test about tree size.
8
- HTMLCanvasElement.prototype.getContext = (() => ({
9
- font: '12px sans-serif',
10
- measureText: (t: string) => ({ width: t.length * 7 }),
11
- })) as unknown as typeof HTMLCanvasElement.prototype.getContext
12
-
13
- // The import form ships a "230k COVID-19 samples (tree only)" example, so a tree
14
- // this size is a supported input rather than a hypothetical. Every leaf gets a
15
- // measured label, and anything that then reduces over them per row has to do it
16
- // without passing one argument per row.
17
- function flatNewick(n: number) {
18
- return `(${Array.from({ length: n }, (_, i) => `s${i}:0.1`).join(',')});`
19
- }
20
-
21
- test('a tree far past the argument limit still lays out', () => {
22
- const model = MSAModelF().create({
23
- type: 'MsaView',
24
- data: { tree: flatNewick(200_000) },
25
- })
26
- model.setWidth(1000)
27
-
28
- expect(model.numRows).toBe(200_000)
29
- // 's199999' is the longest name, at 7 chars * 7px, scaled from the reference
30
- // font size the labels are measured at down to the current one (13/16)
31
- expect(model.labelsWidth).toBeCloseTo((49 * 13) / 16)
32
- expect(model.treeWidth).toBeGreaterThan(0)
33
- })
34
-
35
- test('re-hovering the same tree node does not re-walk the tree', () => {
36
- const model = MSAModelF().create({
37
- type: 'MsaView',
38
- data: { tree: flatNewick(200_000) },
39
- })
40
- model.setWidth(1000)
41
-
42
- const leafId = model.leaves[1000]!.data.id
43
- model.setHoveredTreeNode(leafId)
44
- const first = model.hoveredTreeNode
45
-
46
- // the tree's mousemove handler fires this on every event, so an unchanged id
47
- // has to leave the state object alone: rebuilding it walks all 200k nodes and
48
- // invalidates every overlay that reads hoveredRowIndices
49
- model.setHoveredTreeNode(leafId)
50
- expect(model.hoveredTreeNode).toBe(first)
51
-
52
- model.setHoveredTreeNode(undefined)
53
- expect(model.hoveredTreeNode).toBeUndefined()
54
- })
55
-
56
- test('a vertical zoom does not re-measure every label', () => {
57
- const model = MSAModelF().create({
58
- type: 'MsaView',
59
- data: { tree: flatNewick(200_000) },
60
- })
61
- model.setWidth(1000)
62
-
63
- const before = model.labelWidthMap
64
- model.setRowHeight(24)
65
- expect(model.labelWidthMap).toBe(before)
66
- expect(model.labelsWidth).toBeCloseTo((49 * 18) / 16)
67
- })
@@ -1,167 +0,0 @@
1
- import { getSession } from '@jbrowse/core/util'
2
- import { interProScanResponseToAnnotations } from 'msa-parsers'
3
-
4
- import { isAbortError, jsonfetch, textfetch, timeout } from './fetchUtils.ts'
5
-
6
- import type { MsaViewModel } from './model.ts'
7
- import type { InterProScanResponse } from 'msa-parsers'
8
-
9
- const base = 'https://www.ebi.ac.uk/Tools/services/rest'
10
-
11
- async function runInterProScan({
12
- seq,
13
- onProgress,
14
- onJobId,
15
- programs,
16
- model,
17
- signal,
18
- }: {
19
- seq: string
20
- programs: string[]
21
- onProgress: (arg?: { msg: string; url?: string }) => void
22
- onJobId?: (arg: string) => void
23
- model: MsaViewModel
24
- signal?: AbortSignal
25
- }) {
26
- const jobId = await textfetch(`${base}/iprscan5/run`, {
27
- method: 'POST',
28
- body: new URLSearchParams({
29
- email: 'colin.diesh@gmail.com',
30
- sequence: seq,
31
- programs: programs.join(','),
32
- }),
33
- signal,
34
- })
35
- onJobId?.(jobId)
36
- await wait({
37
- jobId,
38
- onProgress,
39
- signal,
40
- })
41
- await loadInterProScanResultsWithStatus({ jobId, model, onProgress, signal })
42
- }
43
-
44
- function loadInterProScanResults(jobId: string, signal?: AbortSignal) {
45
- return jsonfetch<InterProScanResponse>(
46
- `${base}/iprscan5/result/${jobId}/json`,
47
- { signal },
48
- )
49
- }
50
-
51
- async function wait({
52
- onProgress,
53
- jobId,
54
- signal,
55
- }: {
56
- jobId: string
57
- onProgress: (arg?: { msg: string; url?: string }) => void
58
- signal?: AbortSignal
59
- }) {
60
- const url = `${base}/iprscan5/status/${jobId}`
61
- // EBI jobs run for minutes; cap polling so an unexpected/stuck status can't
62
- // spin forever (the abort signal is the normal early exit). ~10s per check.
63
- const maxChecks = 360
64
- try {
65
- let finished = false
66
- for (let check = 0; check < maxChecks && !finished; check++) {
67
- for (let i = 0; i < 10; i++) {
68
- await timeout(1000, signal)
69
- onProgress({ msg: `Checking status ${10 - i}`, url })
70
- }
71
- // status endpoint returns a bare status token; match exactly rather than
72
- // substring so an error page mentioning "FINISHED" can't be a false hit
73
- const status = (await textfetch(url, { signal })).trim()
74
- if (status === 'FINISHED') {
75
- finished = true
76
- } else if (
77
- status === 'FAILURE' ||
78
- status === 'ERROR' ||
79
- status === 'NOT_FOUND'
80
- ) {
81
- throw new Error(
82
- `InterProScan job ${jobId} ended with status: ${status}`,
83
- )
84
- }
85
- }
86
- if (!finished) {
87
- throw new Error(
88
- `InterProScan job ${jobId} did not finish after ${maxChecks} status checks`,
89
- )
90
- }
91
- } finally {
92
- onProgress()
93
- }
94
- }
95
-
96
- export async function launchInterProScan({
97
- algorithm,
98
- seq,
99
- programs,
100
- onJobId,
101
- onProgress,
102
- model,
103
- signal,
104
- }: {
105
- algorithm: string
106
- seq: string
107
- programs: string[]
108
- onProgress: (arg?: { msg: string; url?: string }) => void
109
- onJobId?: (arg: string) => void
110
- model: MsaViewModel
111
- signal?: AbortSignal
112
- }) {
113
- try {
114
- onProgress({ msg: `Launching ${algorithm} MSA` })
115
- if (algorithm === 'interproscan') {
116
- await runInterProScan({
117
- seq,
118
- onJobId,
119
- onProgress,
120
- programs,
121
- model,
122
- signal,
123
- })
124
- } else {
125
- throw new Error('unknown algorithm')
126
- }
127
- } catch (e) {
128
- if (isAbortError(e)) {
129
- getSession(model).notify('Cancelled InterProScan query', 'info')
130
- } else {
131
- throw e
132
- }
133
- } finally {
134
- onProgress()
135
- }
136
- }
137
-
138
- async function loadInterProScanResultsWithStatus({
139
- jobId,
140
- model,
141
- onProgress,
142
- signal,
143
- }: {
144
- jobId: string
145
- model: MsaViewModel
146
- onProgress: (arg?: { msg: string; url?: string }) => void
147
- signal?: AbortSignal
148
- }) {
149
- try {
150
- onProgress({
151
- msg: `Downloading results of ${jobId} (for larger sequences this can be slow, click status to download and upload in the manual tab)`,
152
- url: `https://www.ebi.ac.uk/Tools/services/rest/iprscan5/result/${jobId}/json`,
153
- })
154
- const ret = await loadInterProScanResults(jobId, signal)
155
- model.setAnnotations(interProScanResponseToAnnotations(ret))
156
- getSession(model).notify(`Loaded interproscan ${jobId} results`, 'success')
157
- } catch (e) {
158
- if (isAbortError(e)) {
159
- getSession(model).notify('Cancelled InterProScan query', 'info')
160
- } else {
161
- console.error(e)
162
- getSession(model).notifyError(`${e}`, e)
163
- }
164
- } finally {
165
- onProgress()
166
- }
167
- }
@@ -1,102 +0,0 @@
1
- // @vitest-environment jsdom
2
- //
3
- // What a failed load LOOKS like, which is a different question from what the
4
- // model recorded. modelFilehandleLoaders.test.ts already pins the model half --
5
- // a rejected fetch sets `error` and clears `loadingMSA`, under the name "a
6
- // failed fetch surfaces as an error" -- and it passed throughout the whole time
7
- // a failed url showed the reader a spinner and nothing else.
8
- //
9
- // The error display was never missing. ImportForm renders `model.error`, and
10
- // the reader could not get to it: a non-abort failure KEEPS its filehandle
11
- // (only an abort clears it), so `hasPendingFilehandle` stayed true and
12
- // Loading.tsx took its spinner branch ahead of the import form, permanently.
13
- // Found on AlphaFold's `files/msa/` prefix answering 403 to every key in August
14
- // 2026 -- the view opened, said "Downloading file" with a Cancel button, and
15
- // was still saying it at 105s.
16
- import React, { act } from 'react'
17
-
18
- import { createRoot } from 'react-dom/client'
19
- import { afterEach, beforeEach, expect, test } from 'vitest'
20
-
21
- import MSAView from './components/Loading.tsx'
22
- import MSAModelF from './model.ts'
23
-
24
- import type { MsaViewModel } from './model.ts'
25
- import type { Root } from 'react-dom/client'
26
-
27
- Reflect.set(globalThis, 'IS_REACT_ACT_ENVIRONMENT', true)
28
-
29
- let container: HTMLDivElement
30
- let root: Root
31
- let model: MsaViewModel
32
-
33
- function uri(u: string) {
34
- return { locationType: 'UriLocation' as const, uri: u }
35
- }
36
-
37
- function render() {
38
- act(() => {
39
- root.render(<MSAView model={model} />)
40
- })
41
- }
42
-
43
- beforeEach(() => {
44
- container = document.createElement('div')
45
- document.body.append(container)
46
- root = createRoot(container)
47
- model = MSAModelF().create({ type: 'MsaView' })
48
- model.setWidth(800)
49
- })
50
-
51
- afterEach(() => {
52
- act(() => {
53
- root.unmount()
54
- })
55
- container.remove()
56
- })
57
-
58
- test('a load that fails shows the error rather than a spinner forever', () => {
59
- // the state the loader leaves behind on a 403: the filehandle it could not
60
- // read is still set, nothing parsed, and the error recorded
61
- model.setMSAFilehandle(uri('https://example.com/blocked.a3m'))
62
- model.setLoadingMSA(false)
63
- model.setError(new Error('HTTP 403 fetching blocked.a3m'))
64
- render()
65
-
66
- expect(container.textContent).toContain('403')
67
- expect(container.textContent).not.toContain('Loading')
68
- })
69
-
70
- test('a load still in flight is a spinner, not an error', () => {
71
- model.setMSAFilehandle(uri('https://example.com/slow.a3m'))
72
- render()
73
-
74
- expect(model.loadingMSA).toBe(true)
75
- expect(container.textContent).not.toContain('Return to import form')
76
- })
77
-
78
- // A guard rather than a pin: this one passes without the fix too, because with
79
- // no filehandle left set the view reaches the ImportForm and that already shows
80
- // the error. It is here so the new branch cannot quietly swallow the case.
81
- //
82
- // It also records the surprise underneath: `dataInitialized` is itself
83
- // `(msa || tree) && !error`, so ANY error hides a loaded alignment, including a
84
- // tree or gff that 404s beside a good one -- setError is shared by every
85
- // filehandle loader. Whether a failed OPTIONAL file should blank a good
86
- // alignment is a real question and a wider one, since `dataInitialized` is read
87
- // in several places; narrowing it is a design change rather than this fix.
88
- test('an error over loaded data says so instead of a bare import form', () => {
89
- const loaded = MSAModelF().create({
90
- type: 'MsaView',
91
- msaFormat: 'fasta',
92
- height: 200,
93
- data: { msa: '>seq1\nACDE\n>seq2\nACDF' },
94
- })
95
- loaded.setWidth(800)
96
- loaded.setError(new Error('domains.gff 404'))
97
- act(() => {
98
- root.render(<MSAView model={loaded} />)
99
- })
100
-
101
- expect(container.textContent).toContain('domains.gff 404')
102
- })
@@ -1,137 +0,0 @@
1
- // The reactive filehandle loaders in afterCreate: each fetches when its
2
- // filehandle changes, and each guards against a stale response, since a user can
3
- // pick a second file while the first is still downloading.
4
- import { beforeEach, expect, test, vi } from 'vitest'
5
-
6
- import MSAModelF from './model.ts'
7
-
8
- import type * as FetchUtils from './fetchUtils.ts'
9
-
10
- // uri -> settle functions for the fetch that autorun kicked off
11
- const inFlight = new Map<
12
- string,
13
- { resolve: (text: string) => void; reject: (e: unknown) => void }
14
- >()
15
-
16
- vi.mock('@jbrowse/core/util/io', () => ({
17
- openLocation: (loc: { uri: string }) => loc,
18
- }))
19
-
20
- vi.mock('./fetchUtils.ts', async importOriginal => ({
21
- ...(await importOriginal<typeof FetchUtils>()),
22
- fetchTextWithProgress: (loc: { uri: string }) =>
23
- new Promise<string>((resolve, reject) => {
24
- inFlight.set(loc.uri, { resolve, reject })
25
- }),
26
- }))
27
-
28
- function uri(u: string) {
29
- return { locationType: 'UriLocation' as const, uri: u }
30
- }
31
-
32
- function fasta(seq: string) {
33
- return `>seq1\n${seq}`
34
- }
35
-
36
- // let the awaited fetch and the actions that follow it run
37
- function flush() {
38
- return new Promise(resolve => setTimeout(resolve, 0))
39
- }
40
-
41
- function makeModel() {
42
- const model = MSAModelF().create({ type: 'MsaView' })
43
- model.setWidth(800)
44
- return model
45
- }
46
-
47
- beforeEach(() => {
48
- inFlight.clear()
49
- })
50
-
51
- test('an msa filehandle loads into the model and clears its loading flag', async () => {
52
- const model = makeModel()
53
- model.setMSAFilehandle(uri('aln.fa'))
54
- expect(model.loadingMSA).toBe(true)
55
-
56
- inFlight.get('aln.fa')!.resolve(fasta('ACDE'))
57
- await flush()
58
-
59
- expect(model.data.msa).toBe(fasta('ACDE'))
60
- expect(model.loadingMSA).toBe(false)
61
- })
62
-
63
- test('a slower earlier fetch never clobbers a newer one', async () => {
64
- const model = makeModel()
65
- model.setMSAFilehandle(uri('first.fa'))
66
- model.setMSAFilehandle(uri('second.fa'))
67
-
68
- // the newer fetch finishes first, then the abandoned one comes back
69
- inFlight.get('second.fa')!.resolve(fasta('SECOND'))
70
- await flush()
71
- inFlight.get('first.fa')!.resolve(fasta('FIRST'))
72
- await flush()
73
-
74
- expect(model.data.msa).toBe(fasta('SECOND'))
75
- expect(model.loadingMSA).toBe(false)
76
- })
77
-
78
- test('a stale failure does not raise an error over live data', async () => {
79
- const model = makeModel()
80
- model.setMSAFilehandle(uri('first.fa'))
81
- model.setMSAFilehandle(uri('second.fa'))
82
-
83
- inFlight.get('second.fa')!.resolve(fasta('SECOND'))
84
- await flush()
85
- inFlight.get('first.fa')!.reject(new Error('too slow'))
86
- await flush()
87
-
88
- expect(model.error).toBeUndefined()
89
- expect(model.data.msa).toBe(fasta('SECOND'))
90
- })
91
-
92
- test('cancelling drops the filehandle so the view returns to the import form', async () => {
93
- const model = makeModel()
94
- model.setMSAFilehandle(uri('aln.fa'))
95
-
96
- inFlight.get('aln.fa')!.reject(new DOMException('Aborted', 'AbortError'))
97
- await flush()
98
-
99
- expect(model.msaFilehandle).toBeUndefined()
100
- expect(model.error).toBeUndefined()
101
- expect(model.loadingMSA).toBe(false)
102
- })
103
-
104
- test('a failed fetch surfaces as an error', async () => {
105
- const model = makeModel()
106
- model.setMSAFilehandle(uri('aln.fa'))
107
-
108
- inFlight.get('aln.fa')!.reject(new Error('404'))
109
- await flush()
110
-
111
- expect(model.error).toBeInstanceOf(Error)
112
- expect(model.loadingMSA).toBe(false)
113
- })
114
-
115
- test('tree, treeMetadata and gff filehandles each load into their own field', async () => {
116
- const model = MSAModelF().create({
117
- type: 'MsaView',
118
- treeFilehandle: uri('tree.nh'),
119
- treeMetadataFilehandle: uri('meta.json'),
120
- gffFilehandle: uri('domains.gff'),
121
- })
122
- model.setWidth(800)
123
-
124
- inFlight.get('tree.nh')!.resolve('(a,b);')
125
- inFlight.get('meta.json')!.resolve('{"a":{"genome":"human"}}')
126
- inFlight
127
- .get('domains.gff')!
128
- .resolve(
129
- '##gff-version 3\na\tPfam\tprotein_match\t1\t2\t.\t.\t.\tName=PF00069',
130
- )
131
- await flush()
132
-
133
- expect(model.data.tree).toBe('(a,b);')
134
- expect(model.treeMetadata.a?.genome).toBe('human')
135
- expect(model.actuallyShowDomains).toBe(true)
136
- expect(model.loadingTree).toBe(false)
137
- })
@@ -1,120 +0,0 @@
1
- // reset() returns the view to the import form, and the next file loads into
2
- // the same model instance. It applies a default snapshot filtered to
3
- // preservedOnReset, so anything file-derived that survived would be a hole in
4
- // that list — the last test enumerates the snapshot keys to keep the list and
5
- // the model's properties from drifting apart as properties are added. The
6
- // stakes: node ids are path-derived (node-0-0-1), so a stale collapsed or
7
- // showOnly id names a real node in the next tree and folds it.
8
- import { getSnapshot } from '@jbrowse/mobx-state-tree'
9
- import { describe, expect, test } from 'vitest'
10
-
11
- import MSAModelF, { preservedOnReset } from './model.ts'
12
-
13
- const firstFile = {
14
- msa: '>a\nACGT\n>b\nACGT\n>c\nACGT\n>d\nACGT',
15
- tree: '((a,b),(c,d));',
16
- }
17
- const secondFile = {
18
- msa: '>e\nTTTT\n>f\nTTTT\n>g\nTTTT\n>h\nTTTT',
19
- tree: '((e,f),(g,h));',
20
- }
21
-
22
- function makeModel() {
23
- const model = MSAModelF().create({ type: 'MsaView', data: firstFile })
24
- model.setWidth(800)
25
- return model
26
- }
27
-
28
- function internalNodeId(model: ReturnType<typeof makeModel>) {
29
- return model.leaves[0]!.parent!.data.id
30
- }
31
-
32
- describe('reset', () => {
33
- test('a collapsed clade does not fold the next loaded tree', () => {
34
- const model = makeModel()
35
- model.toggleCollapsed(internalNodeId(model))
36
- expect(model.leaves.length).toBe(3)
37
-
38
- model.reset()
39
- model.setData(secondFile)
40
- expect(model.collapsed.length).toBe(0)
41
- expect(model.leaves.map(l => l.data.name).toSorted()).toEqual([
42
- 'e',
43
- 'f',
44
- 'g',
45
- 'h',
46
- ])
47
- })
48
-
49
- test('showOnly does not restrict the next loaded tree', () => {
50
- const model = makeModel()
51
- model.setShowOnly(internalNodeId(model))
52
- expect(model.leaves.length).toBe(2)
53
-
54
- model.reset()
55
- model.setData(secondFile)
56
- expect(model.showOnly).toBe(undefined)
57
- expect(model.leaves.length).toBe(4)
58
- })
59
-
60
- test('clears file-derived volatiles', () => {
61
- const model = makeModel()
62
- model.setMousePos(2, 1)
63
- model.setMouseClickPos(3, 0)
64
- model.setHighlightedColumns([1, 2])
65
- model.setHoveredTreeNode(internalNodeId(model))
66
-
67
- model.reset()
68
- expect(model.mouseCol).toBe(undefined)
69
- expect(model.mouseClickCol).toBe(undefined)
70
- expect(model.highlightedColumns).toBe(undefined)
71
- expect(model.hoveredTreeNode).toBe(undefined)
72
- expect(model.error).toBe(undefined)
73
- expect(model.annotations).toEqual([])
74
- })
75
-
76
- test('display preferences survive', () => {
77
- const model = makeModel()
78
- model.setColorSchemeName('clustalx')
79
- model.setDrawNodeBubbles(false)
80
- model.toggleTrack('conservation')
81
-
82
- model.reset()
83
- expect(model.colorSchemeName).toBe('clustalx')
84
- expect(model.drawNodeBubbles).toBe(false)
85
- expect(model.turnedOffTracks.get('conservation')).toBe(true)
86
- })
87
-
88
- test('every property off preservedOnReset matches a fresh model', () => {
89
- const model = makeModel()
90
- model.toggleCollapsed(internalNodeId(model))
91
- model.setShowOnly(internalNodeId(model))
92
- model.drawRelativeTo('a')
93
- model.setFilter('PF00001', true)
94
- model.setScrollX(-100)
95
- model.setScrollY(-50)
96
- model.zoomIn()
97
- model.setCurrentAlignment(1)
98
- model.setMSAFilehandle({
99
- uri: 'http://x/msa.fa',
100
- locationType: 'UriLocation',
101
- })
102
- model.setTreeFilehandle({
103
- uri: 'http://x/t.nh',
104
- locationType: 'UriLocation',
105
- })
106
- model.setTreeMetadataFilehandle({
107
- uri: 'http://x/m.json',
108
- locationType: 'UriLocation',
109
- })
110
- model.setMSAFormat('fasta')
111
-
112
- model.reset()
113
- const fresh = MSAModelF().create({ type: 'MsaView' })
114
- const strip = (snap: object) =>
115
- Object.fromEntries(
116
- Object.entries(snap).filter(([key]) => !preservedOnReset.has(key)),
117
- )
118
- expect(strip(getSnapshot(model))).toEqual(strip(getSnapshot(fresh)))
119
- })
120
- })