sequenceserver 3.0.1 → 3.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/sequenceserver +2 -2
- data/lib/sequenceserver/api_errors.rb +56 -2
- data/lib/sequenceserver/blast/job.rb +20 -3
- data/lib/sequenceserver/blast/report.rb +74 -86
- data/lib/sequenceserver/blast/tasks.rb +38 -0
- data/lib/sequenceserver/blast.rb +6 -0
- data/lib/sequenceserver/config.rb +54 -20
- data/lib/sequenceserver/database.rb +13 -0
- data/lib/sequenceserver/makeblastdb.rb +16 -2
- data/lib/sequenceserver/report.rb +0 -6
- data/lib/sequenceserver/routes.rb +66 -25
- data/lib/sequenceserver/sequence.rb +34 -7
- data/lib/sequenceserver/server.rb +1 -1
- data/lib/sequenceserver/version.rb +1 -1
- data/lib/sequenceserver.rb +1 -1
- data/public/404.html +1 -1
- data/public/css/app.css +121 -0
- data/public/css/app.min.css +1 -0
- data/public/css/sequenceserver.css +0 -148
- data/public/css/sequenceserver.min.css +3 -3
- data/public/js/circos.js +2 -2
- data/public/js/collapse_preferences.js +37 -0
- data/public/js/databases.js +65 -37
- data/public/js/databases_tree.js +2 -1
- data/public/js/dnd.js +37 -50
- data/public/js/download_fasta.js +1 -0
- data/public/js/form.js +79 -50
- data/public/js/grapher.js +23 -37
- data/public/js/hits_overview.js +2 -2
- data/public/js/kablammo.js +2 -2
- data/public/js/length_distribution.js +3 -3
- data/public/js/null_plugins/grapher/histogram.js +25 -0
- data/public/js/null_plugins/options.js +3 -0
- data/public/js/null_plugins/query_stats.js +11 -0
- data/public/js/null_plugins/report_plugins.js +6 -1
- data/public/js/null_plugins/search_header_plugin.js +4 -0
- data/public/js/options.js +161 -56
- data/public/js/query.js +85 -59
- data/public/js/report.js +1 -1
- data/public/js/search.js +2 -0
- data/public/js/search_button.js +67 -56
- data/public/js/sidebar.js +10 -1
- data/public/js/tests/database.spec.js +5 -5
- data/public/js/tests/form.spec.js +98 -0
- data/public/js/tests/mock_data/databases.json +5 -5
- data/public/js/tests/mocks/circos.js +6 -0
- data/public/js/tests/report.spec.js +4 -3
- data/public/js/tests/search_query.spec.js +16 -6
- data/public/sequenceserver-report.min.js +46 -24
- data/public/sequenceserver-search.min.js +57 -13
- data/public/sequenceserver_logo.webp +0 -0
- data/views/blastn_options.erb +66 -66
- data/views/blastp_options.erb +59 -59
- data/views/blastx_options.erb +68 -68
- data/views/layout.erb +61 -3
- data/views/search.erb +33 -38
- data/views/search_layout.erb +153 -0
- data/views/tblastn_options.erb +57 -57
- data/views/tblastx_options.erb +64 -64
- metadata +51 -22
- data/lib/sequenceserver/makeblastdb-modified-with-cache.rb +0 -345
- data/public/SequenceServer_logo.png +0 -0
- data/public/js/tests/advanced_parameters.spec.js +0 -36
data/public/js/query.js
CHANGED
@@ -5,6 +5,7 @@ import HitsOverview from './hits_overview';
|
|
5
5
|
import LengthDistribution from './length_distribution'; // length distribution of hits
|
6
6
|
import Utils from './utils';
|
7
7
|
import { fastqToFasta } from './fastq_to_fasta';
|
8
|
+
import CollapsePreferences from './collapse_preferences';
|
8
9
|
|
9
10
|
/**
|
10
11
|
* Query component displays query defline, graphical overview, length
|
@@ -59,7 +60,7 @@ export class ReportQuery extends Component {
|
|
59
60
|
hitsListJSX() {
|
60
61
|
return <div className="section-content">
|
61
62
|
<HitsOverview key={'GO_' + this.props.query.number} query={this.props.query} program={this.props.program} collapsed={this.props.veryBig} />
|
62
|
-
<LengthDistribution key={'LD_' + this.props.query.id} query={this.props.query} algorithm={this.props.program}
|
63
|
+
<LengthDistribution key={'LD_' + this.props.query.id} query={this.props.query} algorithm={this.props.program} />
|
63
64
|
<HitsTable key={'HT_' + this.props.query.number} query={this.props.query} imported_xml={this.props.imported_xml} />
|
64
65
|
</div>;
|
65
66
|
}
|
@@ -107,8 +108,8 @@ export class SearchQueryWidget extends Component {
|
|
107
108
|
this.preProcessSequence = this.preProcessSequence.bind(this);
|
108
109
|
this.notify = this.notify.bind(this);
|
109
110
|
|
110
|
-
this.textareaRef = createRef()
|
111
|
-
this.controlsRef = createRef()
|
111
|
+
this.textareaRef = createRef();
|
112
|
+
this.controlsRef = createRef();
|
112
113
|
}
|
113
114
|
|
114
115
|
|
@@ -116,13 +117,14 @@ export class SearchQueryWidget extends Component {
|
|
116
117
|
|
117
118
|
componentDidMount() {
|
118
119
|
$('body').click(function () {
|
119
|
-
$('
|
120
|
+
$('[data-notifications] [data-role=notification].active').hide('drop', { direction: 'up' }).removeClass('active');
|
120
121
|
});
|
121
122
|
}
|
122
123
|
|
123
124
|
componentDidUpdate() {
|
124
125
|
this.hideShowButton();
|
125
126
|
this.preProcessSequence();
|
127
|
+
this.props.onSequenceChanged(this.residuesCount());
|
126
128
|
|
127
129
|
var type = this.type();
|
128
130
|
if (!type || type !== this._type) {
|
@@ -155,6 +157,19 @@ export class SearchQueryWidget extends Component {
|
|
155
157
|
}
|
156
158
|
}
|
157
159
|
|
160
|
+
residuesCount() {
|
161
|
+
const sequence = this.value();
|
162
|
+
const lines = sequence.split('\n');
|
163
|
+
const residuesCount = lines.reduce((count, line) => {
|
164
|
+
if (!line.startsWith('>')) {
|
165
|
+
return count + line.length;
|
166
|
+
}
|
167
|
+
return count;
|
168
|
+
}, 0);
|
169
|
+
|
170
|
+
return residuesCount;
|
171
|
+
}
|
172
|
+
|
158
173
|
/**
|
159
174
|
* Clears textarea. Returns `this`.
|
160
175
|
*
|
@@ -311,13 +326,13 @@ export class SearchQueryWidget extends Component {
|
|
311
326
|
notify(type) {
|
312
327
|
this.indicateNormal();
|
313
328
|
clearTimeout(this.notification_timeout);
|
314
|
-
// $('
|
329
|
+
// $('[data-notifications] [data-role=notification].active').hide().removeClass('active');
|
315
330
|
|
316
331
|
if (type) {
|
317
332
|
$('#' + type + '-sequence-notification').show('drop', { direction: 'up' }).addClass('active');
|
318
333
|
|
319
334
|
this.notification_timeout = setTimeout(function () {
|
320
|
-
$('
|
335
|
+
$('[data-notifications] [data-role=notification].active').hide('drop', { direction: 'up' }).removeClass('active');
|
321
336
|
}, 5000);
|
322
337
|
|
323
338
|
if (type === 'mixed') {
|
@@ -328,14 +343,15 @@ export class SearchQueryWidget extends Component {
|
|
328
343
|
|
329
344
|
render() {
|
330
345
|
return (
|
331
|
-
<div
|
332
|
-
className="col-md-12">
|
346
|
+
<div className="relative">
|
333
347
|
<div
|
334
348
|
className="sequence">
|
335
349
|
<textarea
|
336
350
|
id="sequence" ref={this.textareaRef}
|
337
|
-
className="
|
351
|
+
className="block w-full p-4 text-gray-900 border border-gray-300 rounded-l-lg rounded-tr-lg bg-gray-50 text-base text-monospace"
|
338
352
|
name="sequence" value={this.state.value}
|
353
|
+
rows="6"
|
354
|
+
required="required"
|
339
355
|
placeholder="Paste query sequence(s) or drag file
|
340
356
|
containing query sequence(s) in FASTA format here ..."
|
341
357
|
spellCheck="false" autoFocus
|
@@ -343,16 +359,16 @@ export class SearchQueryWidget extends Component {
|
|
343
359
|
</textarea>
|
344
360
|
</div>
|
345
361
|
<div
|
346
|
-
className="hidden"
|
347
|
-
style={{ position: 'absolute', top: '4px', right: '19px' }}
|
362
|
+
className="hidden absolute top-2 right-2"
|
348
363
|
ref={this.controlsRef}>
|
349
364
|
<button
|
350
365
|
type="button"
|
351
|
-
className="
|
366
|
+
className="border border-gray-300 rounded bg-white hover:bg-gray-200" id="btn-sequence-clear"
|
352
367
|
title="Clear query sequence(s)."
|
353
368
|
onClick={this.clear}>
|
354
369
|
<span id="sequence-file"></span>
|
355
|
-
<i className="fa fa-times"></i>
|
370
|
+
<i className="fa fa-times w-6 h-6 p-1"></i>
|
371
|
+
<span className="sr-only">Clear query sequence(s).</span>
|
356
372
|
</button>
|
357
373
|
</div>
|
358
374
|
</div>
|
@@ -368,8 +384,14 @@ export class SearchQueryWidget extends Component {
|
|
368
384
|
class HitsTable extends Component {
|
369
385
|
constructor(props) {
|
370
386
|
super(props);
|
387
|
+
this.name = 'Hit sequences producing significant alignments';
|
388
|
+
this.collapsePreferences = new CollapsePreferences(this);
|
389
|
+
this.state = {
|
390
|
+
collapsed: this.collapsePreferences.preferenceStoredAsCollapsed()
|
391
|
+
};
|
371
392
|
}
|
372
|
-
|
393
|
+
|
394
|
+
tableJSX() {
|
373
395
|
var hasName = _.every(this.props.query.hits, function (hit) {
|
374
396
|
return hit.sciname !== '';
|
375
397
|
});
|
@@ -385,54 +407,58 @@ class HitsTable extends Component {
|
|
385
407
|
// column.
|
386
408
|
if (this.props.imported_xml) seqwidth += 15;
|
387
409
|
|
410
|
+
return <table
|
411
|
+
className="table table-hover table-condensed tabular-view ">
|
412
|
+
<thead>
|
413
|
+
<tr>
|
414
|
+
<th className="text-left">#</th>
|
415
|
+
<th width={`${seqwidth}%`}>Similar sequences</th>
|
416
|
+
{hasName && <th width="15%" className="text-left">Species</th>}
|
417
|
+
{!this.props.imported_xml && <th width="15%" className="text-right">Query coverage (%)</th>}
|
418
|
+
<th width="10%" className="text-right">Total score</th>
|
419
|
+
<th width="10%" className="text-right">E value</th>
|
420
|
+
<th width="10%" className="text-right">Identity (%)</th>
|
421
|
+
</tr>
|
422
|
+
</thead>
|
423
|
+
<tbody>
|
424
|
+
{
|
425
|
+
_.map(this.props.query.hits, _.bind(function (hit) {
|
426
|
+
return (
|
427
|
+
<tr key={hit.number}>
|
428
|
+
<td className="text-left">{hit.number + '.'}</td>
|
429
|
+
<td className="nowrap-ellipsis"
|
430
|
+
title={`${hit.id} ${hit.title}`}
|
431
|
+
data-toggle="tooltip" data-placement="left">
|
432
|
+
<a href={'#Query_' + this.props.query.number + '_hit_' + hit.number}
|
433
|
+
className="btn-link">{hit.id} {hit.title}</a>
|
434
|
+
</td>
|
435
|
+
{hasName &&
|
436
|
+
<td className="nowrap-ellipsis" title={hit.sciname}
|
437
|
+
data-toggle="tooltip" data-placement="top">
|
438
|
+
{hit.sciname}
|
439
|
+
</td>
|
440
|
+
}
|
441
|
+
{!this.props.imported_xml && <td className="text-right">{hit.qcovs}</td>}
|
442
|
+
<td className="text-right">{hit.total_score}</td>
|
443
|
+
<td className="text-right">{Utils.inExponential(hit.hsps[0].evalue)}</td>
|
444
|
+
<td className="text-right">{Utils.inPercentage(hit.hsps[0].identity, hit.hsps[0].length)}</td>
|
445
|
+
</tr>
|
446
|
+
);
|
447
|
+
}, this))
|
448
|
+
}
|
449
|
+
</tbody>
|
450
|
+
</table>;
|
451
|
+
}
|
452
|
+
|
453
|
+
render() {
|
388
454
|
return (
|
389
455
|
<div className="table-hit-overview">
|
390
|
-
<h4 className="caption"
|
391
|
-
|
392
|
-
<span>
|
456
|
+
<h4 className="caption" onClick={() => this.collapsePreferences.toggleCollapse()}>
|
457
|
+
{this.collapsePreferences.renderCollapseIcon()}
|
458
|
+
<span> {this.name}</span>
|
393
459
|
</h4>
|
394
|
-
<div
|
395
|
-
|
396
|
-
className="table table-hover table-condensed tabular-view ">
|
397
|
-
<thead>
|
398
|
-
<tr>
|
399
|
-
<th className="text-left">#</th>
|
400
|
-
<th width={`${seqwidth}%`}>Similar sequences</th>
|
401
|
-
{hasName && <th width="15%" className="text-left">Species</th>}
|
402
|
-
{!this.props.imported_xml && <th width="15%" className="text-right">Query coverage (%)</th>}
|
403
|
-
<th width="10%" className="text-right">Total score</th>
|
404
|
-
<th width="10%" className="text-right">E value</th>
|
405
|
-
<th width="10%" className="text-right">Identity (%)</th>
|
406
|
-
</tr>
|
407
|
-
</thead>
|
408
|
-
<tbody>
|
409
|
-
{
|
410
|
-
_.map(this.props.query.hits, _.bind(function (hit) {
|
411
|
-
return (
|
412
|
-
<tr key={hit.number}>
|
413
|
-
<td className="text-left">{hit.number + '.'}</td>
|
414
|
-
<td className="nowrap-ellipsis"
|
415
|
-
title={`${hit.id} ${hit.title}`}
|
416
|
-
data-toggle="tooltip" data-placement="left">
|
417
|
-
<a href={'#Query_' + this.props.query.number + '_hit_' + hit.number}
|
418
|
-
className="btn-link">{hit.id} {hit.title}</a>
|
419
|
-
</td>
|
420
|
-
{hasName &&
|
421
|
-
<td className="nowrap-ellipsis" title={hit.sciname}
|
422
|
-
data-toggle="tooltip" data-placement="top">
|
423
|
-
{hit.sciname}
|
424
|
-
</td>
|
425
|
-
}
|
426
|
-
{!this.props.imported_xml && <td className="text-right">{hit.qcovs}</td>}
|
427
|
-
<td className="text-right">{hit.total_score}</td>
|
428
|
-
<td className="text-right">{Utils.inExponential(hit.hsps[0].evalue)}</td>
|
429
|
-
<td className="text-right">{Utils.inPercentage(hit.hsps[0].identity, hit.hsps[0].length)}</td>
|
430
|
-
</tr>
|
431
|
-
);
|
432
|
-
}, this))
|
433
|
-
}
|
434
|
-
</tbody>
|
435
|
-
</table>
|
460
|
+
<div id={'Query_' + this.props.query.number + 'HT_' + this.props.query.number}>
|
461
|
+
{!this.state.collapsed && this.tableJSX()}
|
436
462
|
</div>
|
437
463
|
</div>
|
438
464
|
);
|
data/public/js/report.js
CHANGED
@@ -300,6 +300,7 @@ class Report extends Component {
|
|
300
300
|
<div className="col-md-9">
|
301
301
|
{this.overviewJSX()}
|
302
302
|
{this.circosJSX()}
|
303
|
+
{this.plugins.generateStats()}
|
303
304
|
{this.state.results}
|
304
305
|
</div>
|
305
306
|
</div>
|
@@ -386,7 +387,6 @@ class Report extends Component {
|
|
386
387
|
<Circos
|
387
388
|
queries={this.state.queries}
|
388
389
|
program={this.state.program}
|
389
|
-
collapsed="true"
|
390
390
|
/>
|
391
391
|
) : (
|
392
392
|
<span></span>
|
data/public/js/search.js
CHANGED
@@ -3,6 +3,7 @@ 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
|
+
import { SearchHeaderPlugin } from "search_header_plugin";
|
6
7
|
|
7
8
|
/**
|
8
9
|
* Clear sessionStorage on reload.
|
@@ -19,6 +20,7 @@ class Page extends Component {
|
|
19
20
|
render() {
|
20
21
|
return (
|
21
22
|
<div>
|
23
|
+
<SearchHeaderPlugin />
|
22
24
|
<DnD ref="dnd" />
|
23
25
|
<Form ref="form" />
|
24
26
|
</div>
|
data/public/js/search_button.js
CHANGED
@@ -12,6 +12,7 @@ export class SearchButton extends Component {
|
|
12
12
|
methods: [],
|
13
13
|
hasQuery: false,
|
14
14
|
hasDatabases: false,
|
15
|
+
dropdownVisible: false,
|
15
16
|
};
|
16
17
|
this.inputGroup = this.inputGroup.bind(this);
|
17
18
|
this.submitButton = this.submitButton.bind(this);
|
@@ -28,15 +29,17 @@ export class SearchButton extends Component {
|
|
28
29
|
}
|
29
30
|
|
30
31
|
shouldComponentUpdate(props, state) {
|
31
|
-
return !_.isEqual(state.methods, this.state.methods);
|
32
|
+
return !_.isEqual(state.methods, this.state.methods) || state.dropdownVisible !== this.state.dropdownVisible;
|
32
33
|
}
|
33
34
|
|
34
|
-
componentDidUpdate() {
|
35
|
-
if (this.state.methods
|
36
|
-
this.
|
37
|
-
|
38
|
-
|
39
|
-
|
35
|
+
componentDidUpdate(_prevProps, prevState) {
|
36
|
+
if (!_.isEqual(prevState.methods, this.state.methods)) {
|
37
|
+
if (this.state.methods.length > 0) {
|
38
|
+
this.inputGroup().wiggle();
|
39
|
+
this.props.onAlgoChanged(this.state.methods[0]);
|
40
|
+
} else {
|
41
|
+
this.props.onAlgoChanged('');
|
42
|
+
}
|
40
43
|
}
|
41
44
|
}
|
42
45
|
// Internal helpers. //
|
@@ -110,6 +113,7 @@ export class SearchButton extends Component {
|
|
110
113
|
methods.unshift(method);
|
111
114
|
this.setState({
|
112
115
|
methods: methods,
|
116
|
+
dropdownVisible: false,
|
113
117
|
});
|
114
118
|
}
|
115
119
|
|
@@ -131,64 +135,71 @@ export class SearchButton extends Component {
|
|
131
135
|
});
|
132
136
|
}
|
133
137
|
|
138
|
+
toggleDropdownVisibility = () => {
|
139
|
+
this.setState(prevState => ({
|
140
|
+
dropdownVisible: !prevState.dropdownVisible
|
141
|
+
}));
|
142
|
+
}
|
143
|
+
|
134
144
|
render() {
|
135
145
|
var methods = this.state.methods;
|
136
146
|
var method = methods[0];
|
137
147
|
var multi = methods.length > 1;
|
138
148
|
|
139
149
|
return (
|
140
|
-
<div
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
150
|
+
<div
|
151
|
+
// className={multi ? 'flex' : 'flex'}
|
152
|
+
className="my-4 md:my-2 flex justify-end w-full md:w-auto relative"
|
153
|
+
id="methods"
|
154
|
+
ref={this.inputGroupRef}
|
155
|
+
onMouseOver={this.showTooltip}
|
156
|
+
onMouseOut={this.hideTooltip}
|
157
|
+
>
|
158
|
+
<button
|
159
|
+
type="submit"
|
160
|
+
className="uppercase w-full md:w-auto flex text-xl justify-center py-2 px-16 border border-transparent rounded-md shadow-sm text-white bg-seqblue hover:bg-seqorange focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-seqorange"
|
161
|
+
id="method"
|
162
|
+
ref={this.submitButtonRef}
|
163
|
+
name="method"
|
164
|
+
value={method}
|
165
|
+
disabled={!method}
|
166
|
+
>
|
167
|
+
{this.decorate(method || 'blast')}
|
168
|
+
</button>
|
169
|
+
|
170
|
+
{multi && (
|
171
|
+
<div className="ui--multi-dropdown">
|
172
|
+
<button
|
173
|
+
className="text-xl bg-seqblue rounded-r-md text-white p-2 border border-seqblue hover:bg-seqorange focus:outline-none focus:ring-1 focus:ring-seqorange -ml-8"
|
174
|
+
type="button"
|
175
|
+
onClick={this.toggleDropdownVisibility}
|
149
176
|
>
|
150
|
-
<
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
return (
|
174
|
-
<li
|
175
|
-
key={method}
|
176
|
-
className="text-uppercase"
|
177
|
-
onClick={_.bind(function () {
|
178
|
-
this.changeAlgorithm(method);
|
179
|
-
}, this)}
|
180
|
-
>
|
181
|
-
{method}
|
182
|
-
</li>
|
183
|
-
);
|
184
|
-
}, this)
|
185
|
-
)}
|
186
|
-
</ul>
|
187
|
-
</div>
|
188
|
-
)}
|
177
|
+
<i className="fas fa-caret-down w-6 h-6 fill-current"></i>
|
178
|
+
<span className="sr-only">Other methods</span>
|
179
|
+
</button>
|
180
|
+
|
181
|
+
<div id="dropdown"
|
182
|
+
className={`z-10 my-2 uppercase bg-blue-300 divide-y divide-gray-100 rounded-lg shadow absolute left-0 bottom-12 w-full text-xl text-center ${this.state.dropdownVisible ? '' : 'hidden'}`}>
|
183
|
+
<ul className="text-gray-700" aria-labelledby="dropdownDefaultButton">
|
184
|
+
{_.map(
|
185
|
+
methods.slice(1),
|
186
|
+
_.bind(function (method) {
|
187
|
+
return (
|
188
|
+
<li
|
189
|
+
key={method}
|
190
|
+
onClick={_.bind(function () {
|
191
|
+
this.changeAlgorithm(method);
|
192
|
+
}, this)}
|
193
|
+
>
|
194
|
+
<a href="#" className="block px-4 py-2 hover:bg-blue-400 rounded-lg">{method}</a>
|
195
|
+
</li>
|
196
|
+
);
|
197
|
+
}, this)
|
198
|
+
)}
|
199
|
+
</ul>
|
189
200
|
</div>
|
190
201
|
</div>
|
191
|
-
|
202
|
+
)}
|
192
203
|
</div>
|
193
204
|
);
|
194
205
|
}
|
data/public/js/sidebar.js
CHANGED
@@ -198,7 +198,7 @@ export default class extends Component {
|
|
198
198
|
document.body.removeChild(element);
|
199
199
|
|
200
200
|
setTimeout(function () {
|
201
|
-
$('#copyURL')._tooltip('
|
201
|
+
$('#copyURL')._tooltip('hide');
|
202
202
|
}, 3000);
|
203
203
|
}
|
204
204
|
|
@@ -350,6 +350,15 @@ export default class extends Component {
|
|
350
350
|
</a>
|
351
351
|
</li>
|
352
352
|
}
|
353
|
+
{
|
354
|
+
!this.props.data.imported_xml && <li>
|
355
|
+
<a className="btn-link download" data-toggle="tooltip"
|
356
|
+
title="Results in pairwise format."
|
357
|
+
href={'download/' + this.props.data.search_id + '.pairwise'}>
|
358
|
+
Full Pairwise report
|
359
|
+
</a>
|
360
|
+
</li>
|
361
|
+
}
|
353
362
|
<DownloadLinks imported_xml={this.props.data.imported_xml} search_id={this.props.data.search_id} />
|
354
363
|
</ul>
|
355
364
|
</div>
|
@@ -1,6 +1,6 @@
|
|
1
1
|
/* eslint-disable no-unused-vars */
|
2
2
|
/* eslint-disable no-undef */
|
3
|
-
import { render, screen, fireEvent
|
3
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
4
4
|
import { Databases } from '../databases';
|
5
5
|
import data from './mock_data/databases.json';
|
6
6
|
|
@@ -21,13 +21,13 @@ describe('DATABASES COMPONENT', () => {
|
|
21
21
|
});
|
22
22
|
|
23
23
|
test('clicking select all on a database should select all its children', () => {
|
24
|
-
const { container } = render(<Databases databases={data.database} onDatabaseTypeChanged={() => { }} />);
|
25
|
-
|
24
|
+
const { container } = render(<Databases databases={data.database} onDatabaseTypeChanged={() => { }} onDatabaseSelectionChanged={() => { }} />);
|
25
|
+
|
26
26
|
// select all nucleotide databases
|
27
27
|
const nucleotideSelectAllBtn = screen.getByRole('heading', { name: /nucleotide databases/i }).parentElement.querySelector('button');
|
28
28
|
fireEvent.click(nucleotideSelectAllBtn);
|
29
29
|
const nucleotideCheckboxes = container.querySelector('.databases.nucleotide').querySelectorAll('input[type=checkbox]');
|
30
|
-
|
30
|
+
|
31
31
|
// all nucleotide databases should be checked
|
32
32
|
nucleotideCheckboxes.forEach((checkbox) => {
|
33
33
|
expect(checkbox).toBeChecked();
|
@@ -45,7 +45,7 @@ describe('DATABASES COMPONENT', () => {
|
|
45
45
|
|
46
46
|
test('checking any item of a database type should disable other database type', () => {
|
47
47
|
const mockFunction = jest.fn(() => { });
|
48
|
-
const { container } = render(<Databases databases={data.database} onDatabaseTypeChanged={mockFunction} />);
|
48
|
+
const { container } = render(<Databases databases={data.database} onDatabaseTypeChanged={mockFunction} onDatabaseSelectionChanged={mockFunction}/>);
|
49
49
|
|
50
50
|
//select a proteinn database
|
51
51
|
fireEvent.click(screen.getByRole('checkbox', { name: /2020-11 Swiss-Prot insecta/i }));
|
@@ -0,0 +1,98 @@
|
|
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
|
+
|
15
|
+
describe('ADVANCED PARAMETERS', () => {
|
16
|
+
let csrfMetaTag;
|
17
|
+
|
18
|
+
beforeEach(() => {
|
19
|
+
csrfMetaTag = document.createElement('meta');
|
20
|
+
csrfMetaTag.setAttribute('name', '_csrf');
|
21
|
+
csrfMetaTag.setAttribute('content', 'test-token');
|
22
|
+
document.head.appendChild(csrfMetaTag);
|
23
|
+
});
|
24
|
+
|
25
|
+
afterEach(() => {
|
26
|
+
// Remove the CSRF meta tag after each test to clean up
|
27
|
+
document.head.removeChild(csrfMetaTag);
|
28
|
+
});
|
29
|
+
|
30
|
+
const getInputElement = () => screen.getByRole('textbox', { name: '' });
|
31
|
+
test('should not render the link to advanced parameters modal if blast algorithm is unknown', () => {
|
32
|
+
setMockJSONResult(data);
|
33
|
+
const {container } =render(<Form onSequenceTypeChanged={() => { }
|
34
|
+
} />);
|
35
|
+
const modalButton = container.querySelector('[data-target="#help"]');
|
36
|
+
expect(modalButton).toBeNull();
|
37
|
+
});
|
38
|
+
test('should render the link to advanced parameters modal if blast algorithm is known', () => {
|
39
|
+
setMockJSONResult(data);
|
40
|
+
const {container } =render(<Form onSequenceTypeChanged={() => { }
|
41
|
+
} />);
|
42
|
+
|
43
|
+
const inputEl = getInputElement();
|
44
|
+
// populate search and select dbs to determine blast algorithm
|
45
|
+
fireEvent.change(inputEl, { target: { value: AMINO_ACID_SEQUENCE } });
|
46
|
+
const proteinSelectAllBtn = screen.getByRole('heading', { name: /protein databases/i }).parentElement.querySelector('button');
|
47
|
+
fireEvent.click(proteinSelectAllBtn);
|
48
|
+
const modalButton = container.querySelector('[data-target="#help"]');
|
49
|
+
expect(modalButton).not.toBeNull();
|
50
|
+
});
|
51
|
+
});
|
52
|
+
|
53
|
+
describe('query stats', () => {
|
54
|
+
let csrfMetaTag;
|
55
|
+
|
56
|
+
beforeEach(() => {
|
57
|
+
csrfMetaTag = document.createElement('meta');
|
58
|
+
csrfMetaTag.setAttribute('name', '_csrf');
|
59
|
+
csrfMetaTag.setAttribute('content', 'test-token');
|
60
|
+
document.head.appendChild(csrfMetaTag);
|
61
|
+
});
|
62
|
+
|
63
|
+
afterEach(() => {
|
64
|
+
// Remove the CSRF meta tag after each test to clean up
|
65
|
+
document.head.removeChild(csrfMetaTag);
|
66
|
+
});
|
67
|
+
|
68
|
+
const getInputElement = () => screen.getByRole('textbox', { name: '' });
|
69
|
+
|
70
|
+
test('should render the query stats modal when clicked', () => {
|
71
|
+
const logSpy = jest.spyOn(global.console, 'log');
|
72
|
+
|
73
|
+
setMockJSONResult(data);
|
74
|
+
render(<Form onSequenceTypeChanged={() => { }} />);
|
75
|
+
|
76
|
+
expect(logSpy).toHaveBeenCalledTimes(1);
|
77
|
+
|
78
|
+
const inputEl = getInputElement();
|
79
|
+
// populate search and select dbs to determine blast algorithm
|
80
|
+
fireEvent.change(inputEl, { target: { value: AMINO_ACID_SEQUENCE } });
|
81
|
+
|
82
|
+
expect(logSpy).toHaveBeenCalledTimes(2);
|
83
|
+
|
84
|
+
const proteinSelectAllBtn = screen.getByRole('heading', { name: /protein databases/i }).parentElement.querySelector('button');
|
85
|
+
fireEvent.click(proteinSelectAllBtn);
|
86
|
+
|
87
|
+
expect(logSpy).toHaveBeenCalledTimes(4);
|
88
|
+
expect(logSpy).toHaveBeenCalledWith(
|
89
|
+
'Query stats:',
|
90
|
+
{
|
91
|
+
residuesInQuerySequence: 385,
|
92
|
+
numberOfDatabasesSelected: 4,
|
93
|
+
residuesInSelectedDbs: 4343318,
|
94
|
+
currentBlastMethod: 'blastp'
|
95
|
+
}
|
96
|
+
);
|
97
|
+
});
|
98
|
+
});
|
@@ -82,10 +82,10 @@
|
|
82
82
|
}
|
83
83
|
],
|
84
84
|
"options": {
|
85
|
-
"blastn": { "default": ["-task blastn", "-evalue 1e-5"] },
|
86
|
-
"blastp": { "default": ["-evalue 1e-5"] },
|
87
|
-
"blastx": { "default": ["-evalue 1e-5"] },
|
88
|
-
"tblastx": { "default": ["-evalue 1e-5"] },
|
89
|
-
"tblastn": { "default": ["-evalue 1e-5"] }
|
85
|
+
"blastn": { "default": { "attributes": ["-task blastn", "-evalue 1e-5"] }},
|
86
|
+
"blastp": { "default": { "attributes": ["-evalue 1e-5"] }},
|
87
|
+
"blastx": { "default": { "attributes": ["-evalue 1e-5"] }},
|
88
|
+
"tblastx": { "default": { "attributes": ["-evalue 1e-5"] }},
|
89
|
+
"tblastn": { "default": { "attributes": ["-evalue 1e-5"] }}
|
90
90
|
}
|
91
91
|
}
|
@@ -44,6 +44,7 @@ describe('REPORT PAGE', () => {
|
|
44
44
|
render(<Report showErrorModal={showErrorModal} />);
|
45
45
|
expect(showErrorModal).toHaveBeenCalledTimes(1);
|
46
46
|
});
|
47
|
+
|
47
48
|
it('it should render the report page correctly if there\'s a response provided', () => {
|
48
49
|
setMockJSONResult({ status: 200, responseJSON: shortResponseJSON });
|
49
50
|
const { container } = render(<Report getCharacterWidth={jest.fn()} />);
|
@@ -119,7 +120,7 @@ describe('REPORT PAGE', () => {
|
|
119
120
|
it('link for downloading alignment of specific number of selected hits should be disabled on initial load', () => {
|
120
121
|
const alignment_download_link = container.querySelector('.download-alignment-of-selected');
|
121
122
|
expect(alignment_download_link.classList.contains('disabled')).toBeTruthy();
|
122
|
-
|
123
|
+
|
123
124
|
});
|
124
125
|
it('should generate a blob url and filename for downloading alignment of specific number of selected hits', () => {
|
125
126
|
const alignment_download_link = container.querySelector('.download-alignment-of-selected');
|
@@ -132,7 +133,7 @@ describe('REPORT PAGE', () => {
|
|
132
133
|
expect(alignment_download_link.download).toEqual(file_name);
|
133
134
|
});
|
134
135
|
});
|
135
|
-
|
136
|
+
|
136
137
|
describe('FASTA DOWNLOAD', () => {
|
137
138
|
let fasta_download_link;
|
138
139
|
beforeEach(() => {
|
@@ -141,7 +142,7 @@ describe('REPORT PAGE', () => {
|
|
141
142
|
it('link for downloading fasta of selected number of hits should be disabled on initial load', () => {
|
142
143
|
expect(fasta_download_link.classList.contains('disabled')).toBeTruthy();
|
143
144
|
});
|
144
|
-
|
145
|
+
|
145
146
|
it('link for downloading fasta of specific number of selected hits should be active after selection', () => {
|
146
147
|
const checkboxes = container.querySelectorAll('.hit-links input[type="checkbox"]');
|
147
148
|
// SELECT 5 CHECKBOXES
|