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.
- package/README.md +6 -5
- package/bundle/index.js +100 -101
- package/bundle/index.js.map +4 -4
- package/dist/components/Loading.js +2 -2
- package/dist/components/Loading.js.map +1 -1
- package/dist/components/MSAViewer.d.ts +10 -2
- package/dist/components/MSAViewer.js +90 -2
- package/dist/components/MSAViewer.js.map +1 -1
- package/dist/components/ResizeHandles.d.ts +3 -3
- package/dist/components/ResizeHandles.js +28 -21
- package/dist/components/ResizeHandles.js.map +1 -1
- package/dist/components/dialogs/{InterProScanDialog.d.ts → AnnotationFileDialog.d.ts} +2 -2
- package/dist/components/dialogs/AnnotationFileDialog.js +92 -0
- package/dist/components/dialogs/AnnotationFileDialog.js.map +1 -0
- package/dist/components/dialogs/FeatureDialog.js +13 -9
- package/dist/components/dialogs/FeatureDialog.js.map +1 -1
- package/dist/components/header/Header.js +4 -0
- package/dist/components/header/Header.js.map +1 -1
- package/dist/components/header/HeaderStatusArea.js +0 -2
- package/dist/components/header/HeaderStatusArea.js.map +1 -1
- package/dist/components/header/LoadWarnings.d.ts +6 -0
- package/dist/components/header/LoadWarnings.js +36 -0
- package/dist/components/header/LoadWarnings.js.map +1 -0
- package/dist/components/header/MultiAlignmentSelector.js +0 -0
- package/dist/components/header/MultiAlignmentSelector.js.map +1 -1
- package/dist/components/header/TreeSettingsMenu.js +9 -1
- package/dist/components/header/TreeSettingsMenu.js.map +1 -1
- package/dist/components/header/UnshareableDataWarning.d.ts +6 -0
- package/dist/components/header/UnshareableDataWarning.js +42 -0
- package/dist/components/header/UnshareableDataWarning.js.map +1 -0
- package/dist/components/header/getAnnotationsMenu.js +14 -14
- package/dist/components/header/getAnnotationsMenu.js.map +1 -1
- package/dist/components/import/ImportForm.js +1 -1
- package/dist/components/import/ImportForm.js.map +1 -1
- package/dist/components/import/ImportFormExamples.js +2 -6
- package/dist/components/import/ImportFormExamples.js.map +1 -1
- package/dist/components/import/util.js +3 -0
- package/dist/components/import/util.js.map +1 -1
- package/dist/components/msa/MSACanvasBlock.js +4 -5
- package/dist/components/msa/MSACanvasBlock.js.map +1 -1
- package/dist/components/msa/msaRaster.d.ts +13 -3
- package/dist/components/msa/msaRaster.js +173 -61
- package/dist/components/msa/msaRaster.js.map +1 -1
- package/dist/components/msa/renderHighlights.d.ts +39 -1
- package/dist/components/msa/renderHighlights.js +71 -6
- package/dist/components/msa/renderHighlights.js.map +1 -1
- package/dist/components/msa/renderMSABlock.d.ts +1 -2
- package/dist/components/msa/renderMSABlock.js +24 -28
- package/dist/components/msa/renderMSABlock.js.map +1 -1
- package/dist/components/msa/renderMSAMouseover.js +9 -30
- package/dist/components/msa/renderMSAMouseover.js.map +1 -1
- package/dist/components/renderCtx.d.ts +1 -1
- package/dist/components/tracks/TrackBlocks.js +10 -11
- package/dist/components/tracks/TrackBlocks.js.map +1 -1
- package/dist/components/tracks/drawTracks.d.ts +26 -8
- package/dist/components/tracks/drawTracks.js +123 -13
- package/dist/components/tracks/drawTracks.js.map +1 -1
- package/dist/components/tree/TreeCanvas.js +2 -3
- package/dist/components/tree/TreeCanvas.js.map +1 -1
- package/dist/components/tree/TreeCanvasBlock.js +4 -2
- package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
- package/dist/components/tree/TreeRuler.js +13 -3
- package/dist/components/tree/TreeRuler.js.map +1 -1
- package/dist/components/tree/renderTreeCanvas.d.ts +0 -1
- package/dist/components/tree/renderTreeCanvas.js +70 -30
- package/dist/components/tree/renderTreeCanvas.js.map +1 -1
- package/dist/components/tree/scaleBar.d.ts +10 -0
- package/dist/components/tree/scaleBar.js +24 -0
- package/dist/components/tree/scaleBar.js.map +1 -0
- package/dist/constants.d.ts +3 -1
- package/dist/constants.js +18 -1
- package/dist/constants.js.map +1 -1
- package/dist/fetchUtils.d.ts +7 -7
- package/dist/fetchUtils.js +4 -32
- package/dist/fetchUtils.js.map +1 -1
- package/dist/hierarchy.d.ts +2 -1
- package/dist/hierarchy.js +16 -10
- package/dist/hierarchy.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/model/DataModel.js +5 -2
- package/dist/model/DataModel.js.map +1 -1
- package/dist/model.d.ts +454 -177
- package/dist/model.js +1180 -511
- package/dist/model.js.map +1 -1
- package/dist/renderToStaticMarkup.d.ts +11 -0
- package/dist/renderToStaticMarkup.js +30 -7
- package/dist/renderToStaticMarkup.js.map +1 -1
- package/dist/renderToSvg.js +25 -19
- package/dist/renderToSvg.js.map +1 -1
- package/dist/rowCoordinateCalculations.d.ts +25 -25
- package/dist/rowCoordinateCalculations.js +12 -36
- package/dist/rowCoordinateCalculations.js.map +1 -1
- package/dist/seqPosToGlobalCol.js +3 -1
- package/dist/seqPosToGlobalCol.js.map +1 -1
- package/dist/svgTestUtil.d.ts +144 -82
- package/dist/types.d.ts +97 -3
- package/dist/util.d.ts +11 -4
- package/dist/util.js +30 -10
- package/dist/util.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/wuss.d.ts +14 -0
- package/dist/wuss.js +55 -0
- package/dist/wuss.js.map +1 -0
- package/package.json +10 -8
- package/src/components/Loading.tsx +12 -2
- package/src/components/MSAViewer.tsx +106 -2
- package/src/components/ResizeHandles.tsx +31 -31
- package/src/components/dialogs/AnnotationFileDialog.tsx +162 -0
- package/src/components/dialogs/FeatureDialog.tsx +13 -9
- package/src/components/header/Header.tsx +4 -0
- package/src/components/header/HeaderStatusArea.tsx +0 -5
- package/src/components/header/LoadWarnings.tsx +49 -0
- package/src/components/header/MultiAlignmentSelector.tsx +0 -0
- package/src/components/header/TreeSettingsMenu.tsx +9 -1
- package/src/components/header/UnshareableDataWarning.tsx +58 -0
- package/src/components/header/getAnnotationsMenu.ts +16 -17
- package/src/components/import/ImportForm.tsx +2 -2
- package/src/components/import/ImportFormExamples.tsx +2 -6
- package/src/components/import/util.ts +3 -0
- package/src/components/msa/MSACanvasBlock.tsx +4 -5
- package/src/components/msa/msaRaster.ts +216 -59
- package/src/components/msa/renderHighlights.ts +103 -5
- package/src/components/msa/renderMSABlock.ts +25 -36
- package/src/components/msa/renderMSAMouseover.ts +9 -39
- package/src/components/renderCtx.ts +1 -0
- package/src/components/tracks/TrackBlocks.tsx +10 -13
- package/src/components/tracks/drawTracks.ts +165 -17
- package/src/components/tree/TreeCanvas.tsx +2 -3
- package/src/components/tree/TreeCanvasBlock.tsx +4 -2
- package/src/components/tree/TreeRuler.tsx +38 -3
- package/src/components/tree/renderTreeCanvas.ts +79 -35
- package/src/components/tree/scaleBar.ts +23 -0
- package/src/constants.ts +20 -1
- package/src/fetchUtils.ts +7 -40
- package/src/hierarchy.ts +20 -11
- package/src/index.ts +18 -2
- package/src/model/DataModel.ts +6 -2
- package/src/model.ts +1352 -616
- package/src/rawImports.d.ts +9 -0
- package/src/renderToStaticMarkup.ts +34 -7
- package/src/renderToSvg.tsx +33 -27
- package/src/rowCoordinateCalculations.ts +15 -59
- package/src/seqPosToGlobalCol.ts +3 -1
- package/src/types.ts +131 -4
- package/src/util.ts +31 -14
- package/src/version.ts +1 -1
- package/src/wuss.ts +62 -0
- package/dist/components/dialogs/InterProScanDialog.js +0 -137
- package/dist/components/dialogs/InterProScanDialog.js.map +0 -1
- package/dist/components/dialogs/InterProScanFileDialog.d.ts +0 -7
- package/dist/components/dialogs/InterProScanFileDialog.js +0 -64
- package/dist/components/dialogs/InterProScanFileDialog.js.map +0 -1
- package/dist/launchInterProScan.d.ts +0 -13
- package/dist/launchInterProScan.js +0 -110
- package/dist/launchInterProScan.js.map +0 -1
- package/dist/useColorContrast.d.ts +0 -10
- package/dist/useColorContrast.js +0 -13
- package/dist/useColorContrast.js.map +0 -1
- package/src/clustalX.test.ts +0 -74
- package/src/colSpace.test.ts +0 -67
- package/src/collapseLogic.test.ts +0 -115
- package/src/colorSchemes.docs.test.ts +0 -57
- package/src/columnCounts.test.ts +0 -73
- package/src/columnStats.test.ts +0 -94
- package/src/columnTracks.test.ts +0 -97
- package/src/components/dialogs/InterProScanDialog.tsx +0 -214
- package/src/components/dialogs/InterProScanFileDialog.tsx +0 -138
- package/src/components/minimap/minimapLayout.test.ts +0 -71
- package/src/components/msa/msaRaster.test.ts +0 -145
- package/src/components/msa/renderBoxFeatureCanvasBlock.test.ts +0 -51
- package/src/components/tree/renderTreeCanvas.test.ts +0 -296
- package/src/dataInitialized.test.ts +0 -42
- package/src/domainBands.test.ts +0 -122
- package/src/domainLetterColor.test.ts +0 -80
- package/src/exportFileName.test.ts +0 -40
- package/src/fetchUtils.test.ts +0 -83
- package/src/flatToTree.test.ts +0 -51
- package/src/hierarchy.test.ts +0 -228
- package/src/highlights.test.ts +0 -70
- package/src/impgRender.test.tsx +0 -68
- package/src/largeTree.test.ts +0 -67
- package/src/launchInterProScan.ts +0 -167
- package/src/loadErrorDisplay.test.tsx +0 -102
- package/src/modelFilehandleLoaders.test.ts +0 -137
- package/src/modelReset.test.ts +0 -120
- package/src/neighborJoining.test.ts +0 -164
- package/src/parseAsn1.test.ts +0 -36
- package/src/propertyConservation.test.ts +0 -36
- package/src/renderColumnTracksSvg.test.tsx +0 -108
- package/src/renderDomainsSvg.test.tsx +0 -118
- package/src/renderHighlightsSvg.test.tsx +0 -85
- package/src/renderMinimapSvg.test.tsx +0 -62
- package/src/renderRasterSvg.test.tsx +0 -144
- package/src/renderSequenceLogoSvg.test.tsx +0 -165
- package/src/renderToSvg.test.tsx +0 -65
- package/src/reset.test.ts +0 -61
- package/src/rowCoordinateCalculations.test.ts +0 -211
- package/src/rowSpace.test.ts +0 -46
- package/src/segmentOverlay.test.ts +0 -70
- package/src/seqPosToGlobalCol.test.ts +0 -59
- package/src/sequenceLogo.test.ts +0 -88
- package/src/setFontSize.test.ts +0 -33
- package/src/stripDefault.test.ts +0 -48
- package/src/stripDefaultSnapshot.test.ts +0 -87
- package/src/treeMetadata.test.ts +0 -39
- package/src/useCanvasAutorun.test.tsx +0 -98
- package/src/useColorContrast.ts +0 -18
- package/src/viewChrome.test.tsx +0 -134
- package/src/wheelZoom.test.ts +0 -46
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
// Verifies that ordinal segment features (exons) from a GFF overlay are treated
|
|
2
|
-
// as a numbered gene model — alternating two shades, labeled by number, and
|
|
3
|
-
// kept out of the categorical color-key legend — while real domains keep a
|
|
4
|
-
// distinct color + legend entry.
|
|
5
|
-
import { expect, test } from 'vitest'
|
|
6
|
-
|
|
7
|
-
import { segmentShades } from './constants.ts'
|
|
8
|
-
import MSAModelF from './model.ts'
|
|
9
|
-
|
|
10
|
-
const msa = `>human
|
|
11
|
-
ACDEFGHIKLMNPQRSTVWYACDEFGHIKLMNPQRSTVWYACDEFGHIKLMNPQRSTVWY
|
|
12
|
-
>mouse
|
|
13
|
-
ACDEFGHIKLMNPQRSTVWYACDEFGHIKLMNPQRSTVWYACDEFGHIKLMNPQRSTVWY`
|
|
14
|
-
|
|
15
|
-
// three exons in CDS coordinates plus one real domain, deliberately out of
|
|
16
|
-
// start order in the file to prove ordering is by position not file/length
|
|
17
|
-
const gff = `##gff-version 3
|
|
18
|
-
human src exon 41 60 . . . ID=human.exon3;Name=exon-3
|
|
19
|
-
human src exon 1 20 . . . ID=human.exon1;Name=exon-1
|
|
20
|
-
human src exon 21 40 . . . ID=human.exon2;Name=exon-2
|
|
21
|
-
human Pfam protein_match 5 35 . . . Name=PF00069;signature_desc=Kinase`
|
|
22
|
-
|
|
23
|
-
function makeModel() {
|
|
24
|
-
const model = MSAModelF().create({
|
|
25
|
-
id: 'segment-overlay-test',
|
|
26
|
-
type: 'MsaView',
|
|
27
|
-
height: 400,
|
|
28
|
-
msaFormat: 'fasta',
|
|
29
|
-
data: { msa },
|
|
30
|
-
})
|
|
31
|
-
model.setWidth(800)
|
|
32
|
-
model.applyGFFText(gff)
|
|
33
|
-
return model
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
test('exons are ordered by sequence position, domains kept separate', () => {
|
|
37
|
-
const model = makeModel()
|
|
38
|
-
expect(model.segmentDomainTypes.map(d => d.accession)).toEqual([
|
|
39
|
-
'exon-1',
|
|
40
|
-
'exon-2',
|
|
41
|
-
'exon-3',
|
|
42
|
-
])
|
|
43
|
-
expect(model.categoricalDomainTypes.map(d => d.accession)).toEqual([
|
|
44
|
-
'PF00069',
|
|
45
|
-
])
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
test('exons alternate two shades; domain gets its own color', () => {
|
|
49
|
-
const { fillPalette } = makeModel()
|
|
50
|
-
expect(fillPalette['exon-1']).toBe(segmentShades[0])
|
|
51
|
-
expect(fillPalette['exon-2']).toBe(segmentShades[1])
|
|
52
|
-
expect(fillPalette['exon-3']).toBe(segmentShades[0])
|
|
53
|
-
expect(fillPalette.PF00069).not.toBe(segmentShades[0])
|
|
54
|
-
expect(fillPalette.PF00069).not.toBe(segmentShades[1])
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
test('exons are labeled by their number', () => {
|
|
58
|
-
const { segmentLabels } = makeModel()
|
|
59
|
-
expect(segmentLabels.get('exon-1')).toBe('1')
|
|
60
|
-
expect(segmentLabels.get('exon-2')).toBe('2')
|
|
61
|
-
expect(segmentLabels.get('exon-3')).toBe('3')
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
test('legend lists only categorical domains, not exons', () => {
|
|
65
|
-
const model = makeModel()
|
|
66
|
-
for (const acc of ['exon-1', 'exon-2', 'exon-3', 'PF00069']) {
|
|
67
|
-
model.setFilter(acc, true)
|
|
68
|
-
}
|
|
69
|
-
expect(model.visibleDomainTypes.map(d => d.accession)).toEqual(['PF00069'])
|
|
70
|
-
})
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
// seqPos -> global column, exercised through the model method rather than the
|
|
2
|
-
// buildSeqPosIndex primitive behind it: the method is what
|
|
3
|
-
// jbrowse-plugin-protein3d calls (see the cross-repo note in model.ts), and it
|
|
4
|
-
// owns the past-the-end answers the index alone cannot give.
|
|
5
|
-
import { describe, expect, test } from 'vitest'
|
|
6
|
-
|
|
7
|
-
import MSAModelF from './model.ts'
|
|
8
|
-
|
|
9
|
-
function colsOf(row: string, positions: number[]) {
|
|
10
|
-
const model = MSAModelF().create({
|
|
11
|
-
type: 'MsaView',
|
|
12
|
-
data: { msa: `>r\n${row}` },
|
|
13
|
-
})
|
|
14
|
-
model.setWidth(800)
|
|
15
|
-
return positions.map(pos => model.seqPosToGlobalCol('r', pos))
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
describe('seqPosToGlobalCol', () => {
|
|
19
|
-
test('is the identity when the row has no gaps', () => {
|
|
20
|
-
// 8 residues, so seqPos 8 is one past the end
|
|
21
|
-
expect(colsOf('ATGCATGC', [0, 3, 7, 8])).toEqual([0, 3, 7, 8])
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
test('skips gap columns', () => {
|
|
25
|
-
// Global: A(0) -(1) T(2) G(3) -(4) C(5) A(6) -(7) T(8) G(9) C(10)
|
|
26
|
-
// SeqPos: A(0) T(1) G(2) C(3) A(4) T(5) G(6) C(7)
|
|
27
|
-
expect(colsOf('A-TG-CA-TGC', [0, 1, 2, 3, 4, 5, 6, 7, 8])).toEqual([
|
|
28
|
-
0, 2, 3, 5, 6, 8, 9, 10, 11,
|
|
29
|
-
])
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
test('counts both gap characters, - and .', () => {
|
|
33
|
-
// Global: A(0) -(1) .(2) G(3) -(4) C(5) .(6)
|
|
34
|
-
// SeqPos: A(0) G(1) C(2)
|
|
35
|
-
expect(colsOf('A-.G-C.', [0, 1, 2, 3])).toEqual([0, 3, 5, 7])
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
test('handles leading and trailing gaps', () => {
|
|
39
|
-
expect(colsOf('--ACG', [0, 1, 2])).toEqual([2, 3, 4])
|
|
40
|
-
expect(colsOf('ACG--', [0, 1, 2, 3])).toEqual([0, 1, 2, 5])
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
test('an all-gap row reports position 0 at column 0', () => {
|
|
44
|
-
expect(colsOf('---..--', [0, 1])).toEqual([0, 7])
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
test('an empty row reports column 0', () => {
|
|
48
|
-
expect(colsOf('', [0])).toEqual([0])
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
test('an unknown row reports column 0', () => {
|
|
52
|
-
const model = MSAModelF().create({
|
|
53
|
-
type: 'MsaView',
|
|
54
|
-
data: { msa: '>r\nACGT' },
|
|
55
|
-
})
|
|
56
|
-
model.setWidth(800)
|
|
57
|
-
expect(model.seqPosToGlobalCol('nope', 2)).toBe(0)
|
|
58
|
-
})
|
|
59
|
-
})
|
package/src/sequenceLogo.test.ts
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import { expect, test } from 'vitest'
|
|
2
|
-
|
|
3
|
-
import { columnCountsFromColumns } from './columnCounts.ts'
|
|
4
|
-
import { columnLogoStack, maxBitsFor } from './sequenceLogo.ts'
|
|
5
|
-
|
|
6
|
-
const dnaBits = maxBitsFor('dna')
|
|
7
|
-
const aminoBits = maxBitsFor('amino')
|
|
8
|
-
|
|
9
|
-
function stackOf(column: string, maxBits = dnaBits) {
|
|
10
|
-
return columnLogoStack(columnCountsFromColumns([column]), 0, maxBits)
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
test('maxBits is log2 of the alphabet', () => {
|
|
14
|
-
expect(maxBitsFor('dna')).toBe(2)
|
|
15
|
-
expect(maxBitsFor('rna')).toBe(2)
|
|
16
|
-
expect(maxBitsFor('amino')).toBeCloseTo(4.3219, 4)
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
test('a fully conserved column is one letter at the full height', () => {
|
|
20
|
-
const stack = stackOf('AAAA')
|
|
21
|
-
expect(stack).toHaveLength(1)
|
|
22
|
-
expect(stack[0]!.letter).toBe('A')
|
|
23
|
-
expect(stack[0]!.bits).toBeCloseTo(2, 10)
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
test('an even four-way column carries no information', () => {
|
|
27
|
-
expect(stackOf('ACGT')).toEqual([])
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
test('a two-way split is half the height, shared evenly', () => {
|
|
31
|
-
const stack = stackOf('AACC')
|
|
32
|
-
expect(stack.map(s => s.letter)).toEqual(['A', 'C'])
|
|
33
|
-
// entropy 1 bit, so information content is 2 - 1 = 1, split evenly
|
|
34
|
-
expect(stack[0]!.bits).toBeCloseTo(0.5, 10)
|
|
35
|
-
expect(stack[1]!.bits).toBeCloseTo(0.5, 10)
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
test('letters are ordered ascending so the tallest draws last, on top', () => {
|
|
39
|
-
const stack = stackOf('AAAC')
|
|
40
|
-
expect(stack.map(s => s.letter)).toEqual(['C', 'A'])
|
|
41
|
-
expect(stack[0]!.bits).toBeLessThan(stack[1]!.bits)
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
test('ties break on letter so redraws are stable', () => {
|
|
45
|
-
expect(stackOf('GGCC').map(s => s.letter)).toEqual(['C', 'G'])
|
|
46
|
-
expect(stackOf('CCGG').map(s => s.letter)).toEqual(['C', 'G'])
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
test('gaps scale the stack down without skewing the frequencies', () => {
|
|
50
|
-
// 'A' is the only residue, so it takes the whole stack either way; the height
|
|
51
|
-
// is what the gaps change
|
|
52
|
-
const clean = stackOf('AAAA')
|
|
53
|
-
const gappy = stackOf('AA--')
|
|
54
|
-
expect(gappy[0]!.letter).toBe('A')
|
|
55
|
-
expect(gappy[0]!.bits).toBeCloseTo(clean[0]!.bits / 2, 10)
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
test('gaps do not make a two-residue column look like a four-way one', () => {
|
|
59
|
-
// frequencies come from the two real residues, so this is a 1-bit choice
|
|
60
|
-
// scaled by the half-gap occupancy, not a 4-way split
|
|
61
|
-
const stack = stackOf('AC--')
|
|
62
|
-
expect(stack.map(s => s.letter)).toEqual(['A', 'C'])
|
|
63
|
-
expect(stack[0]!.bits + stack[1]!.bits).toBeCloseTo(0.5, 10)
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
test('an all-gap column has no stack', () => {
|
|
67
|
-
expect(stackOf('----')).toEqual([])
|
|
68
|
-
expect(stackOf('....')).toEqual([])
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
test('the protein ceiling is higher than the nucleotide one', () => {
|
|
72
|
-
const protein = stackOf('WWWW', aminoBits)
|
|
73
|
-
expect(protein[0]!.bits).toBeCloseTo(aminoBits, 10)
|
|
74
|
-
expect(protein[0]!.bits).toBeGreaterThan(stackOf('AAAA')[0]!.bits)
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
test('stack height never exceeds the ceiling', () => {
|
|
78
|
-
const columns = ['AAAA', 'AACG', 'A-C-', 'ACGT', 'AAAT']
|
|
79
|
-
for (const column of columns) {
|
|
80
|
-
const total = stackOf(column).reduce((a, b) => a + b.bits, 0)
|
|
81
|
-
expect(total).toBeLessThanOrEqual(dnaBits + 1e-9)
|
|
82
|
-
}
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
test('a column past the end of a ragged alignment is empty, not a crash', () => {
|
|
86
|
-
const counts = columnCountsFromColumns(['AC'])
|
|
87
|
-
expect(columnLogoStack(counts, 5, dnaBits)).toEqual([])
|
|
88
|
-
})
|
package/src/setFontSize.test.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'vitest'
|
|
2
|
-
|
|
3
|
-
import { setFontSize } from './setFontSize.ts'
|
|
4
|
-
|
|
5
|
-
describe('setFontSize', () => {
|
|
6
|
-
test('replaces integer size, preserving family', () => {
|
|
7
|
-
const ctx = { font: '10px sans-serif' }
|
|
8
|
-
setFontSize(ctx, 16)
|
|
9
|
-
expect(ctx.font).toBe('16px sans-serif')
|
|
10
|
-
})
|
|
11
|
-
|
|
12
|
-
test('handles fractional sizes from smooth zoom', () => {
|
|
13
|
-
// regression: \d+ alone matched "8604" inside "14.8604px" and produced an
|
|
14
|
-
// invalid string like "14 0.18px sans-serif", silently breaking fillText
|
|
15
|
-
const ctx = { font: '14.8604px sans-serif' }
|
|
16
|
-
setFontSize(ctx, 16.9372)
|
|
17
|
-
expect(ctx.font).toBe('16.9372px sans-serif')
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
test('does not accumulate "bold " across re-renders', () => {
|
|
21
|
-
const ctx = { font: '10px sans-serif' }
|
|
22
|
-
setFontSize(ctx, 12, true)
|
|
23
|
-
expect(ctx.font).toBe('bold 12px sans-serif')
|
|
24
|
-
setFontSize(ctx, 14, true)
|
|
25
|
-
expect(ctx.font).toBe('bold 14px sans-serif')
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
test('drops "bold " when bold goes false', () => {
|
|
29
|
-
const ctx = { font: 'bold 12px sans-serif' }
|
|
30
|
-
setFontSize(ctx, 14)
|
|
31
|
-
expect(ctx.font).toBe('14px sans-serif')
|
|
32
|
-
})
|
|
33
|
-
})
|
package/src/stripDefault.test.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { getSnapshot, types } from '@jbrowse/mobx-state-tree'
|
|
2
|
-
import { afterEach, expect, test } from 'vitest'
|
|
3
|
-
|
|
4
|
-
import { stripDefault } from './stripDefault.ts'
|
|
5
|
-
|
|
6
|
-
const real = types.stripDefault
|
|
7
|
-
|
|
8
|
-
afterEach(() => {
|
|
9
|
-
types.stripDefault = real
|
|
10
|
-
})
|
|
11
|
-
|
|
12
|
-
test('strips defaults from the snapshot when the host mst supports it', () => {
|
|
13
|
-
const model = types
|
|
14
|
-
.model({ a: stripDefault(types.boolean, false) })
|
|
15
|
-
.create({})
|
|
16
|
-
expect(model.a).toBe(false)
|
|
17
|
-
expect(getSnapshot(model).a).toBeUndefined()
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
// The path that actually runs inside every RELEASED jbrowse-web: the host
|
|
21
|
-
// supplies its own mobx-state-tree, and stripDefault only exists in the copy
|
|
22
|
-
// that ships with unreleased @jbrowse/core. Calling it there used to throw while
|
|
23
|
-
// the model was built, which error-paged the whole app. dev has the API, so
|
|
24
|
-
// without stubbing it away this path is never exercised.
|
|
25
|
-
test('falls back to types.optional when the host mst lacks stripDefault', () => {
|
|
26
|
-
// @ts-expect-error deleting it is the point: simulate an older host
|
|
27
|
-
delete types.stripDefault
|
|
28
|
-
const model = types
|
|
29
|
-
.model({ a: stripDefault(types.boolean, false) })
|
|
30
|
-
.create({})
|
|
31
|
-
expect(model.a).toBe(false)
|
|
32
|
-
// the only difference: the default is serialized rather than omitted
|
|
33
|
-
expect(getSnapshot(model).a).toBe(false)
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
test('non-default values survive both paths', () => {
|
|
37
|
-
const withApi = types
|
|
38
|
-
.model({ a: stripDefault(types.number, 1) })
|
|
39
|
-
.create({ a: 5 })
|
|
40
|
-
expect(getSnapshot(withApi).a).toBe(5)
|
|
41
|
-
|
|
42
|
-
// @ts-expect-error see above
|
|
43
|
-
delete types.stripDefault
|
|
44
|
-
const without = types
|
|
45
|
-
.model({ a: stripDefault(types.number, 1) })
|
|
46
|
-
.create({ a: 5 })
|
|
47
|
-
expect(getSnapshot(without).a).toBe(5)
|
|
48
|
-
})
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import { getSnapshot } from '@jbrowse/mobx-state-tree'
|
|
2
|
-
import { expect, test } from 'vitest'
|
|
3
|
-
|
|
4
|
-
import stateModelFactory from './model.ts'
|
|
5
|
-
|
|
6
|
-
const MsaView = stateModelFactory()
|
|
7
|
-
|
|
8
|
-
test('default view produces a minimal snapshot (defaults stripped)', () => {
|
|
9
|
-
const model = MsaView.create({ type: 'MsaView' })
|
|
10
|
-
const snap = getSnapshot(model)
|
|
11
|
-
// only the always-present keys remain; every defaulted scalar/collection is gone
|
|
12
|
-
expect(snap.colWidth).toBeUndefined()
|
|
13
|
-
expect(snap.rowHeight).toBeUndefined()
|
|
14
|
-
expect(snap.showDomains).toBeUndefined()
|
|
15
|
-
expect(snap.colorSchemeName).toBeUndefined()
|
|
16
|
-
expect(snap.drawTree).toBeUndefined()
|
|
17
|
-
expect(snap.treeAreaWidth).toBeUndefined()
|
|
18
|
-
expect(snap.collapsed).toBeUndefined()
|
|
19
|
-
expect(snap.turnedOffTracks).toBeUndefined()
|
|
20
|
-
expect(snap.type).toBe('MsaView')
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
test('non-default values are retained in the snapshot', () => {
|
|
24
|
-
const model = MsaView.create({ type: 'MsaView' })
|
|
25
|
-
model.setColWidth(30)
|
|
26
|
-
model.setColorSchemeName('clustalx')
|
|
27
|
-
model.setDrawTree(false)
|
|
28
|
-
model.toggleCollapsed('nodeA')
|
|
29
|
-
const snap = getSnapshot(model)
|
|
30
|
-
expect(snap.colWidth).toBe(30)
|
|
31
|
-
expect(snap.colorSchemeName).toBe('clustalx')
|
|
32
|
-
expect(snap.drawTree).toBe(false)
|
|
33
|
-
expect(snap.collapsed).toEqual(['nodeA'])
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
test('setting a value back to its default strips it again', () => {
|
|
37
|
-
const model = MsaView.create({ type: 'MsaView' })
|
|
38
|
-
model.setColWidth(30)
|
|
39
|
-
expect(getSnapshot(model).colWidth).toBe(30)
|
|
40
|
-
model.setColWidth(12) // defaultColWidth
|
|
41
|
-
expect(getSnapshot(model).colWidth).toBeUndefined()
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
test('inline data is stripped when a filehandle can refetch it', () => {
|
|
45
|
-
const model = MsaView.create({
|
|
46
|
-
type: 'MsaView',
|
|
47
|
-
data: { msa: '>s1\nACGT', tree: '(a,b);' },
|
|
48
|
-
msaFilehandle: {
|
|
49
|
-
uri: 'http://example.com/x.fa',
|
|
50
|
-
locationType: 'UriLocation',
|
|
51
|
-
},
|
|
52
|
-
})
|
|
53
|
-
const snap = getSnapshot(model)
|
|
54
|
-
// msa dropped (msaFilehandle present), tree kept (no treeFilehandle)
|
|
55
|
-
expect(snap.data.msa).toBeUndefined()
|
|
56
|
-
expect(snap.data.tree).toBe('(a,b);')
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
test('inline gff survives the snapshot when no gffFilehandle can refetch it', () => {
|
|
60
|
-
const gff = 's1\tx\tprotein_match\t1\t3\t.\t.\t.\tName=PF1'
|
|
61
|
-
const withInlineGff = MsaView.create({
|
|
62
|
-
type: 'MsaView',
|
|
63
|
-
data: { msa: '>s1\nACGT', gff },
|
|
64
|
-
})
|
|
65
|
-
expect(getSnapshot(withInlineGff).data.gff).toBe(gff)
|
|
66
|
-
|
|
67
|
-
const withFilehandle = MsaView.create({
|
|
68
|
-
type: 'MsaView',
|
|
69
|
-
data: { msa: '>s1\nACGT', gff },
|
|
70
|
-
gffFilehandle: {
|
|
71
|
-
uri: 'http://example.com/x.gff',
|
|
72
|
-
locationType: 'UriLocation',
|
|
73
|
-
},
|
|
74
|
-
})
|
|
75
|
-
expect(getSnapshot(withFilehandle).data.gff).toBeUndefined()
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
// The legend floats over the top-right of the alignment, so collapsing it is a
|
|
79
|
-
// way of seeing the residues under it. As component state that choice was lost
|
|
80
|
-
// on every reload and could not be set by a session or a figure at all.
|
|
81
|
-
test('a collapsed domain legend survives into the snapshot', () => {
|
|
82
|
-
const model = MsaView.create({ type: 'MsaView' })
|
|
83
|
-
expect(getSnapshot(model).showDomainLegend).toBeUndefined()
|
|
84
|
-
model.setShowDomainLegend(false)
|
|
85
|
-
expect(getSnapshot(model).showDomainLegend).toBe(false)
|
|
86
|
-
expect(MsaView.create(getSnapshot(model)).showDomainLegend).toBe(false)
|
|
87
|
-
})
|
package/src/treeMetadata.test.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { expect, test, vi } from 'vitest'
|
|
2
|
-
|
|
3
|
-
import stateModelFactory from './model.ts'
|
|
4
|
-
|
|
5
|
-
const MsaView = stateModelFactory()
|
|
6
|
-
|
|
7
|
-
const msa = '>seq1\nACGT\n>seq2\nACCT\n'
|
|
8
|
-
|
|
9
|
-
test('treeMetadata is parsed and keyed by row name', () => {
|
|
10
|
-
const model = MsaView.create({ type: 'MsaView' })
|
|
11
|
-
model.setData({
|
|
12
|
-
msa,
|
|
13
|
-
treeMetadata: JSON.stringify({ seq1: { genome: 'Human' } }),
|
|
14
|
-
})
|
|
15
|
-
expect(model.treeMetadata.seq1?.genome).toBe('Human')
|
|
16
|
-
expect(model.treeMetadata.seq2).toBeUndefined()
|
|
17
|
-
expect(model.getRowData('seq1').treeMetadata).toEqual({ genome: 'Human' })
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
test('malformed treeMetadata degrades to empty rather than throwing', () => {
|
|
21
|
-
const model = MsaView.create({ type: 'MsaView' })
|
|
22
|
-
const spy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
23
|
-
model.setData({ msa, treeMetadata: '{not json' })
|
|
24
|
-
|
|
25
|
-
// treeMetadata comes from a user-supplied file, and consumers of this
|
|
26
|
-
// computed run during layout/render (labelWidthMap measures every label), so
|
|
27
|
-
// a parse failure must not throw out of them
|
|
28
|
-
expect(model.treeMetadata).toEqual({})
|
|
29
|
-
expect(() => model.getRowData('seq1')).not.toThrow()
|
|
30
|
-
expect(spy).toHaveBeenCalled()
|
|
31
|
-
spy.mockRestore()
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
test('non-object treeMetadata degrades to empty', () => {
|
|
35
|
-
const model = MsaView.create({ type: 'MsaView' })
|
|
36
|
-
model.setData({ msa, treeMetadata: 'null' })
|
|
37
|
-
expect(model.treeMetadata).toEqual({})
|
|
38
|
-
expect(() => model.getRowData('seq1')).not.toThrow()
|
|
39
|
-
})
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
//
|
|
3
|
-
// Assigning width/height to a canvas resets its bitmap, and React assigns them
|
|
4
|
-
// on every commit where they changed. A draw that ran during the mobx reaction
|
|
5
|
-
// -- before React commits -- is therefore wiped by the resize that follows it,
|
|
6
|
-
// which is what used to blank the tree on a tree-area drag and the consensus
|
|
7
|
-
// tracks on a vertical zoom. So the hook has to draw again after the resize.
|
|
8
|
-
import React, { act } from 'react'
|
|
9
|
-
|
|
10
|
-
import { observable, runInAction } from 'mobx'
|
|
11
|
-
import { observer } from 'mobx-react'
|
|
12
|
-
import { createRoot } from 'react-dom/client'
|
|
13
|
-
import { afterEach, beforeAll, beforeEach, expect, test } from 'vitest'
|
|
14
|
-
|
|
15
|
-
import { useCanvasAutorun } from './useCanvasAutorun.ts'
|
|
16
|
-
|
|
17
|
-
import type { Root } from 'react-dom/client'
|
|
18
|
-
|
|
19
|
-
Reflect.set(globalThis, 'IS_REACT_ACT_ENVIRONMENT', true)
|
|
20
|
-
|
|
21
|
-
beforeAll(() => {
|
|
22
|
-
HTMLCanvasElement.prototype.getContext = function (this: HTMLCanvasElement) {
|
|
23
|
-
return { canvas: this } as unknown as CanvasRenderingContext2D
|
|
24
|
-
} as unknown as typeof HTMLCanvasElement.prototype.getContext
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
const state = observable({ width: 100, color: 'red' })
|
|
28
|
-
// what each draw saw: the observables it renders from, and the size of the
|
|
29
|
-
// canvas it was drawing onto
|
|
30
|
-
let draws: { observed: number; color: string; canvas: number }[] = []
|
|
31
|
-
|
|
32
|
-
const Probe = observer(function () {
|
|
33
|
-
const ref = useCanvasAutorun({
|
|
34
|
-
draw: ctx => {
|
|
35
|
-
draws.push({
|
|
36
|
-
observed: state.width,
|
|
37
|
-
color: state.color,
|
|
38
|
-
canvas: ctx.canvas.width,
|
|
39
|
-
})
|
|
40
|
-
},
|
|
41
|
-
width: state.width,
|
|
42
|
-
height: 50,
|
|
43
|
-
deps: [],
|
|
44
|
-
})
|
|
45
|
-
return <canvas ref={ref} width={state.width} height={50} />
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
let container: HTMLElement
|
|
49
|
-
let root: Root
|
|
50
|
-
|
|
51
|
-
beforeEach(() => {
|
|
52
|
-
draws = []
|
|
53
|
-
runInAction(() => {
|
|
54
|
-
state.width = 100
|
|
55
|
-
state.color = 'red'
|
|
56
|
-
})
|
|
57
|
-
container = document.createElement('div')
|
|
58
|
-
document.body.appendChild(container)
|
|
59
|
-
root = createRoot(container)
|
|
60
|
-
act(() => {
|
|
61
|
-
root.render(<Probe />)
|
|
62
|
-
})
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
afterEach(() => {
|
|
66
|
-
act(() => {
|
|
67
|
-
root.unmount()
|
|
68
|
-
})
|
|
69
|
-
container.remove()
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
test('the first draw sees the canvas at its rendered size', () => {
|
|
73
|
-
expect(draws).toEqual([{ observed: 100, color: 'red', canvas: 100 }])
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
test('a resize is followed by a draw onto the resized canvas', () => {
|
|
77
|
-
act(() => {
|
|
78
|
-
runInAction(() => {
|
|
79
|
-
state.width = 300
|
|
80
|
-
})
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
expect(container.querySelector('canvas')!.width).toBe(300)
|
|
84
|
-
expect(draws.at(-1)).toEqual({ observed: 300, color: 'red', canvas: 300 })
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
test('a change that leaves the size alone draws exactly once', () => {
|
|
88
|
-
act(() => {
|
|
89
|
-
runInAction(() => {
|
|
90
|
-
state.color = 'blue'
|
|
91
|
-
})
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
expect(draws).toEqual([
|
|
95
|
-
{ observed: 100, color: 'red', canvas: 100 },
|
|
96
|
-
{ observed: 100, color: 'blue', canvas: 100 },
|
|
97
|
-
])
|
|
98
|
-
})
|
package/src/useColorContrast.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { useMemo } from 'react'
|
|
2
|
-
|
|
3
|
-
import { useTheme } from '@mui/material'
|
|
4
|
-
|
|
5
|
-
import { colorContrast } from './util.ts'
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Resolves the current theme and a memoized contrast-color scheme for a given
|
|
9
|
-
* color scheme. Shared by the MSA and text-track canvas blocks.
|
|
10
|
-
*/
|
|
11
|
-
export function useColorContrast(colorScheme: Record<string, string>) {
|
|
12
|
-
const theme = useTheme()
|
|
13
|
-
const contrastScheme = useMemo(
|
|
14
|
-
() => colorContrast(colorScheme, theme),
|
|
15
|
-
[colorScheme, theme],
|
|
16
|
-
)
|
|
17
|
-
return { theme, contrastScheme }
|
|
18
|
-
}
|
package/src/viewChrome.test.tsx
DELETED
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
//
|
|
3
|
-
// The chrome around the alignment -- the tree/height resize handles, the
|
|
4
|
-
// minimap and the vertical scrollbar -- is all built from the same
|
|
5
|
-
// mousedown-then-drag hook, and none of it is exercised by the render tests.
|
|
6
|
-
// This mounts the real view and drives one handle of each kind.
|
|
7
|
-
import React, { act } from 'react'
|
|
8
|
-
|
|
9
|
-
import { createRoot } from 'react-dom/client'
|
|
10
|
-
import { afterEach, beforeAll, beforeEach, expect, test } from 'vitest'
|
|
11
|
-
|
|
12
|
-
import MSAView from './components/Loading.tsx'
|
|
13
|
-
import MSAModelF from './model.ts'
|
|
14
|
-
|
|
15
|
-
import type { MsaViewModel } from './model.ts'
|
|
16
|
-
import type { Root } from 'react-dom/client'
|
|
17
|
-
|
|
18
|
-
Reflect.set(globalThis, 'IS_REACT_ACT_ENVIRONMENT', true)
|
|
19
|
-
|
|
20
|
-
beforeAll(() => {
|
|
21
|
-
HTMLCanvasElement.prototype.getContext = function (this: HTMLCanvasElement) {
|
|
22
|
-
return {
|
|
23
|
-
canvas: this,
|
|
24
|
-
font: '12px sans-serif',
|
|
25
|
-
measureText: (t: string) => ({ width: t.length * 7 }),
|
|
26
|
-
arc() {},
|
|
27
|
-
beginPath() {},
|
|
28
|
-
clearRect() {},
|
|
29
|
-
closePath() {},
|
|
30
|
-
fill() {},
|
|
31
|
-
fillRect() {},
|
|
32
|
-
fillText() {},
|
|
33
|
-
lineTo() {},
|
|
34
|
-
moveTo() {},
|
|
35
|
-
resetTransform() {},
|
|
36
|
-
scale() {},
|
|
37
|
-
setLineDash() {},
|
|
38
|
-
stroke() {},
|
|
39
|
-
strokeRect() {},
|
|
40
|
-
translate() {},
|
|
41
|
-
} as unknown as CanvasRenderingContext2D
|
|
42
|
-
} as unknown as typeof HTMLCanvasElement.prototype.getContext
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
// tall and wide enough that both scrollbars are on screen
|
|
46
|
-
const names = Array.from({ length: 20 }, (_, i) => `seq${i}`)
|
|
47
|
-
const msa = names.map(name => `>${name}\n${'ACGT'.repeat(25)}`).join('\n')
|
|
48
|
-
const tree = `(${names.map(n => `${n}:0.1`).join(',')});`
|
|
49
|
-
|
|
50
|
-
let container: HTMLDivElement
|
|
51
|
-
let root: Root
|
|
52
|
-
let model: MsaViewModel
|
|
53
|
-
|
|
54
|
-
beforeEach(() => {
|
|
55
|
-
container = document.createElement('div')
|
|
56
|
-
document.body.append(container)
|
|
57
|
-
root = createRoot(container)
|
|
58
|
-
model = MSAModelF().create({
|
|
59
|
-
type: 'MsaView',
|
|
60
|
-
msaFormat: 'fasta',
|
|
61
|
-
height: 200,
|
|
62
|
-
data: { msa, tree },
|
|
63
|
-
})
|
|
64
|
-
model.setWidth(800)
|
|
65
|
-
act(() => {
|
|
66
|
-
root.render(<MSAView model={model} />)
|
|
67
|
-
})
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
afterEach(() => {
|
|
71
|
-
act(() => {
|
|
72
|
-
root.unmount()
|
|
73
|
-
})
|
|
74
|
-
container.remove()
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
function byCursor(cursor: string) {
|
|
78
|
-
return [...container.querySelectorAll<HTMLElement>('div')].filter(
|
|
79
|
-
el => el.style.cursor === cursor,
|
|
80
|
-
)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// useDragScroll defers each move to an animation frame, so a drag needs one
|
|
84
|
-
// flushed frame before the model has moved
|
|
85
|
-
async function drag(el: HTMLElement, { x = 0, y = 0 }) {
|
|
86
|
-
await act(async () => {
|
|
87
|
-
el.dispatchEvent(
|
|
88
|
-
new MouseEvent('mousedown', { bubbles: true, clientX: 0, clientY: 0 }),
|
|
89
|
-
)
|
|
90
|
-
})
|
|
91
|
-
await act(async () => {
|
|
92
|
-
document.dispatchEvent(
|
|
93
|
-
new MouseEvent('mousemove', { bubbles: true, clientX: x, clientY: y }),
|
|
94
|
-
)
|
|
95
|
-
await new Promise(resolve => {
|
|
96
|
-
requestAnimationFrame(resolve)
|
|
97
|
-
})
|
|
98
|
-
})
|
|
99
|
-
await act(async () => {
|
|
100
|
-
document.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }))
|
|
101
|
-
})
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
test('the view mounts with both scrollbars', () => {
|
|
105
|
-
expect(container.querySelector('[data-testid="msa_canvas"]')).toBeTruthy()
|
|
106
|
-
expect(model.showVerticalScrollbar).toBe(true)
|
|
107
|
-
expect(model.showHorizontalScrollbar).toBe(true)
|
|
108
|
-
// one horizontal divider (tree/msa), two vertical (view height, and the
|
|
109
|
-
// conservation track's own)
|
|
110
|
-
expect(byCursor('ew-resize')).toHaveLength(1)
|
|
111
|
-
expect(byCursor('ns-resize')).toHaveLength(2)
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
test('dragging the tree divider widens the tree area', async () => {
|
|
115
|
-
const before = model.treeAreaWidth
|
|
116
|
-
await drag(byCursor('ew-resize')[0]!, { x: 60 })
|
|
117
|
-
expect(model.treeAreaWidth).toBe(before + 60)
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
test('dragging the view divider grows the view', async () => {
|
|
121
|
-
const before = model.height
|
|
122
|
-
// the last ns-resize handle is the one below the whole view
|
|
123
|
-
await drag(byCursor('ns-resize').at(-1)!, { y: 50 })
|
|
124
|
-
expect(model.height).toBe(before + 50)
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
test('dragging the scrollbar thumb scrolls the alignment', async () => {
|
|
128
|
-
const thumb = container.querySelector<HTMLElement>(
|
|
129
|
-
'div[style*="cursor: pointer"]',
|
|
130
|
-
)!
|
|
131
|
-
expect(model.scrollX).toBe(0)
|
|
132
|
-
await drag(thumb, { x: 40 })
|
|
133
|
-
expect(model.scrollX).toBeLessThan(0)
|
|
134
|
-
})
|