sequenceserver 2.1.0 → 3.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of sequenceserver might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/COPYRIGHT.txt +1 -1
- data/bin/sequenceserver +10 -3
- data/lib/sequenceserver/blast/error.rb +53 -0
- data/lib/sequenceserver/blast/formatter.rb +13 -4
- data/lib/sequenceserver/blast/job.rb +2 -43
- data/lib/sequenceserver/blast/report.rb +33 -3
- data/lib/sequenceserver/config.rb +4 -1
- data/lib/sequenceserver/job.rb +21 -11
- data/lib/sequenceserver/makeblastdb-modified-with-cache.rb +345 -0
- data/lib/sequenceserver/makeblastdb.rb +97 -75
- data/lib/sequenceserver/pool.rb +1 -1
- data/lib/sequenceserver/report.rb +1 -5
- data/lib/sequenceserver/routes.rb +52 -5
- data/lib/sequenceserver/server.rb +1 -1
- data/lib/sequenceserver/sys.rb +1 -1
- data/lib/sequenceserver/version.rb +1 -1
- data/lib/sequenceserver.rb +11 -2
- data/public/404.html +27 -0
- data/public/config.js +0 -6
- data/public/css/grapher.css +1 -1
- data/public/css/sequenceserver.css +22 -11
- data/public/css/sequenceserver.min.css +2 -2
- data/public/js/circos.js +7 -3
- data/public/js/dnd.js +3 -3
- data/public/js/fastq_to_fasta.js +35 -0
- data/public/js/form.js +30 -11
- data/public/js/grapher.js +123 -113
- data/public/js/hit.js +8 -2
- data/public/js/hits_overview.js +4 -1
- data/public/js/jquery_world.js +0 -1
- data/public/js/kablammo.js +4 -0
- data/public/js/length_distribution.js +5 -1
- data/public/js/null_plugins/download_links.js +7 -0
- data/public/js/null_plugins/hit_buttons.js +11 -0
- data/public/js/null_plugins/report_plugins.js +18 -0
- data/public/js/query.js +26 -6
- data/public/js/report.js +92 -22
- data/public/js/search.js +0 -8
- data/public/js/sidebar.js +11 -1
- data/public/js/tests/advanced_parameters.spec.js +36 -0
- data/public/js/tests/mock_data/sequences.js +49 -0
- data/public/js/tests/report.spec.js +62 -6
- data/public/js/tests/search_query.spec.js +45 -19
- data/public/js/visualisation_helpers.js +1 -1
- data/public/sequenceserver-report.min.js +76 -42
- data/public/sequenceserver-search.min.js +34 -33
- data/views/layout.erb +9 -12
- metadata +34 -23
data/public/js/report.js
CHANGED
@@ -8,15 +8,14 @@ import { ReportQuery } from './query';
|
|
8
8
|
import Hit from './hit';
|
9
9
|
import HSP from './hsp';
|
10
10
|
import AlignmentExporter from './alignment_exporter';
|
11
|
-
|
12
|
-
|
13
|
-
|
11
|
+
import ReportPlugins from 'report_plugins';
|
14
12
|
|
15
13
|
/**
|
16
14
|
* Renders entire report.
|
17
15
|
*
|
18
16
|
* Composed of Query and Sidebar components.
|
19
17
|
*/
|
18
|
+
|
20
19
|
class Report extends Component {
|
21
20
|
constructor(props) {
|
22
21
|
super(props);
|
@@ -28,6 +27,8 @@ class Report extends Component {
|
|
28
27
|
this.nextHSP = 0;
|
29
28
|
this.maxHSPs = 3; // max HSPs to render in a cycle
|
30
29
|
this.state = {
|
30
|
+
user_warning: null,
|
31
|
+
download_links: [],
|
31
32
|
search_id: '',
|
32
33
|
seqserv_version: '',
|
33
34
|
program: '',
|
@@ -45,16 +46,21 @@ class Report extends Component {
|
|
45
46
|
this.prepareAlignmentOfSelectedHits = this.prepareAlignmentOfSelectedHits.bind(this);
|
46
47
|
this.prepareAlignmentOfAllHits = this.prepareAlignmentOfAllHits.bind(this);
|
47
48
|
this.setStateFromJSON = this.setStateFromJSON.bind(this);
|
49
|
+
this.plugins = new ReportPlugins(this);
|
48
50
|
}
|
51
|
+
|
49
52
|
/**
|
50
53
|
* Fetch results.
|
51
54
|
*/
|
52
55
|
fetchResults() {
|
53
|
-
|
54
|
-
|
56
|
+
const path = location.pathname + '.json' + location.search;
|
57
|
+
this.pollPeriodically(path, this.setStateFromJSON, this.props.showErrorModal);
|
58
|
+
}
|
55
59
|
|
60
|
+
pollPeriodically(path, callback, errCallback) {
|
61
|
+
var intervals = [200, 400, 800, 1200, 2000, 3000, 5000];
|
56
62
|
function poll() {
|
57
|
-
$.getJSON(
|
63
|
+
$.getJSON(path).complete(function (jqXHR) {
|
58
64
|
switch (jqXHR.status) {
|
59
65
|
case 202:
|
60
66
|
var interval;
|
@@ -66,12 +72,12 @@ class Report extends Component {
|
|
66
72
|
setTimeout(poll, interval);
|
67
73
|
break;
|
68
74
|
case 200:
|
69
|
-
|
75
|
+
callback(jqXHR.responseJSON);
|
70
76
|
break;
|
71
|
-
case 404:
|
72
77
|
case 400:
|
78
|
+
case 422:
|
73
79
|
case 500:
|
74
|
-
|
80
|
+
errCallback(jqXHR.responseJSON);
|
75
81
|
break;
|
76
82
|
}
|
77
83
|
});
|
@@ -86,8 +92,13 @@ class Report extends Component {
|
|
86
92
|
setStateFromJSON(responseJSON) {
|
87
93
|
this.lastTimeStamp = Date.now();
|
88
94
|
// the callback prepares the download link for all alignments
|
89
|
-
|
95
|
+
if (responseJSON.user_warning == 'LARGE_RESULT') {
|
96
|
+
this.setState({user_warning: responseJSON.user_warning, download_links: responseJSON.download_links});
|
97
|
+
} else {
|
98
|
+
this.setState(responseJSON, this.prepareAlignmentOfAllHits);
|
99
|
+
}
|
90
100
|
}
|
101
|
+
|
91
102
|
/**
|
92
103
|
* Called as soon as the page has loaded and the user sees the loading spinner.
|
93
104
|
* We use this opportunity to setup services that make use of delegated events
|
@@ -95,6 +106,7 @@ class Report extends Component {
|
|
95
106
|
*/
|
96
107
|
componentDidMount() {
|
97
108
|
this.fetchResults();
|
109
|
+
this.plugins.init();
|
98
110
|
// This sets up an event handler which enables users to select text from
|
99
111
|
// hit header without collapsing the hit.
|
100
112
|
this.preventCollapseOnSelection();
|
@@ -107,9 +119,9 @@ class Report extends Component {
|
|
107
119
|
* and circos would have been rendered at this point. At this stage we kick
|
108
120
|
* start iteratively adding 1 HSP to the page every 25 milli-seconds.
|
109
121
|
*/
|
110
|
-
componentDidUpdate() {
|
111
|
-
|
112
|
-
console.log((Date.now() - this.lastTimeStamp) / 1000);
|
122
|
+
componentDidUpdate(prevProps, prevState) {
|
123
|
+
// Log to console how long the last update take?
|
124
|
+
// console.log((Date.now() - this.lastTimeStamp) / 1000);
|
113
125
|
|
114
126
|
// Lock sidebar in its position on the first update.
|
115
127
|
if (this.nextQuery == 0 && this.nextHit == 0 && this.nextHSP == 0) {
|
@@ -125,6 +137,8 @@ class Report extends Component {
|
|
125
137
|
} else {
|
126
138
|
this.componentFinishedUpdating();
|
127
139
|
}
|
140
|
+
|
141
|
+
this.plugins.componentDidUpdate(prevProps, prevState);
|
128
142
|
}
|
129
143
|
|
130
144
|
/**
|
@@ -135,13 +149,14 @@ class Report extends Component {
|
|
135
149
|
var numHSPsProcessed = 0;
|
136
150
|
while (this.nextQuery < this.state.queries.length) {
|
137
151
|
var query = this.state.queries[this.nextQuery];
|
152
|
+
|
138
153
|
// We may see a query multiple times during rendering because only
|
139
|
-
// 3 hsps
|
154
|
+
// 3 hsps are rendered in each cycle, but we want to create the
|
140
155
|
// corresponding Query component only the first time we see it.
|
141
156
|
if (this.nextHit == 0 && this.nextHSP == 0) {
|
142
157
|
results.push(
|
143
158
|
<ReportQuery
|
144
|
-
key={'Query_' + query.
|
159
|
+
key={'Query_' + query.id}
|
145
160
|
query={query}
|
146
161
|
program={this.state.program}
|
147
162
|
querydb={this.state.querydb}
|
@@ -151,6 +166,8 @@ class Report extends Component {
|
|
151
166
|
veryBig={this.state.veryBig}
|
152
167
|
/>
|
153
168
|
);
|
169
|
+
|
170
|
+
results.push(...this.plugins.queryResults(query));
|
154
171
|
}
|
155
172
|
|
156
173
|
while (this.nextHit < query.hits.length) {
|
@@ -185,11 +202,11 @@ class Report extends Component {
|
|
185
202
|
<HSP
|
186
203
|
key={
|
187
204
|
'Query_' +
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
205
|
+
query.number +
|
206
|
+
'_Hit_' +
|
207
|
+
hit.number +
|
208
|
+
'_HSP_' +
|
209
|
+
hsp.number
|
193
210
|
}
|
194
211
|
query={query}
|
195
212
|
hit={hit}
|
@@ -256,6 +273,9 @@ class Report extends Component {
|
|
256
273
|
<br />
|
257
274
|
You can bookmark the page and come back to it later or share the
|
258
275
|
link with someone.
|
276
|
+
<br />
|
277
|
+
<br />
|
278
|
+
{ process.env.targetEnv === 'cloud' && <b>If the job takes more than 10 minutes to complete, we will send you an email upon completion.</b> }
|
259
279
|
</p>
|
260
280
|
</div>
|
261
281
|
</div>
|
@@ -286,6 +306,40 @@ class Report extends Component {
|
|
286
306
|
);
|
287
307
|
}
|
288
308
|
|
309
|
+
|
310
|
+
warningJSX() {
|
311
|
+
return(
|
312
|
+
<div className="container">
|
313
|
+
<div className="row">
|
314
|
+
<div className="col-md-6 col-md-offset-3 text-center">
|
315
|
+
<h1>
|
316
|
+
<i className="fa fa-exclamation-triangle"></i> Warning
|
317
|
+
</h1>
|
318
|
+
<p>
|
319
|
+
<br />
|
320
|
+
The BLAST result might be too large to load in the browser. If you have a powerful machine you can try loading the results anyway. Otherwise, you can download the results and view them locally.
|
321
|
+
</p>
|
322
|
+
<br />
|
323
|
+
<p>
|
324
|
+
{this.state.download_links.map((link, index) => {
|
325
|
+
return (
|
326
|
+
<a href={link.url} className="btn btn-secondary" key={'download_link_' + index} >
|
327
|
+
{link.name}
|
328
|
+
</a>
|
329
|
+
);
|
330
|
+
})}
|
331
|
+
</p>
|
332
|
+
<br />
|
333
|
+
<p>
|
334
|
+
<a href={location.pathname + '?bypass_file_size_warning=true'} className="btn btn-primary">
|
335
|
+
View results in browser anyway
|
336
|
+
</a>
|
337
|
+
</p>
|
338
|
+
</div>
|
339
|
+
</div>
|
340
|
+
</div>
|
341
|
+
);
|
342
|
+
}
|
289
343
|
/**
|
290
344
|
* Renders report overview.
|
291
345
|
*/
|
@@ -350,6 +404,15 @@ class Report extends Component {
|
|
350
404
|
return this.state.queries.length >= 1;
|
351
405
|
}
|
352
406
|
|
407
|
+
/**
|
408
|
+
* Indicates the response contains a warning message for the user
|
409
|
+
* in which case we should not render the results and render the
|
410
|
+
* warning instead.
|
411
|
+
**/
|
412
|
+
isUserWarningPresent() {
|
413
|
+
return this.state.user_warning;
|
414
|
+
}
|
415
|
+
|
353
416
|
/**
|
354
417
|
* Returns true if we have at least one hit.
|
355
418
|
*/
|
@@ -403,7 +466,7 @@ class Report extends Component {
|
|
403
466
|
toggleTable() {
|
404
467
|
$('body').on(
|
405
468
|
'mousedown',
|
406
|
-
'.resultn
|
469
|
+
'.resultn .caption[data-toggle="collapse"]',
|
407
470
|
function (event) {
|
408
471
|
var $this = $(this);
|
409
472
|
$this.on('mouseup mousemove', function handler(event) {
|
@@ -461,6 +524,7 @@ class Report extends Component {
|
|
461
524
|
} else {
|
462
525
|
$hit.removeClass('glow');
|
463
526
|
$hit.next('.hsp').removeClass('glow');
|
527
|
+
$('.download-fasta-of-selected').attr('href', '#').removeAttr('download');
|
464
528
|
}
|
465
529
|
|
466
530
|
var $a = $('.download-fasta-of-selected');
|
@@ -539,7 +603,13 @@ class Report extends Component {
|
|
539
603
|
}
|
540
604
|
|
541
605
|
render() {
|
542
|
-
|
606
|
+
if (this.isUserWarningPresent()) {
|
607
|
+
return this.warningJSX();
|
608
|
+
} else if (this.isResultAvailable()) {
|
609
|
+
return this.resultsJSX();
|
610
|
+
} else {
|
611
|
+
return this.loadingJSX();
|
612
|
+
}
|
543
613
|
}
|
544
614
|
}
|
545
615
|
|
data/public/js/search.js
CHANGED
@@ -3,14 +3,6 @@ import React, { Component } from "react";
|
|
3
3
|
import { createRoot } from "react-dom/client";
|
4
4
|
import { DnD } from "./dnd";
|
5
5
|
import { Form } from "./form";
|
6
|
-
/**
|
7
|
-
* Load necessary polyfills.
|
8
|
-
*/
|
9
|
-
$.webshims.setOptions(
|
10
|
-
"basePath",
|
11
|
-
"/vendor/npm/webshim@1.15.8/js-webshim/minified/shims/"
|
12
|
-
);
|
13
|
-
$.webshims.polyfill("forms");
|
14
6
|
|
15
7
|
/**
|
16
8
|
* Clear sessionStorage on reload.
|
data/public/js/sidebar.js
CHANGED
@@ -4,7 +4,7 @@ import _ from 'underscore';
|
|
4
4
|
import downloadFASTA from './download_fasta';
|
5
5
|
import asMailtoHref from './mailto';
|
6
6
|
import CloudShareModal from './cloud_share_modal';
|
7
|
-
|
7
|
+
import DownloadLinks from 'download_links';
|
8
8
|
/**
|
9
9
|
* checks whether code is being run by jest
|
10
10
|
*/
|
@@ -172,6 +172,9 @@ export default class extends Component {
|
|
172
172
|
var sequence_ids = $('.hit-links :checkbox:checked').map(function () {
|
173
173
|
return this.value;
|
174
174
|
}).get();
|
175
|
+
if (sequence_ids.length === 0) {
|
176
|
+
return false;
|
177
|
+
}
|
175
178
|
var database_ids = _.map(this.props.data.querydb, _.iteratee('id'));
|
176
179
|
downloadFASTA(sequence_ids, database_ids);
|
177
180
|
return false;
|
@@ -347,6 +350,7 @@ export default class extends Component {
|
|
347
350
|
</a>
|
348
351
|
</li>
|
349
352
|
}
|
353
|
+
<DownloadLinks imported_xml={this.props.data.imported_xml} search_id={this.props.data.search_id} />
|
350
354
|
</ul>
|
351
355
|
</div>
|
352
356
|
);
|
@@ -406,6 +410,12 @@ export default class extends Component {
|
|
406
410
|
{this.topPanelJSX()}
|
407
411
|
{this.downloadsPanelJSX()}
|
408
412
|
{this.sharingPanelJSX()}
|
413
|
+
<div className="referral-panel">
|
414
|
+
<div className="section-header-sidebar">
|
415
|
+
<h4>Recommend SequenceServer</h4>
|
416
|
+
<p><a href="https://sequenceserver.com/referral-program" target="_blank">Earn up to $100 per signup</a></p>
|
417
|
+
</div>
|
418
|
+
</div>
|
409
419
|
</div>
|
410
420
|
);
|
411
421
|
}
|
@@ -0,0 +1,36 @@
|
|
1
|
+
/* eslint-disable no-unused-vars */
|
2
|
+
/* eslint-disable no-undef */
|
3
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
4
|
+
import { Form } from '../form';
|
5
|
+
import { AMINO_ACID_SEQUENCE } from './mock_data/sequences';
|
6
|
+
import data from './mock_data/databases.json';
|
7
|
+
import userEvent from '@testing-library/user-event';
|
8
|
+
import '@testing-library/jest-dom/extend-expect';
|
9
|
+
import '@testing-library/react/dont-cleanup-after-each';
|
10
|
+
|
11
|
+
export const setMockJSONResult = (result) => {
|
12
|
+
global.$.getJSON = (_, cb) => cb(result);
|
13
|
+
};
|
14
|
+
describe('ADVANCED PARAMETERS', () => {
|
15
|
+
const getInputElement = () => screen.getByRole('textbox', { name: '' });
|
16
|
+
test('should not render the link to advanced parameters modal if blast algorithm is unknown', () => {
|
17
|
+
setMockJSONResult(data);
|
18
|
+
const {container } =render(<Form onSequenceTypeChanged={() => { }
|
19
|
+
} />);
|
20
|
+
const modalButton = container.querySelector('[data-target="#help"]');
|
21
|
+
expect(modalButton).toBeNull();
|
22
|
+
});
|
23
|
+
test('should render the link to advanced parameters modal if blast algorithm is known', () => {
|
24
|
+
setMockJSONResult(data);
|
25
|
+
const {container } =render(<Form onSequenceTypeChanged={() => { }
|
26
|
+
} />);
|
27
|
+
|
28
|
+
const inputEl = getInputElement();
|
29
|
+
// populate search and select dbs to determine blast algorithm
|
30
|
+
fireEvent.change(inputEl, { target: { value: AMINO_ACID_SEQUENCE } });
|
31
|
+
const proteinSelectAllBtn = screen.getByRole('heading', { name: /protein databases/i }).parentElement.querySelector('button');
|
32
|
+
fireEvent.click(proteinSelectAllBtn);
|
33
|
+
const modalButton = container.querySelector('[data-target="#help"]');
|
34
|
+
expect(modalButton).not.toBeNull();
|
35
|
+
});
|
36
|
+
});
|
@@ -0,0 +1,49 @@
|
|
1
|
+
export const AMINO_ACID_SEQUENCE = `MNTLWLSLWDYPGKLPLNFMVFDTKDDLQAAYWRDPYSIPLAVIFEDPQPISQRLIYEIR
|
2
|
+
TNPSYTLPPPPTKLYSAPISCRKNKTGHWMDDILSIKTGESCPVNNYLHSGFLALQMITD
|
3
|
+
ITKIKLENSDVTIPDIKLIMFPKEPYTADWMLAFRVVIPLYMVLALSQFITYLLILIVGE
|
4
|
+
KENKIKEGMKMMGLNDSVF
|
5
|
+
>SI2.2.0_13722 locus=Si_gnF.scaffold06207[1925625..1928536].pep_1 quality=100.00
|
6
|
+
MSANRLNVLVTLMLAVALLVTESGNAQVDGYLQFNPKRSAVSSPQKYCGKKLSNALQIIC
|
7
|
+
DGVYNSMFKKSGQDFPPQNKRHIAHRINGNEEESFTTLKSNFLNWCVEVYHRHYRFVFVS
|
8
|
+
EMEMADYPLAYDISPYLPPFLSRARARGMLDGRFAGRRYRRESRGIHEECCINGCTINEL
|
9
|
+
TSYCGP
|
10
|
+
`;
|
11
|
+
export const NUCLEOTIDE_SEQUENCE = `ATGAATACCCTCTGGCTCTCTTTATGGGATTATCCCGGTAAGCTTCCCTTAAACTTCATG
|
12
|
+
GTGTTTGACACGAAGGATGATCTGCAAGCAGCGTATTGGAGAGATCCTTACAGCATACCT
|
13
|
+
CTGGCAGTTATCTTCGAGGACCCCCAACCGATATCACAGCGACTTATATATGAAATTAGG
|
14
|
+
ACGAATCCTTCATACACTTTGCCGCCACCGCCAACCAAATTGTATTCTGCTCCGATCAGT
|
15
|
+
TGTCGAAAGAATAAAACTGGTCACTGGATGGACGACATTTTATCGATAAAAACCGGTGAA
|
16
|
+
TCTTGTCCCGTTAACAATTACTTGCATTCTGGCTTCTTGGCTCTGCAAATGATAACGGAT
|
17
|
+
ATCACAAAGATAAAATTGGAAAATTCTGACGTGACAATACCGGATATTAAACTCATAATG
|
18
|
+
TTTCCTAAAGAGCCGTATACCGCTGACTGGATGCTGGCCTTCAGAGTTGTTATTCCGCTT
|
19
|
+
TACATGGTCTTGGCTCTCTCGCAATTTATCACTTATCTTCTGATCCTAATAGTTGGCGAG
|
20
|
+
AAGGAAAATAAGATTAAAGAGGGAATGAAGATGATGGGCTTAAATGATTCTGTGTTT
|
21
|
+
>SI2.2.0_13722 Si_gnF.scaffold06207[1925625..1928536].pep_1
|
22
|
+
ATGTCCGCGAATCGATTGAACGTGCTGGTGACCCTGATGCTCGCCGTCGCGCTTCTTGTG
|
23
|
+
ACGGAATCAGGAAATGCACAGGTGGATGGCTATCTCCAATTCAACCCAAAGCGATCCGCC
|
24
|
+
GTGAGCTCGCCGCAGAAGTATTGCGGCAAAAAGCTTTCTAATGCTCTACAGATAATCTGT
|
25
|
+
GATGGCGTGTACAATTCCATGTTTAAGAAGAGTGGTCAAGATTTTCCCCCGCAAAATAAG
|
26
|
+
AGACACATAGCACACAGAATAAATGGGAATGAGGAAGAGAGCTTTACTACGTTAAAGTCG
|
27
|
+
AATTTTTTAAACTGGTGTGTTGAAGTTTATCATCGTCACTACAGATTCGTTTTTGTTTCA
|
28
|
+
GAGATGGAAATGGCCGATTACCCGCTCGCCTATGATATTTCCCCGTATCTTCCGCCGTTC
|
29
|
+
CTGTCGCGAGCGAGGGCACGGGGAATGTTAGACGGTCGCTTCGCCGGCAGACGCTACCGA
|
30
|
+
AGGGAGTCGCGGGGCATTCACGAGGAGTGTTGCATCAACGGATGTACGATAAACGAATTG
|
31
|
+
ACCAGCTACTGCGGCCCC
|
32
|
+
`;
|
33
|
+
|
34
|
+
export const FASTQ_SEQUENCE =
|
35
|
+
`@SRR001666.1 071112_SLXA-EAS1_s_7:5:1:817:345 length=72
|
36
|
+
GGGTGATGGCCGCTGCCGATGGCGTCAAATCCCACCAAGTTACCCTTAACAACTTAAGGGTTTTCAAATAGA
|
37
|
+
+SRR001666.1 071112_SLXA-EAS1_s_7:5:1:817:345 length=72
|
38
|
+
IIIIIIIIIIIIIIIIIIIIIIIIIIIIII9IG9ICIIIIIIIIIIIIIIIIIIIIDIIIIIII>IIIIII/
|
39
|
+
@SRR001666.2 071112_SLXA-EAS1_s_7:5:1:801:338 length=72
|
40
|
+
GTTCAGGGATACGACGTTTGTATTTTAAGAATCTGAAGCAGAAGTCGATGATAATACGCGTCGTTTTATCAT
|
41
|
+
+SRR001666.2 071112_SLXA-EAS1_s_7:5:1:801:338 length=72
|
42
|
+
IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII6IBIIIIIIIIIIIIIIIIIIIIIIIGII>IIIII-I)8I
|
43
|
+
`;
|
44
|
+
|
45
|
+
export const FASTA_OF_FASTQ_SEQUENCE =
|
46
|
+
`>SRR001666.1 071112_SLXA-EAS1_s_7:5:1:817:345 length=72
|
47
|
+
GGGTGATGGCCGCTGCCGATGGCGTCAAATCCCACCAAGTTACCCTTAACAACTTAAGGGTTTTCAAATAGA
|
48
|
+
>SRR001666.2 071112_SLXA-EAS1_s_7:5:1:801:338 length=72
|
49
|
+
GTTCAGGGATACGACGTTTGTATTTTAAGAATCTGAAGCAGAAGTCGATGATAATACGCGTCGTTTTATCAT`;
|
@@ -25,7 +25,14 @@ const TestSidebar = ({ long }) => {
|
|
25
25
|
/>;
|
26
26
|
};
|
27
27
|
|
28
|
+
const clickCheckboxes = (checkboxes, count) => {
|
29
|
+
Array.from(checkboxes).slice(0, count).forEach((checkbox) => {
|
30
|
+
fireEvent.click(checkbox);
|
31
|
+
});
|
32
|
+
};
|
28
33
|
describe('REPORT PAGE', () => {
|
34
|
+
global.URL.createObjectURL = jest.fn();//.mockReturnValue('xyz.test');
|
35
|
+
global.setTimeout = (cb) => cb();
|
29
36
|
it('should render the report component with initial loading state', () => {
|
30
37
|
render(<Report />);
|
31
38
|
expect(screen.getByRole('heading', { name: 'BLAST-ing' })).toBeInTheDocument();
|
@@ -39,7 +46,7 @@ describe('REPORT PAGE', () => {
|
|
39
46
|
});
|
40
47
|
it('it should render the report page correctly if there\'s a response provided', () => {
|
41
48
|
setMockJSONResult({ status: 200, responseJSON: shortResponseJSON });
|
42
|
-
const { container } = render(<Report />);
|
49
|
+
const { container } = render(<Report getCharacterWidth={jest.fn()} />);
|
43
50
|
expect(container.querySelector('#results')).toBeInTheDocument();
|
44
51
|
|
45
52
|
});
|
@@ -63,18 +70,18 @@ describe('REPORT PAGE', () => {
|
|
63
70
|
});
|
64
71
|
|
65
72
|
describe('LONG QUERIES (>12)', () => {
|
66
|
-
|
73
|
+
let container;
|
74
|
+
beforeEach(() => {
|
75
|
+
container = render(<TestSidebar long />).container;
|
76
|
+
});
|
67
77
|
it('should not show navigation links for long queries', () => {
|
68
|
-
const { container } = render(<TestSidebar long />);
|
69
78
|
expect(container.querySelectorAll('a[href^="#Query_"]').length).toBe(0);
|
70
79
|
});
|
71
80
|
it('should show only next button if on first query ', () => {
|
72
|
-
render(<TestSidebar long />);
|
73
81
|
expect(nextQueryButton()).toBeInTheDocument();
|
74
82
|
expect(previousQueryButton()).not.toBeInTheDocument();
|
75
83
|
});
|
76
84
|
it('should show both previous and next buttons if not on first query', () => {
|
77
|
-
render(<TestSidebar long />);
|
78
85
|
const nextBtn = nextQueryButton();
|
79
86
|
expect(nextBtn).toBeInTheDocument();
|
80
87
|
fireEvent.click(nextBtn);
|
@@ -84,7 +91,6 @@ describe('REPORT PAGE', () => {
|
|
84
91
|
});
|
85
92
|
it('should show only previous button if on last query', () => {
|
86
93
|
const { queries } = longResponseJSON;
|
87
|
-
render(<TestSidebar long />);
|
88
94
|
expect(nextQueryButton()).toBeInTheDocument();
|
89
95
|
expect(previousQueryButton()).not.toBeInTheDocument();
|
90
96
|
|
@@ -95,5 +101,55 @@ describe('REPORT PAGE', () => {
|
|
95
101
|
expect(previousQueryButton()).toBeInTheDocument();
|
96
102
|
});
|
97
103
|
});
|
104
|
+
|
105
|
+
describe('DOWNLOAD LINKS', () => {
|
106
|
+
let container;
|
107
|
+
beforeEach(() => {
|
108
|
+
setMockJSONResult({ status: 200, responseJSON: shortResponseJSON });
|
109
|
+
container = render(<Report getCharacterWidth={jest.fn()} />).container;
|
110
|
+
});
|
111
|
+
describe('ALIGNMENT DOWNLOAD', () => {
|
112
|
+
it('should generate a blob url and filename for downloading alignment of all hits on render', () => {
|
113
|
+
const alignment_download_link = container.querySelector('.download-alignment-of-all');
|
114
|
+
const expected_num_hits = container.querySelectorAll('.hit-links input[type="checkbox"]').length;
|
115
|
+
const file_name = `alignment-${expected_num_hits}_hits.txt`;
|
116
|
+
expect(alignment_download_link.download).toEqual(file_name);
|
117
|
+
expect(alignment_download_link.hred).not.toEqual('#');
|
118
|
+
});
|
119
|
+
it('link for downloading alignment of specific number of selected hits should be disabled on initial load', () => {
|
120
|
+
const alignment_download_link = container.querySelector('.download-alignment-of-selected');
|
121
|
+
expect(alignment_download_link.classList.contains('disabled')).toBeTruthy();
|
122
|
+
|
123
|
+
});
|
124
|
+
it('should generate a blob url and filename for downloading alignment of specific number of selected hits', () => {
|
125
|
+
const alignment_download_link = container.querySelector('.download-alignment-of-selected');
|
126
|
+
// QUERY ALL HIT LINKS CHECKBOXES
|
127
|
+
const checkboxes = container.querySelectorAll('.hit-links input[type="checkbox"]');
|
128
|
+
// SELECT 4 CHECKBOXES
|
129
|
+
clickCheckboxes(checkboxes, 4);
|
130
|
+
const file_name = 'alignment-4_hits.txt';
|
131
|
+
expect(alignment_download_link.textContent).toEqual('Alignment of 4 selected hit(s)');
|
132
|
+
expect(alignment_download_link.download).toEqual(file_name);
|
133
|
+
});
|
134
|
+
});
|
135
|
+
|
136
|
+
describe('FASTA DOWNLOAD', () => {
|
137
|
+
let fasta_download_link;
|
138
|
+
beforeEach(() => {
|
139
|
+
fasta_download_link = container.querySelector('.download-fasta-of-selected');
|
140
|
+
});
|
141
|
+
it('link for downloading fasta of selected number of hits should be disabled on initial load', () => {
|
142
|
+
expect(fasta_download_link.classList.contains('disabled')).toBeTruthy();
|
143
|
+
});
|
144
|
+
|
145
|
+
it('link for downloading fasta of specific number of selected hits should be active after selection', () => {
|
146
|
+
const checkboxes = container.querySelectorAll('.hit-links input[type="checkbox"]');
|
147
|
+
// SELECT 5 CHECKBOXES
|
148
|
+
clickCheckboxes(checkboxes, 5);
|
149
|
+
expect(fasta_download_link.textContent).toEqual('FASTA of 5 selected hit(s)');
|
150
|
+
});
|
151
|
+
});
|
152
|
+
});
|
98
153
|
});
|
154
|
+
|
99
155
|
});
|
@@ -3,39 +3,65 @@
|
|
3
3
|
import { render, screen, fireEvent } from '@testing-library/react';
|
4
4
|
import { SearchQueryWidget } from '../query';
|
5
5
|
import { Form } from '../form';
|
6
|
-
import
|
6
|
+
import { AMINO_ACID_SEQUENCE, NUCLEOTIDE_SEQUENCE, FASTQ_SEQUENCE, FASTA_OF_FASTQ_SEQUENCE } from './mock_data/sequences';
|
7
7
|
import '@testing-library/jest-dom/extend-expect';
|
8
8
|
import '@testing-library/react/dont-cleanup-after-each';
|
9
9
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
KENKIKEGMKMMGLNDSVF
|
14
|
-
>SI2.2.0_13722 locus=Si_gnF.scaffold06207[1925625..1928536].pep_1 quality=100.00
|
15
|
-
MSANRLNVLVTLMLAVALLVTESGNAQVDGYLQFNPKRSAVSSPQKYCGKKLSNALQIIC
|
16
|
-
DGVYNSMFKKSGQDFPPQNKRHIAHRINGNEEESFTTLKSNFLNWCVEVYHRHYRFVFVS
|
17
|
-
EMEMADYPLAYDISPYLPPFLSRARARGMLDGRFAGRRYRRESRGIHEECCINGCTINEL
|
18
|
-
TSYCGP
|
19
|
-
`;
|
10
|
+
let container;
|
11
|
+
let inputEl;
|
12
|
+
|
20
13
|
describe('SEARCH COMPONENT', () => {
|
21
|
-
|
14
|
+
beforeEach(() => {
|
15
|
+
container = render(<Form onSequenceTypeChanged={() => { }
|
16
|
+
} />).container;
|
17
|
+
inputEl = screen.getByRole('textbox', { name: '' });
|
18
|
+
});
|
19
|
+
|
22
20
|
test('should render the search component textarea', () => {
|
23
|
-
|
24
|
-
} />);
|
25
|
-
const el = getInputElement();
|
26
|
-
expect(el).toHaveClass('form-control');
|
21
|
+
expect(inputEl).toHaveClass('form-control');
|
27
22
|
});
|
28
23
|
|
29
24
|
test('clear button should only become visible if textarea is not empty', () => {
|
30
|
-
render(<SearchQueryWidget onSequenceTypeChanged={() => { }
|
31
|
-
} />);
|
32
25
|
const getButtonWrapper = () => screen.getByRole('button', { name: /clear query sequence\(s\)\./i }).parentElement;
|
33
26
|
expect(getButtonWrapper()).toHaveClass('hidden');
|
34
|
-
const inputEl = getInputElement();
|
35
27
|
fireEvent.change(inputEl, { target: { value: AMINO_ACID_SEQUENCE } });
|
36
28
|
expect(getButtonWrapper()).not.toHaveClass('hidden');
|
37
29
|
fireEvent.change(inputEl, { target: { value: '' } });
|
38
30
|
expect(getButtonWrapper()).toHaveClass('hidden');
|
31
|
+
});
|
32
|
+
|
33
|
+
test('should correctly detect the amino-acid sequence type and show notification', () => {
|
34
|
+
// populate search
|
35
|
+
fireEvent.change(inputEl, { target: { value: AMINO_ACID_SEQUENCE } });
|
36
|
+
const activeNotification = container.querySelector('.notification.active');
|
37
|
+
expect(activeNotification.id).toBe('protein-sequence-notification');
|
38
|
+
const alertWrapper = activeNotification.children[0];
|
39
|
+
expect(alertWrapper).toHaveTextContent('Detected: amino-acid sequence(s).');
|
40
|
+
});
|
41
|
+
|
42
|
+
test('should correctly detect the nucleotide sequence type and show notification', () => {
|
43
|
+
// populate search
|
44
|
+
fireEvent.change(inputEl, { target: { value: NUCLEOTIDE_SEQUENCE } });
|
45
|
+
const activeNotification = container.querySelector('.notification.active');
|
46
|
+
const alertWrapper = activeNotification.children[0];
|
47
|
+
expect(activeNotification.id).toBe('nucleotide-sequence-notification');
|
48
|
+
expect(alertWrapper).toHaveTextContent('Detected: nucleotide sequence(s).');
|
49
|
+
});
|
50
|
+
|
51
|
+
test('should correctly detect the mixed sequences and show error notification', () => {
|
52
|
+
fireEvent.change(inputEl, { target: { value: `${NUCLEOTIDE_SEQUENCE}${AMINO_ACID_SEQUENCE}` } });
|
53
|
+
const activeNotification = container.querySelector('.notification.active');
|
54
|
+
expect(activeNotification.id).toBe('mixed-sequence-notification');
|
55
|
+
const alertWrapper = activeNotification.children[0];
|
56
|
+
expect(alertWrapper).toHaveTextContent('Error: mixed nucleotide and amino-acid sequences detected.');
|
57
|
+
});
|
39
58
|
|
59
|
+
test('should correctly detect FASTQ and convert it to FASTA', () => {
|
60
|
+
fireEvent.change(inputEl, { target: { value: FASTQ_SEQUENCE } });
|
61
|
+
const activeNotification = container.querySelector('.notification.active');
|
62
|
+
const alertWrapper = activeNotification.children[0];
|
63
|
+
expect(activeNotification.id).toBe('fastq-sequence-notification');
|
64
|
+
expect(alertWrapper).toHaveTextContent('Detected FASTQ and automatically converted to FASTA.');
|
65
|
+
expect(inputEl).toHaveValue(FASTA_OF_FASTQ_SEQUENCE);
|
40
66
|
});
|
41
67
|
});
|