sequenceserver 3.0.1 → 3.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/bin/sequenceserver +2 -2
  3. data/lib/sequenceserver/api_errors.rb +56 -2
  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/blast.rb +6 -0
  8. data/lib/sequenceserver/config.rb +54 -20
  9. data/lib/sequenceserver/database.rb +13 -0
  10. data/lib/sequenceserver/makeblastdb.rb +16 -2
  11. data/lib/sequenceserver/report.rb +0 -6
  12. data/lib/sequenceserver/routes.rb +66 -25
  13. data/lib/sequenceserver/sequence.rb +34 -7
  14. data/lib/sequenceserver/server.rb +1 -1
  15. data/lib/sequenceserver/version.rb +1 -1
  16. data/lib/sequenceserver.rb +1 -1
  17. data/public/404.html +1 -1
  18. data/public/css/app.css +121 -0
  19. data/public/css/app.min.css +1 -0
  20. data/public/css/sequenceserver.css +0 -148
  21. data/public/css/sequenceserver.min.css +3 -3
  22. data/public/js/circos.js +2 -2
  23. data/public/js/collapse_preferences.js +37 -0
  24. data/public/js/databases.js +65 -37
  25. data/public/js/databases_tree.js +2 -1
  26. data/public/js/dnd.js +37 -50
  27. data/public/js/download_fasta.js +1 -0
  28. data/public/js/form.js +79 -50
  29. data/public/js/grapher.js +23 -37
  30. data/public/js/hits_overview.js +2 -2
  31. data/public/js/kablammo.js +2 -2
  32. data/public/js/length_distribution.js +3 -3
  33. data/public/js/null_plugins/grapher/histogram.js +25 -0
  34. data/public/js/null_plugins/options.js +3 -0
  35. data/public/js/null_plugins/query_stats.js +11 -0
  36. data/public/js/null_plugins/report_plugins.js +6 -1
  37. data/public/js/null_plugins/search_header_plugin.js +4 -0
  38. data/public/js/options.js +161 -56
  39. data/public/js/query.js +85 -59
  40. data/public/js/report.js +1 -1
  41. data/public/js/search.js +2 -0
  42. data/public/js/search_button.js +67 -56
  43. data/public/js/sidebar.js +10 -1
  44. data/public/js/tests/database.spec.js +5 -5
  45. data/public/js/tests/form.spec.js +98 -0
  46. data/public/js/tests/mock_data/databases.json +5 -5
  47. data/public/js/tests/mocks/circos.js +6 -0
  48. data/public/js/tests/report.spec.js +4 -3
  49. data/public/js/tests/search_query.spec.js +16 -6
  50. data/public/sequenceserver-report.min.js +46 -24
  51. data/public/sequenceserver-search.min.js +57 -13
  52. data/public/sequenceserver_logo.webp +0 -0
  53. data/views/blastn_options.erb +66 -66
  54. data/views/blastp_options.erb +59 -59
  55. data/views/blastx_options.erb +68 -68
  56. data/views/layout.erb +61 -3
  57. data/views/search.erb +33 -38
  58. data/views/search_layout.erb +153 -0
  59. data/views/tblastn_options.erb +57 -57
  60. data/views/tblastx_options.erb +64 -64
  61. metadata +51 -22
  62. data/lib/sequenceserver/makeblastdb-modified-with-cache.rb +0 -345
  63. data/public/SequenceServer_logo.png +0 -0
  64. 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} 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
 
@@ -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, 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 }));
@@ -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
  }
@@ -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