sequenceserver 2.1.0 → 3.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of sequenceserver might be problematic. Click here for more details.

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