sequenceserver 3.0 → 3.1.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.

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.');