sequenceserver 3.0.1 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/bin/sequenceserver +2 -2
  3. data/lib/sequenceserver/api_errors.rb +1 -1
  4. data/lib/sequenceserver/blast/job.rb +20 -3
  5. data/lib/sequenceserver/blast/report.rb +74 -86
  6. data/lib/sequenceserver/blast/tasks.rb +38 -0
  7. data/lib/sequenceserver/config.rb +54 -20
  8. data/lib/sequenceserver/makeblastdb.rb +16 -2
  9. data/lib/sequenceserver/report.rb +0 -6
  10. data/lib/sequenceserver/routes.rb +32 -21
  11. data/lib/sequenceserver/version.rb +1 -1
  12. data/lib/sequenceserver.rb +1 -1
  13. data/public/css/app.css +121 -0
  14. data/public/css/app.min.css +1 -0
  15. data/public/css/sequenceserver.css +0 -148
  16. data/public/css/sequenceserver.min.css +3 -3
  17. data/public/js/circos.js +2 -2
  18. data/public/js/collapse_preferences.js +37 -0
  19. data/public/js/databases.js +65 -37
  20. data/public/js/databases_tree.js +2 -1
  21. data/public/js/dnd.js +37 -50
  22. data/public/js/form.js +78 -50
  23. data/public/js/grapher.js +23 -37
  24. data/public/js/hits_overview.js +2 -2
  25. data/public/js/kablammo.js +2 -2
  26. data/public/js/length_distribution.js +3 -3
  27. data/public/js/null_plugins/grapher/histogram.js +25 -0
  28. data/public/js/null_plugins/options.js +3 -0
  29. data/public/js/null_plugins/query_stats.js +11 -0
  30. data/public/js/null_plugins/report_plugins.js +6 -1
  31. data/public/js/null_plugins/search_header_plugin.js +4 -0
  32. data/public/js/options.js +161 -56
  33. data/public/js/query.js +85 -59
  34. data/public/js/report.js +1 -1
  35. data/public/js/search.js +2 -0
  36. data/public/js/search_button.js +67 -56
  37. data/public/js/sidebar.js +1 -1
  38. data/public/js/tests/database.spec.js +5 -5
  39. data/public/js/tests/{advanced_parameters.spec.js → form.spec.js} +35 -1
  40. data/public/js/tests/mock_data/databases.json +5 -5
  41. data/public/js/tests/mocks/circos.js +6 -0
  42. data/public/js/tests/report.spec.js +4 -3
  43. data/public/js/tests/search_query.spec.js +5 -6
  44. data/public/sequenceserver-report.min.js +45 -23
  45. data/public/sequenceserver-search.min.js +57 -13
  46. data/public/sequenceserver_logo.webp +0 -0
  47. data/views/blastn_options.erb +66 -66
  48. data/views/blastp_options.erb +59 -59
  49. data/views/blastx_options.erb +68 -68
  50. data/views/layout.erb +60 -3
  51. data/views/search.erb +33 -38
  52. data/views/search_layout.erb +152 -0
  53. data/views/tblastn_options.erb +57 -57
  54. data/views/tblastx_options.erb +64 -64
  55. metadata +31 -22
  56. data/lib/sequenceserver/makeblastdb-modified-with-cache.rb +0 -345
  57. data/public/SequenceServer_logo.png +0 -0
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} collapsed="true" />
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
- $('.notifications .active').hide('drop', { direction: 'up' }).removeClass('active');
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
- // $('.notifications .active').hide().removeClass('active');
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
- $('.notifications .active').hide('drop', { direction: 'up' }).removeClass('active');
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="form-control text-monospace"
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="btn btn-sm btn-default" id="btn-sequence-clear"
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
- render() {
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" data-toggle="collapse" data-target={'#Query_' + this.props.query.number + 'HT_' + this.props.query.number}>
391
- <i className="fa fa-minus-square-o"></i>&nbsp;
392
- <span>Hit sequences producing significant alignments</span>
456
+ <h4 className="caption" onClick={() => this.collapsePreferences.toggleCollapse()}>
457
+ {this.collapsePreferences.renderCollapseIcon()}
458
+ <span> {this.name}</span>
393
459
  </h4>
394
- <div className="collapsed in" id={'Query_' + this.props.query.number + 'HT_' + this.props.query.number}>
395
- <table
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>
@@ -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.length > 0) {
36
- this.inputGroup().wiggle();
37
- this.props.onAlgoChanged(this.state.methods[0]);
38
- } else {
39
- this.props.onAlgoChanged('');
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 className="col-md-3">
141
- <div className="form-group">
142
- <div className="col-md-12">
143
- <div
144
- className={multi ? 'input-group' : ''}
145
- id="methods"
146
- ref={this.inputGroupRef}
147
- onMouseOver={this.showTooltip}
148
- onMouseOut={this.hideTooltip}
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
- <button
151
- type="submit"
152
- className="btn btn-primary form-control text-uppercase"
153
- id="method"
154
- ref={this.submitButtonRef}
155
- name="method"
156
- value={method}
157
- disabled={!method}
158
- >
159
- {this.decorate(method || 'blast')}
160
- </button>
161
- {multi && (
162
- <div className="input-group-btn">
163
- <button
164
- className="btn btn-primary dropdown-toggle"
165
- data-toggle="dropdown"
166
- >
167
- <span className="caret"></span>
168
- </button>
169
- <ul className="dropdown-menu dropdown-menu-right">
170
- {_.map(
171
- methods.slice(1),
172
- _.bind(function (method) {
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
- </div>
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('destroy');
201
+ $('#copyURL')._tooltip('hide');
202
202
  }, 3000);
203
203
  }
204
204
 
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable no-unused-vars */
2
2
  /* eslint-disable no-undef */
3
- import { render, screen, fireEvent, waitFor, findByText, getByText } from '@testing-library/react';
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 }));
@@ -11,6 +11,7 @@ import '@testing-library/react/dont-cleanup-after-each';
11
11
  export const setMockJSONResult = (result) => {
12
12
  global.$.getJSON = (_, cb) => cb(result);
13
13
  };
14
+
14
15
  describe('ADVANCED PARAMETERS', () => {
15
16
  const getInputElement = () => screen.getByRole('textbox', { name: '' });
16
17
  test('should not render the link to advanced parameters modal if blast algorithm is unknown', () => {
@@ -24,7 +25,7 @@ describe('ADVANCED PARAMETERS', () => {
24
25
  setMockJSONResult(data);
25
26
  const {container } =render(<Form onSequenceTypeChanged={() => { }
26
27
  } />);
27
-
28
+
28
29
  const inputEl = getInputElement();
29
30
  // populate search and select dbs to determine blast algorithm
30
31
  fireEvent.change(inputEl, { target: { value: AMINO_ACID_SEQUENCE } });
@@ -34,3 +35,36 @@ describe('ADVANCED PARAMETERS', () => {
34
35
  expect(modalButton).not.toBeNull();
35
36
  });
36
37
  });
38
+
39
+ describe('query stats', () => {
40
+ const getInputElement = () => screen.getByRole('textbox', { name: '' });
41
+
42
+ test('should render the query stats modal when clicked', () => {
43
+ const logSpy = jest.spyOn(global.console, 'log');
44
+
45
+ setMockJSONResult(data);
46
+ render(<Form onSequenceTypeChanged={() => { }} />);
47
+
48
+ expect(logSpy).toHaveBeenCalledTimes(1);
49
+
50
+ const inputEl = getInputElement();
51
+ // populate search and select dbs to determine blast algorithm
52
+ fireEvent.change(inputEl, { target: { value: AMINO_ACID_SEQUENCE } });
53
+
54
+ expect(logSpy).toHaveBeenCalledTimes(2);
55
+
56
+ const proteinSelectAllBtn = screen.getByRole('heading', { name: /protein databases/i }).parentElement.querySelector('button');
57
+ fireEvent.click(proteinSelectAllBtn);
58
+
59
+ expect(logSpy).toHaveBeenCalledTimes(4);
60
+ expect(logSpy).toHaveBeenCalledWith(
61
+ 'Query stats:',
62
+ {
63
+ residuesInQuerySequence: 385,
64
+ numberOfDatabasesSelected: 4,
65
+ residuesInSelectedDbs: 4343318,
66
+ currentBlastMethod: 'blastp'
67
+ }
68
+ );
69
+ });
70
+ });
@@ -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
  }
@@ -0,0 +1,6 @@
1
+ const Circos = () => {
2
+ // console.log("Circos mock loaded");
3
+ return <div></div>;
4
+ };
5
+
6
+ export default Circos;
@@ -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
@@ -1,7 +1,6 @@
1
1
  /* eslint-disable no-unused-vars */
2
2
  /* eslint-disable no-undef */
3
3
  import { render, screen, fireEvent } from '@testing-library/react';
4
- import { SearchQueryWidget } from '../query';
5
4
  import { Form } from '../form';
6
5
  import { AMINO_ACID_SEQUENCE, NUCLEOTIDE_SEQUENCE, FASTQ_SEQUENCE, FASTA_OF_FASTQ_SEQUENCE } from './mock_data/sequences';
7
6
  import '@testing-library/jest-dom/extend-expect';
@@ -18,7 +17,7 @@ describe('SEARCH COMPONENT', () => {
18
17
  });
19
18
 
20
19
  test('should render the search component textarea', () => {
21
- expect(inputEl).toHaveClass('form-control');
20
+ expect(inputEl).toBeInTheDocument();
22
21
  });
23
22
 
24
23
  test('clear button should only become visible if textarea is not empty', () => {
@@ -33,7 +32,7 @@ describe('SEARCH COMPONENT', () => {
33
32
  test('should correctly detect the amino-acid sequence type and show notification', () => {
34
33
  // populate search
35
34
  fireEvent.change(inputEl, { target: { value: AMINO_ACID_SEQUENCE } });
36
- const activeNotification = container.querySelector('.notification.active');
35
+ const activeNotification = container.querySelector('[data-role=notification].active');
37
36
  expect(activeNotification.id).toBe('protein-sequence-notification');
38
37
  const alertWrapper = activeNotification.children[0];
39
38
  expect(alertWrapper).toHaveTextContent('Detected: amino-acid sequence(s).');
@@ -42,7 +41,7 @@ describe('SEARCH COMPONENT', () => {
42
41
  test('should correctly detect the nucleotide sequence type and show notification', () => {
43
42
  // populate search
44
43
  fireEvent.change(inputEl, { target: { value: NUCLEOTIDE_SEQUENCE } });
45
- const activeNotification = container.querySelector('.notification.active');
44
+ const activeNotification = container.querySelector('[data-role=notification].active');
46
45
  const alertWrapper = activeNotification.children[0];
47
46
  expect(activeNotification.id).toBe('nucleotide-sequence-notification');
48
47
  expect(alertWrapper).toHaveTextContent('Detected: nucleotide sequence(s).');
@@ -50,7 +49,7 @@ describe('SEARCH COMPONENT', () => {
50
49
 
51
50
  test('should correctly detect the mixed sequences and show error notification', () => {
52
51
  fireEvent.change(inputEl, { target: { value: `${NUCLEOTIDE_SEQUENCE}${AMINO_ACID_SEQUENCE}` } });
53
- const activeNotification = container.querySelector('.notification.active');
52
+ const activeNotification = container.querySelector('[data-role=notification].active');
54
53
  expect(activeNotification.id).toBe('mixed-sequence-notification');
55
54
  const alertWrapper = activeNotification.children[0];
56
55
  expect(alertWrapper).toHaveTextContent('Error: mixed nucleotide and amino-acid sequences detected.');
@@ -58,7 +57,7 @@ describe('SEARCH COMPONENT', () => {
58
57
 
59
58
  test('should correctly detect FASTQ and convert it to FASTA', () => {
60
59
  fireEvent.change(inputEl, { target: { value: FASTQ_SEQUENCE } });
61
- const activeNotification = container.querySelector('.notification.active');
60
+ const activeNotification = container.querySelector('[data-role=notification].active');
62
61
  const alertWrapper = activeNotification.children[0];
63
62
  expect(activeNotification.id).toBe('fastq-sequence-notification');
64
63
  expect(alertWrapper).toHaveTextContent('Detected FASTQ and automatically converted to FASTA.');