jbrowse-plugin-msaview 3.3.0 → 3.4.1

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 (149) hide show
  1. package/dist/AddHighlightModel/GenomeMouseoverHighlight.js +1 -1
  2. package/dist/AddHighlightModel/MsaToGenomeHighlight.js +1 -1
  3. package/dist/AddHighlightModel/index.js +1 -1
  4. package/dist/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.js +60 -18
  5. package/dist/LaunchMsaView/components/BlastQuery/BlastManualPanel.js +1 -1
  6. package/dist/LaunchMsaView/components/BlastQuery/BlastPanel.js +2 -2
  7. package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.d.ts +12 -0
  8. package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.js +21 -2
  9. package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.test.d.ts +1 -0
  10. package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.test.js +29 -0
  11. package/dist/LaunchMsaView/components/BlastQuery/MsaAlgorithmSelect.js +1 -1
  12. package/dist/LaunchMsaView/components/BlastQuery/consts.d.ts +28 -0
  13. package/dist/LaunchMsaView/components/BlastQuery/consts.js +21 -0
  14. package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.d.ts +7 -0
  15. package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.js +42 -0
  16. package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.test.d.ts +1 -0
  17. package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.test.js +32 -0
  18. package/dist/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.js +1 -1
  19. package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.js +5 -4
  20. package/dist/LaunchMsaView/components/SubmitCancelActions.test.js +1 -1
  21. package/dist/LaunchMsaView/components/useFeatureSequence.js +1 -1
  22. package/dist/LaunchMsaView/detectQueryRow.d.ts +15 -2
  23. package/dist/LaunchMsaView/detectQueryRow.js +20 -21
  24. package/dist/LaunchMsaView/detectQueryRow.test.js +15 -15
  25. package/dist/LaunchMsaView/useQueryRowName.js +5 -8
  26. package/dist/LaunchMsaViewExtensionPoint/index.js +11 -6
  27. package/dist/LaunchMsaViewExtensionPoint/index.test.d.ts +1 -0
  28. package/dist/LaunchMsaViewExtensionPoint/index.test.js +43 -0
  29. package/dist/MsaViewPanel/afterCreateAutoruns.d.ts +15 -0
  30. package/dist/MsaViewPanel/afterCreateAutoruns.js +95 -72
  31. package/dist/MsaViewPanel/components/ErrorBoundary.d.ts +2 -2
  32. package/dist/MsaViewPanel/components/JobLink.js +7 -1
  33. package/dist/MsaViewPanel/components/LaunchProgress.d.ts +17 -0
  34. package/dist/MsaViewPanel/components/LaunchProgress.js +50 -0
  35. package/dist/MsaViewPanel/components/MsaViewPanel.js +6 -3
  36. package/dist/MsaViewPanel/components/MsaViewPanel.test.d.ts +1 -0
  37. package/dist/MsaViewPanel/components/MsaViewPanel.test.js +87 -0
  38. package/dist/MsaViewPanel/doLaunchBlast.d.ts +4 -2
  39. package/dist/MsaViewPanel/doLaunchBlast.js +86 -56
  40. package/dist/MsaViewPanel/doLaunchOrthologs.d.ts +3 -1
  41. package/dist/MsaViewPanel/doLaunchOrthologs.js +6 -5
  42. package/dist/MsaViewPanel/doLaunchOrthologs.test.js +39 -25
  43. package/dist/MsaViewPanel/genomeToMSA.js +4 -2
  44. package/dist/MsaViewPanel/genomeToMSA.test.js +34 -0
  45. package/dist/MsaViewPanel/model.d.ts +190 -19
  46. package/dist/MsaViewPanel/model.js +51 -1
  47. package/dist/MsaViewPanel/msaDataStore.d.ts +5 -3
  48. package/dist/MsaViewPanel/msaDataStore.js +16 -7
  49. package/dist/MsaViewPanel/msaDataStore.test.d.ts +1 -0
  50. package/dist/MsaViewPanel/msaDataStore.test.js +44 -0
  51. package/dist/MsaViewPanel/observeProteinHighlights.test.js +11 -0
  52. package/dist/MsaViewPanel/runLaunch.d.ts +38 -0
  53. package/dist/MsaViewPanel/runLaunch.js +65 -0
  54. package/dist/MsaViewPanel/runLaunch.test.d.ts +1 -0
  55. package/dist/MsaViewPanel/runLaunch.test.js +129 -0
  56. package/dist/MsaViewPanel/storedData.test.d.ts +1 -0
  57. package/dist/MsaViewPanel/storedData.test.js +160 -0
  58. package/dist/MsaViewPanel/syncGenomeHoverToMsaColumn.test.js +1 -0
  59. package/dist/MsaViewPanel/util.d.ts +18 -0
  60. package/dist/MsaViewPanel/util.js +17 -0
  61. package/dist/jbrowse-plugin-msaview.umd.production.min.js +47 -35
  62. package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
  63. package/dist/utils/blastCache.d.ts +10 -6
  64. package/dist/utils/blastCache.js +47 -3
  65. package/dist/utils/blastCache.test.d.ts +1 -0
  66. package/dist/utils/blastCache.test.js +72 -0
  67. package/dist/utils/ebiBlast.d.ts +5 -3
  68. package/dist/utils/ebiBlast.js +6 -4
  69. package/dist/utils/ebiJobDispatcher.d.ts +6 -3
  70. package/dist/utils/ebiJobDispatcher.js +13 -6
  71. package/dist/utils/fetch.d.ts +8 -1
  72. package/dist/utils/fetch.js +28 -2
  73. package/dist/utils/msa.d.ts +15 -1
  74. package/dist/utils/msa.js +40 -13
  75. package/dist/utils/msaRows.d.ts +31 -0
  76. package/dist/utils/msaRows.js +67 -0
  77. package/dist/utils/ncbiDomains.d.ts +2 -2
  78. package/dist/utils/ncbiOrthologs.d.ts +11 -1
  79. package/dist/utils/ncbiOrthologs.js +26 -7
  80. package/dist/utils/ncbiOrthologs.test.js +23 -1
  81. package/dist/utils/pantherOrthologs.js +2 -10
  82. package/dist/utils/phmmer.d.ts +54 -0
  83. package/dist/utils/phmmer.js +120 -0
  84. package/dist/utils/poll.d.ts +6 -1
  85. package/dist/utils/poll.js +7 -2
  86. package/dist/utils/taxonomyNames.d.ts +1 -1
  87. package/dist/utils/taxonomyNames.js +6 -1
  88. package/dist/version.d.ts +1 -1
  89. package/dist/version.js +1 -1
  90. package/package.json +30 -24
  91. package/src/AddHighlightModel/GenomeMouseoverHighlight.tsx +1 -1
  92. package/src/AddHighlightModel/MsaToGenomeHighlight.tsx +1 -1
  93. package/src/AddHighlightModel/index.tsx +1 -1
  94. package/src/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.tsx +85 -31
  95. package/src/LaunchMsaView/components/BlastQuery/BlastManualPanel.tsx +1 -1
  96. package/src/LaunchMsaView/components/BlastQuery/BlastPanel.tsx +4 -4
  97. package/src/LaunchMsaView/components/BlastQuery/CachedBlastResults.test.ts +50 -0
  98. package/src/LaunchMsaView/components/BlastQuery/CachedBlastResults.tsx +23 -3
  99. package/src/LaunchMsaView/components/BlastQuery/MsaAlgorithmSelect.tsx +1 -1
  100. package/src/LaunchMsaView/components/BlastQuery/consts.ts +40 -0
  101. package/src/LaunchMsaView/components/BlastQuery/searchChoiceStorage.test.ts +43 -0
  102. package/src/LaunchMsaView/components/BlastQuery/searchChoiceStorage.ts +64 -0
  103. package/src/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.tsx +1 -1
  104. package/src/LaunchMsaView/components/OrthologQuery/OrthologPanel.tsx +7 -7
  105. package/src/LaunchMsaView/components/SubmitCancelActions.test.tsx +1 -1
  106. package/src/LaunchMsaView/components/useFeatureSequence.ts +1 -1
  107. package/src/LaunchMsaView/detectQueryRow.test.ts +17 -15
  108. package/src/LaunchMsaView/detectQueryRow.ts +34 -23
  109. package/src/LaunchMsaView/useQueryRowName.ts +6 -9
  110. package/src/LaunchMsaViewExtensionPoint/index.test.ts +51 -0
  111. package/src/LaunchMsaViewExtensionPoint/index.ts +21 -6
  112. package/src/MsaViewPanel/afterCreateAutoruns.ts +102 -68
  113. package/src/MsaViewPanel/components/ErrorBoundary.tsx +2 -1
  114. package/src/MsaViewPanel/components/JobLink.tsx +7 -2
  115. package/src/MsaViewPanel/components/LaunchProgress.tsx +80 -0
  116. package/src/MsaViewPanel/components/MsaViewPanel.test.tsx +104 -0
  117. package/src/MsaViewPanel/components/MsaViewPanel.tsx +7 -4
  118. package/src/MsaViewPanel/doLaunchBlast.ts +134 -72
  119. package/src/MsaViewPanel/doLaunchOrthologs.test.ts +43 -28
  120. package/src/MsaViewPanel/doLaunchOrthologs.ts +9 -5
  121. package/src/MsaViewPanel/genomeToMSA.test.ts +37 -0
  122. package/src/MsaViewPanel/genomeToMSA.ts +6 -2
  123. package/src/MsaViewPanel/model.ts +82 -6
  124. package/src/MsaViewPanel/msaDataStore.test.ts +54 -0
  125. package/src/MsaViewPanel/msaDataStore.ts +22 -13
  126. package/src/MsaViewPanel/observeProteinHighlights.test.ts +13 -0
  127. package/src/MsaViewPanel/runLaunch.test.ts +154 -0
  128. package/src/MsaViewPanel/runLaunch.ts +102 -0
  129. package/src/MsaViewPanel/storedData.test.ts +196 -0
  130. package/src/MsaViewPanel/syncGenomeHoverToMsaColumn.test.ts +1 -0
  131. package/src/MsaViewPanel/util.ts +18 -0
  132. package/src/utils/blastCache.test.ts +86 -0
  133. package/src/utils/blastCache.ts +67 -13
  134. package/src/utils/ebiBlast.ts +9 -3
  135. package/src/utils/ebiJobDispatcher.ts +18 -3
  136. package/src/utils/fetch.ts +29 -2
  137. package/src/utils/msa.ts +51 -12
  138. package/src/utils/msaRows.ts +95 -0
  139. package/src/utils/ncbiDomains.ts +4 -2
  140. package/src/utils/ncbiOrthologs.test.ts +25 -0
  141. package/src/utils/ncbiOrthologs.ts +27 -7
  142. package/src/utils/pantherOrthologs.ts +6 -11
  143. package/src/utils/phmmer.ts +178 -0
  144. package/src/utils/poll.ts +8 -1
  145. package/src/utils/taxonomyNames.ts +6 -1
  146. package/src/version.ts +1 -1
  147. package/dist/MsaViewPanel/components/LoadingBLAST.d.ts +0 -6
  148. package/dist/MsaViewPanel/components/LoadingBLAST.js +0 -26
  149. package/src/MsaViewPanel/components/LoadingBLAST.tsx +0 -48
@@ -2,29 +2,68 @@ import { makeId, strip } from '../LaunchMsaView/components/util';
2
2
  import { cleanProteinSequence } from '../LaunchMsaView/util';
3
3
  import { saveBlastResult } from '../utils/blastCache';
4
4
  import { queryEbiBlast } from '../utils/ebiBlast';
5
- import { launchMSA } from '../utils/msa';
5
+ import { launchMSA, launchTree } from '../utils/msa';
6
+ import { buildPhmmerMsa, buildRowMetadata } from '../utils/msaRows';
7
+ import { queryPhmmer } from '../utils/phmmer';
6
8
  import { fetchTaxonomyInfo } from '../utils/taxonomyNames';
7
- export async function doLaunchBlast({ self, }) {
8
- const { blastDatabase, msaAlgorithm, proteinSequence, selectedTranscript } = self.blastParams;
9
- const cleanedSeq = cleanProteinSequence(proteinSequence);
10
- const onProgress = (arg) => {
11
- self.setProgress(arg);
12
- };
9
+ export async function doLaunchBlast({ self, scope, }) {
10
+ // kept whole rather than destructured: the database's type depends on
11
+ // searchProgram, and pulling the two apart loses the link between them
12
+ const params = self.blastParams;
13
+ const { selectedTranscript } = params;
14
+ const cleanedSeq = cleanProteinSequence(params.proteinSequence);
15
+ const { onProgress, onRid, signal } = scope;
16
+ const { msa, tree, treeMetadata, rid } = params.searchProgram === 'phmmer'
17
+ ? await runPhmmer({
18
+ query: cleanedSeq,
19
+ database: params.blastDatabase,
20
+ onProgress,
21
+ onRid,
22
+ signal,
23
+ })
24
+ : await runBlast({
25
+ query: cleanedSeq,
26
+ blastDatabase: params.blastDatabase,
27
+ msaAlgorithm: params.msaAlgorithm,
28
+ onProgress,
29
+ onRid,
30
+ signal,
31
+ });
32
+ const treeMetadataJson = JSON.stringify(treeMetadata);
33
+ await saveBlastResult({
34
+ proteinSequence: cleanedSeq,
35
+ blastDatabase: params.blastDatabase,
36
+ msaAlgorithm: params.msaAlgorithm,
37
+ searchProgram: params.searchProgram,
38
+ msa,
39
+ tree,
40
+ treeMetadata: treeMetadataJson,
41
+ rid,
42
+ geneId: selectedTranscript?.get('parentId'),
43
+ transcriptId: selectedTranscript?.id(),
44
+ transcriptName: selectedTranscript?.get('name') ?? selectedTranscript?.get('id'),
45
+ geneName: selectedTranscript?.get('gene_name') ??
46
+ selectedTranscript?.get('parentId'),
47
+ });
48
+ return { msa, tree, treeMetadata: treeMetadataJson };
49
+ }
50
+ /**
51
+ * BLAST returns each hit already aligned to the query, but pairwise and one hit
52
+ * at a time, so the alignments are stripped back off and every hit is realigned
53
+ * together by a dedicated aligner.
54
+ */
55
+ async function runBlast({ query, blastDatabase, msaAlgorithm, onProgress, onRid, signal, }) {
13
56
  const { hits, rid } = await queryEbiBlast({
14
- query: cleanedSeq,
57
+ query,
15
58
  blastDatabase,
16
59
  onProgress,
17
- // publish the job id before the first poll so the view can link out while
18
- // the job is still running
19
- onRid: r => {
20
- self.setRid(r);
21
- },
60
+ onRid,
61
+ signal,
22
62
  });
23
- self.setProgress('Fetching species taxonomy info...');
24
- const taxids = hits
63
+ onProgress('Fetching species taxonomy info...');
64
+ const taxonomyInfo = await fetchTaxonomyInfo(hits
25
65
  .map(h => h.description[0]?.taxid)
26
- .filter((t) => t !== undefined);
27
- const taxonomyInfo = await fetchTaxonomyInfo(taxids);
66
+ .filter((t) => t !== undefined));
28
67
  const treeMetadata = {};
29
68
  const sequences = hits.map(h => {
30
69
  const desc = h.description[0] ?? {
@@ -33,52 +72,43 @@ export async function doLaunchBlast({ self, }) {
33
72
  sciname: 'unknown',
34
73
  };
35
74
  const rowName = makeId(desc, taxonomyInfo);
36
- const seq = strip(h.hsps[0]?.hseq ?? '');
37
75
  treeMetadata[rowName] = buildRowMetadata(desc, taxonomyInfo);
38
- return `>${rowName}\n${seq}`;
76
+ return `>${rowName}\n${strip(h.hsps[0]?.hseq ?? '')}`;
39
77
  });
40
78
  const result = await launchMSA({
41
79
  algorithm: msaAlgorithm,
42
- sequence: [`>QUERY\n${cleanedSeq}`, ...sequences].join('\n'),
80
+ sequence: [`>QUERY\n${query}`, ...sequences].join('\n'),
43
81
  onProgress,
82
+ signal,
44
83
  });
45
- const treeMetadataJson = JSON.stringify(treeMetadata);
46
- await saveBlastResult({
47
- proteinSequence: cleanedSeq,
48
- blastDatabase,
49
- msaAlgorithm,
50
- msa: result.msa,
51
- tree: result.tree,
52
- treeMetadata: treeMetadataJson,
53
- rid,
54
- geneId: selectedTranscript?.get('parentId'),
55
- transcriptId: selectedTranscript?.id(),
56
- transcriptName: selectedTranscript?.get('name') ?? selectedTranscript?.get('id'),
57
- geneName: selectedTranscript?.get('gene_name') ??
58
- selectedTranscript?.get('parentId'),
84
+ return { ...result, treeMetadata, rid };
85
+ }
86
+ /**
87
+ * phmmer aligns every hit to a profile of the query as it searches, so its own
88
+ * output is the MSA and there is no realignment step — the hits keep the
89
+ * placement HMMER gave them, and the query row is derived from the alignment's
90
+ * match columns rather than being aligned back in afterwards. That leaves no
91
+ * aligner run to take a tree from, so the tree is built from this alignment.
92
+ */
93
+ async function runPhmmer({ query, database, onProgress, onRid, signal, }) {
94
+ const { rows, queryRow, rid } = await queryPhmmer({
95
+ query,
96
+ database,
97
+ onProgress,
98
+ onRid,
99
+ signal,
100
+ });
101
+ onProgress('Fetching species taxonomy info...');
102
+ const taxonomyInfo = await fetchTaxonomyInfo(rows.map(r => r.taxid).filter((t) => t !== undefined));
103
+ const { msa, treeMetadata } = buildPhmmerMsa({
104
+ rows,
105
+ queryRow,
106
+ taxonomyInfo,
59
107
  });
60
108
  return {
61
- ...result,
62
- treeMetadata: treeMetadataJson,
109
+ msa,
110
+ tree: await launchTree({ alignment: msa, onProgress, signal }),
111
+ treeMetadata,
112
+ rid,
63
113
  };
64
114
  }
65
- function buildRowMetadata(desc, taxonomyInfo) {
66
- const metadata = {};
67
- const taxInfo = desc.taxid ? taxonomyInfo.get(desc.taxid) : undefined;
68
- if (taxInfo?.sciname) {
69
- metadata['Scientific name'] = taxInfo.sciname;
70
- }
71
- if (taxInfo?.commonName) {
72
- metadata['Common name'] = taxInfo.commonName;
73
- }
74
- if (desc.accession) {
75
- metadata.Accession = desc.accession;
76
- }
77
- if (desc.id) {
78
- metadata.ID = desc.id;
79
- }
80
- if (desc.title) {
81
- metadata.Description = desc.title;
82
- }
83
- return metadata;
84
- }
@@ -1,4 +1,5 @@
1
1
  import type { JBrowsePluginMsaViewModel } from './model';
2
+ import type { LaunchScope } from './runLaunch';
2
3
  /**
3
4
  * The no-search-job alternative to doLaunchBlast.
4
5
  *
@@ -16,8 +17,9 @@ import type { JBrowsePluginMsaViewModel } from './model';
16
17
  * would silently break the genome<->MSA linkage. The query species is therefore
17
18
  * excluded from the ortholog set rather than appearing twice.
18
19
  */
19
- export declare function doLaunchOrthologs({ self, }: {
20
+ export declare function doLaunchOrthologs({ self, scope, }: {
20
21
  self: JBrowsePluginMsaViewModel;
22
+ scope: LaunchScope;
21
23
  }): Promise<{
22
24
  treeMetadata: string;
23
25
  msa: string;
@@ -20,11 +20,9 @@ import { fetchTaxonomyInfo } from '../utils/taxonomyNames';
20
20
  * would silently break the genome<->MSA linkage. The query species is therefore
21
21
  * excluded from the ortholog set rather than appearing twice.
22
22
  */
23
- export async function doLaunchOrthologs({ self, }) {
23
+ export async function doLaunchOrthologs({ self, scope, }) {
24
24
  const { taxId, taxa, maxSpecies, geneCandidates, msaAlgorithm, proteinSequence, source = 'ncbi', } = self.orthologParams;
25
- const onProgress = (arg) => {
26
- self.setProgress(arg);
27
- };
25
+ const { onProgress, act, signal } = scope;
28
26
  const request = {
29
27
  taxId,
30
28
  geneCandidates,
@@ -62,7 +60,9 @@ export async function doLaunchOrthologs({ self, }) {
62
60
  // token, and a collision would silently point the coordinate mapping at
63
61
  // another animal's row.
64
62
  const queryLabel = await queryRowLabel(taxId, rows);
65
- self.setQuerySeqName(queryLabel);
63
+ act(() => {
64
+ self.setQuerySeqName(queryLabel);
65
+ });
66
66
  const treeMetadata = {
67
67
  [queryLabel]: buildQueryMetadata(self, geneId, cleanedSeq, representative),
68
68
  };
@@ -76,6 +76,7 @@ export async function doLaunchOrthologs({ self, }) {
76
76
  ...rows.map(r => `>${r.label}\n${r.sequence}`),
77
77
  ].join('\n'),
78
78
  onProgress,
79
+ signal,
79
80
  });
80
81
  return {
81
82
  ...result,
@@ -1,9 +1,9 @@
1
1
  import { beforeEach, describe, expect, test, vi } from 'vitest';
2
- import { doLaunchOrthologs } from './doLaunchOrthologs';
3
2
  import { launchMSA } from '../utils/msa';
4
3
  import { defaultMaxSpecies, fetchOrthologRows, fetchProteinForGene, resolveGeneId, } from '../utils/ncbiOrthologs';
5
4
  import { fetchPantherOrthologs } from '../utils/pantherOrthologs';
6
5
  import { fetchTaxonomyInfo } from '../utils/taxonomyNames';
6
+ import { doLaunchOrthologs } from './doLaunchOrthologs';
7
7
  // Every network call is mocked and nothing else is. What is under test is the
8
8
  // argument shaping either side of those calls -- which species get asked for,
9
9
  // what becomes the QUERY row, and whether the row earns the Accession that
@@ -33,10 +33,24 @@ const setQuerySeqName = vi.fn();
33
33
  function makeModel(orthologParams) {
34
34
  return {
35
35
  orthologParams,
36
- setProgress: () => { },
37
36
  setQuerySeqName,
38
37
  };
39
38
  }
39
+ // a scope that never cancels, so these tests see the launch's own behaviour --
40
+ // what the scope does when it IS cancelled is runLaunch.test.ts's subject
41
+ function launch({ self }) {
42
+ return doLaunchOrthologs({
43
+ self,
44
+ scope: {
45
+ signal: new AbortController().signal,
46
+ act: fn => {
47
+ fn();
48
+ },
49
+ onProgress: () => { },
50
+ onRid: () => { },
51
+ },
52
+ });
53
+ }
40
54
  function params(extra = {}) {
41
55
  return {
42
56
  taxId: HUMAN,
@@ -79,11 +93,11 @@ beforeEach(() => {
79
93
  });
80
94
  describe('which species become rows', () => {
81
95
  test('omitted taxa asks for no restriction at all, which is every ortholog NCBI has', async () => {
82
- await doLaunchOrthologs({ self: makeModel(params()) });
96
+ await launch({ self: makeModel(params()) });
83
97
  expect(rowRequest().taxa).toBeUndefined();
84
98
  });
85
99
  test('given taxa is taken as written', async () => {
86
- await doLaunchOrthologs({
100
+ await launch({
87
101
  self: makeModel(params({ taxa: [HUMAN, 10090, 9615] })),
88
102
  });
89
103
  expect(rowRequest().taxa).toEqual([9606, 9615, 10090]);
@@ -92,20 +106,20 @@ describe('which species become rows', () => {
92
106
  // and "the query row already covers this one" stay separable -- an unrestricted
93
107
  // launch still has to drop the query species.
94
108
  test('the query species is excluded whether or not taxa was given', async () => {
95
- await doLaunchOrthologs({ self: makeModel(params()) });
109
+ await launch({ self: makeModel(params()) });
96
110
  expect(rowRequest().exclude).toBe(HUMAN);
97
111
  vi.clearAllMocks();
98
112
  mockResolveGeneId.mockResolvedValue({ geneId: GENE_ID, matched: 'NLRP1' });
99
113
  mockFetchProtein.mockResolvedValue(REPRESENTATIVE);
100
114
  mockFetchRows.mockResolvedValue([]);
101
115
  mockLaunchMSA.mockResolvedValue({ msa: '', tree: '' });
102
- await doLaunchOrthologs({
116
+ await launch({
103
117
  self: makeModel(params({ taxa: [HUMAN, 10090] })),
104
118
  });
105
119
  expect(rowRequest().exclude).toBe(HUMAN);
106
120
  });
107
121
  test('an empty list is a request for no rows, not a request for all of them', async () => {
108
- await doLaunchOrthologs({ self: makeModel(params({ taxa: [] })) });
122
+ await launch({ self: makeModel(params({ taxa: [] })) });
109
123
  expect(rowRequest().taxa).toEqual([]);
110
124
  });
111
125
  });
@@ -114,11 +128,11 @@ describe('which species become rows', () => {
114
128
  // second a row.
115
129
  describe('the row cap', () => {
116
130
  test('is passed through when given', async () => {
117
- await doLaunchOrthologs({ self: makeModel(params({ maxSpecies: 12 })) });
131
+ await launch({ self: makeModel(params({ maxSpecies: 12 })) });
118
132
  expect(rowRequest().limit).toBe(12);
119
133
  });
120
134
  test('omitted leaves the default to fetchOrthologGenes rather than sending Infinity', async () => {
121
- await doLaunchOrthologs({ self: makeModel(params()) });
135
+ await launch({ self: makeModel(params()) });
122
136
  expect(rowRequest().limit).toBeUndefined();
123
137
  expect(defaultMaxSpecies).toBeGreaterThan(2);
124
138
  });
@@ -129,7 +143,7 @@ describe('the row cap', () => {
129
143
  // model's querySeqName and the header have to be the same string.
130
144
  describe('the query row name', () => {
131
145
  test('is the species, marked, rather than a bare QUERY among named rows', async () => {
132
- await doLaunchOrthologs({ self: makeModel(params()) });
146
+ await launch({ self: makeModel(params()) });
133
147
  expect(queryRowName()).toBe('human_query');
134
148
  expect(setQuerySeqName).toHaveBeenCalledWith('human_query');
135
149
  });
@@ -137,41 +151,41 @@ describe('the query row name', () => {
137
151
  mockFetchRows.mockResolvedValue([
138
152
  { label: 'human_query', sequence: 'MM' },
139
153
  ]);
140
- await doLaunchOrthologs({ self: makeModel(params()) });
154
+ await launch({ self: makeModel(params()) });
141
155
  expect(queryRowName()).toBe('human_query_2');
142
156
  expect(setQuerySeqName).toHaveBeenCalledWith('human_query_2');
143
157
  });
144
158
  test('falls back rather than throwing when NCBI cannot name the taxon', async () => {
145
159
  vi.spyOn(console, 'warn').mockImplementation(() => { });
146
160
  mockFetchTaxonomy.mockRejectedValue(new Error('429'));
147
- await doLaunchOrthologs({ self: makeModel(params()) });
161
+ await launch({ self: makeModel(params()) });
148
162
  expect(queryRowName()).toBe('query_query');
149
163
  });
150
164
  test('the metadata that drives the domain overlay is keyed to that same name', async () => {
151
- const result = await doLaunchOrthologs({ self: makeModel(params()) });
165
+ const result = await launch({ self: makeModel(params()) });
152
166
  expect(Object.keys(JSON.parse(result.treeMetadata))).toContain('human_query');
153
167
  });
154
168
  });
155
169
  describe('the query row sequence', () => {
156
170
  test('omitted proteinSequence falls back to the representative protein', async () => {
157
- await doLaunchOrthologs({ self: makeModel(params()) });
171
+ await launch({ self: makeModel(params()) });
158
172
  expect(queryRowSent()).toBe(REPRESENTATIVE.sequence);
159
173
  });
160
174
  test('a supplied sequence is used, and is cleaned first', async () => {
161
- await doLaunchOrthologs({
175
+ await launch({
162
176
  self: makeModel(params({ proteinSequence: 'MAGG*AWGR&' })),
163
177
  });
164
178
  expect(queryRowSent()).toBe('MAGGAWGR');
165
179
  });
166
180
  test('throws when neither a sequence nor a representative is available', async () => {
167
181
  mockFetchProtein.mockResolvedValue(undefined);
168
- await expect(doLaunchOrthologs({ self: makeModel(params()) })).rejects.toThrow(/No query protein/);
182
+ await expect(launch({ self: makeModel(params()) })).rejects.toThrow(/No query protein/);
169
183
  expect(mockLaunchMSA).not.toHaveBeenCalled();
170
184
  });
171
185
  test('a failed representative lookup does not take down a launch that brought its own sequence', async () => {
172
186
  vi.spyOn(console, 'warn').mockImplementation(() => { });
173
187
  mockFetchProtein.mockRejectedValue(new Error('429'));
174
- await doLaunchOrthologs({
188
+ await launch({
175
189
  self: makeModel(params({ proteinSequence: REPRESENTATIVE.sequence })),
176
190
  });
177
191
  expect(queryRowSent()).toBe(REPRESENTATIVE.sequence);
@@ -183,14 +197,14 @@ describe('the query row sequence', () => {
183
197
  // here, in both directions.
184
198
  describe('the Accession that drives the domain overlay', () => {
185
199
  test('is attached when the query row IS the representative protein', async () => {
186
- const result = await doLaunchOrthologs({ self: makeModel(params()) });
200
+ const result = await launch({ self: makeModel(params()) });
187
201
  expect(queryMetadata(result)).toMatchObject({
188
202
  'Gene ID': GENE_ID,
189
203
  Accession: REPRESENTATIVE.accession,
190
204
  });
191
205
  });
192
206
  test('is withheld from a non-representative isoform', async () => {
193
- const result = await doLaunchOrthologs({
207
+ const result = await launch({
194
208
  self: makeModel(params({ proteinSequence: 'MDIFFERENTISOFORM' })),
195
209
  });
196
210
  expect(queryMetadata(result).Accession).toBeUndefined();
@@ -198,7 +212,7 @@ describe('the Accession that drives the domain overlay', () => {
198
212
  test('is withheld when the representative lookup failed', async () => {
199
213
  vi.spyOn(console, 'warn').mockImplementation(() => { });
200
214
  mockFetchProtein.mockRejectedValue(new Error('429'));
201
- const result = await doLaunchOrthologs({
215
+ const result = await launch({
202
216
  self: makeModel(params({ proteinSequence: REPRESENTATIVE.sequence })),
203
217
  });
204
218
  expect(queryMetadata(result).Accession).toBeUndefined();
@@ -235,12 +249,12 @@ describe('the PANTHER source', () => {
235
249
  mockFetchTaxonomy.mockResolvedValue(new Map([[YEAST, { sciname: 'Saccharomyces cerevisiae' }]]));
236
250
  });
237
251
  test('source omitted is NCBI, so an old launch never reaches PANTHER', async () => {
238
- await doLaunchOrthologs({ self: makeModel(params()) });
252
+ await launch({ self: makeModel(params()) });
239
253
  expect(mockFetchPanther).not.toHaveBeenCalled();
240
254
  expect(mockResolveGeneId).toHaveBeenCalled();
241
255
  });
242
256
  test('source panther asks PANTHER with the same species semantics, and skips NCBI', async () => {
243
- await doLaunchOrthologs({
257
+ await launch({
244
258
  self: makeModel({
245
259
  taxId: YEAST,
246
260
  source: 'panther',
@@ -260,7 +274,7 @@ describe('the PANTHER source', () => {
260
274
  expect(limit).toBe(7);
261
275
  });
262
276
  test("the query row is PANTHER's own entry for the gene when no sequence was supplied, and carries its UniProt accession for the domain overlay", async () => {
263
- const result = await doLaunchOrthologs({
277
+ const result = await launch({
264
278
  self: makeModel({
265
279
  taxId: YEAST,
266
280
  source: 'panther',
@@ -280,7 +294,7 @@ describe('the PANTHER source', () => {
280
294
  });
281
295
  });
282
296
  test('a supplied sequence still wins, and a different isoform earns no Accession', async () => {
283
- const result = await doLaunchOrthologs({
297
+ const result = await launch({
284
298
  self: makeModel({
285
299
  taxId: YEAST,
286
300
  source: 'panther',
@@ -294,7 +308,7 @@ describe('the PANTHER source', () => {
294
308
  });
295
309
  test('names PANTHER when it has no protein for the query row', async () => {
296
310
  mockFetchPanther.mockResolvedValue({ ...found, query: undefined });
297
- await expect(doLaunchOrthologs({
311
+ await expect(launch({
298
312
  self: makeModel({
299
313
  taxId: YEAST,
300
314
  source: 'panther',
@@ -1,9 +1,11 @@
1
1
  import { getSession } from '@jbrowse/core/util';
2
- import { hasHoverPosition } from './util';
2
+ import { hasHoverPosition, hasQueryRow } from './util';
3
3
  export function genomeToMSA({ model }) {
4
4
  const { hovered } = getSession(model);
5
5
  const { querySeqName, transcriptToMsaMap, connectedView, mafRegion } = model;
6
- if (!connectedView?.initialized || !hasHoverPosition(hovered)) {
6
+ if (!connectedView?.initialized ||
7
+ !hasHoverPosition(hovered) ||
8
+ !hasQueryRow(model)) {
7
9
  return undefined;
8
10
  }
9
11
  const { coord, refName } = hovered.hoverPosition;
@@ -19,6 +19,7 @@ describe('genomeToMSA', () => {
19
19
  });
20
20
  const model = {
21
21
  querySeqName: 'hg38.chr1',
22
+ rows: [['hg38.chr1', 'ACGTACGTAC']],
22
23
  transcriptToMsaMap: undefined,
23
24
  mafRegion: {
24
25
  refName: 'chr1',
@@ -38,6 +39,7 @@ describe('genomeToMSA', () => {
38
39
  });
39
40
  const model = {
40
41
  querySeqName: 'hg38.chr1',
42
+ rows: [['hg38.chr1', 'ACGTACGTAC']],
41
43
  transcriptToMsaMap: undefined,
42
44
  mafRegion: {
43
45
  refName: 'chr1',
@@ -62,6 +64,7 @@ describe('genomeToMSA', () => {
62
64
  const mockSeqPosToVisibleCol = vi.fn().mockReturnValue(5);
63
65
  const model = {
64
66
  querySeqName: 'hg38.chr1',
67
+ rows: [['hg38.chr1', 'ACGTACGTAC']],
65
68
  transcriptToMsaMap: undefined,
66
69
  mafRegion: {
67
70
  refName: 'chr1',
@@ -90,6 +93,7 @@ describe('genomeToMSA', () => {
90
93
  });
91
94
  const model = {
92
95
  querySeqName: 'hg38.chr1',
96
+ rows: [['hg38.chr1', 'ACGTACGTAC']],
93
97
  transcriptToMsaMap: undefined,
94
98
  mafRegion: {
95
99
  refName: 'chr1',
@@ -116,6 +120,7 @@ describe('genomeToMSA', () => {
116
120
  });
117
121
  const model = {
118
122
  querySeqName: 'hg38.chr1',
123
+ rows: [['hg38.chr1', 'ACGTACGTAC']],
119
124
  transcriptToMsaMap: undefined,
120
125
  mafRegion: {
121
126
  refName: 'chr1',
@@ -142,6 +147,7 @@ describe('genomeToMSA', () => {
142
147
  });
143
148
  const model = {
144
149
  querySeqName: 'hg38.chr1',
150
+ rows: [['hg38.chr1', 'ACGTACGTAC']],
145
151
  transcriptToMsaMap: undefined,
146
152
  mafRegion: {
147
153
  refName: 'chr1',
@@ -167,6 +173,7 @@ describe('genomeToMSA', () => {
167
173
  });
168
174
  const model = {
169
175
  querySeqName: 'hg38.chr1',
176
+ rows: [['hg38.chr1', 'ACGTACGTAC']],
170
177
  transcriptToMsaMap: undefined,
171
178
  mafRegion: {
172
179
  refName: 'chr1',
@@ -195,6 +202,7 @@ describe('genomeToMSA', () => {
195
202
  const mockSeqPosToVisibleCol = vi.fn().mockReturnValue(10);
196
203
  const model = {
197
204
  querySeqName: 'QUERY',
205
+ rows: [['QUERY', 'MKVLTAEEK']],
198
206
  transcriptToMsaMap: {
199
207
  refName: 'chr1',
200
208
  // g2p is keyed by 0-based genome position, the hover coord is 1-based
@@ -220,6 +228,7 @@ describe('genomeToMSA', () => {
220
228
  const mockSeqPosToVisibleCol = vi.fn();
221
229
  const model = {
222
230
  querySeqName: 'QUERY',
231
+ rows: [['QUERY', 'MKVLTAEEK']],
223
232
  transcriptToMsaMap: {
224
233
  refName: 'chr1',
225
234
  g2p: { 1004: 10 },
@@ -240,6 +249,7 @@ describe('genomeToMSA', () => {
240
249
  });
241
250
  const model = {
242
251
  querySeqName: 'QUERY',
252
+ rows: [['QUERY', 'MKVLTAEEK']],
243
253
  transcriptToMsaMap: {
244
254
  refName: 'chr1',
245
255
  g2p: { 1000: 0 }, // No entry for 1004
@@ -261,6 +271,7 @@ describe('genomeToMSA', () => {
261
271
  });
262
272
  const model = {
263
273
  querySeqName: 'QUERY',
274
+ rows: [['QUERY', 'MKVLTAEEK']],
264
275
  transcriptToMsaMap: undefined,
265
276
  mafRegion: undefined,
266
277
  connectedView: { initialized: true },
@@ -269,4 +280,27 @@ describe('genomeToMSA', () => {
269
280
  const result = genomeToMSA({ model });
270
281
  expect(result).toBeUndefined();
271
282
  });
283
+ // seqPosToVisibleCol answers 0 for a row name it does not know, so without a
284
+ // guard an alignment whose query row is missing -- the default 'QUERY' on an
285
+ // uploaded file, or the empty name the manual panel leaves when it matches
286
+ // nothing -- lights column 0 on every genome hover
287
+ test('returns undefined when querySeqName names no row here', () => {
288
+ mockGetSession.mockReturnValue({
289
+ hovered: {
290
+ hoverFeature: {},
291
+ hoverPosition: { coord: 1005, refName: 'chr1' },
292
+ },
293
+ });
294
+ const seqPosToVisibleCol = vi.fn(() => 0);
295
+ const model = {
296
+ querySeqName: 'QUERY',
297
+ rows: [['some_other_row', 'MKVLTAEEK']],
298
+ transcriptToMsaMap: { refName: 'chr1', g2p: { 1004: 3 } },
299
+ mafRegion: undefined,
300
+ connectedView: { initialized: true },
301
+ seqPosToVisibleCol,
302
+ };
303
+ expect(genomeToMSA({ model })).toBeUndefined();
304
+ expect(seqPosToVisibleCol).not.toHaveBeenCalled();
305
+ });
272
306
  });