sequenceserver 3.1.2 → 3.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/lib/sequenceserver/blast/tasks.rb +1 -1
  3. data/lib/sequenceserver/routes.rb +1 -1
  4. data/lib/sequenceserver/version.rb +1 -1
  5. data/lib/sequenceserver.rb +1 -1
  6. data/public/css/app.min.css +1 -1
  7. data/public/css/sequenceserver.css +0 -18
  8. data/public/css/sequenceserver.min.css +2 -2
  9. data/public/js/alignment_exporter.js +16 -28
  10. data/public/js/cloud_share_modal.js +42 -42
  11. data/public/js/form.js +12 -10
  12. data/public/js/grapher.js +4 -4
  13. data/public/js/hit.js +3 -3
  14. data/public/js/hits.js +276 -0
  15. data/public/js/jquery_world.js +1 -1
  16. data/public/js/mailto.js +1 -3
  17. data/public/js/null_plugins/report_plugins.js +1 -0
  18. data/public/js/options.js +2 -6
  19. data/public/js/query.js +1 -1
  20. data/public/js/report.js +68 -252
  21. data/public/js/report_root.js +7 -5
  22. data/public/js/search.js +28 -11
  23. data/public/js/sequence.js +158 -158
  24. data/public/js/sequence_modal.js +28 -36
  25. data/public/js/sidebar.js +7 -6
  26. data/public/js/tests/alignment_exporter.spec.js +38 -0
  27. data/public/js/tests/cloud_share_modal.spec.js +75 -0
  28. data/public/js/tests/report.spec.js +37 -15
  29. data/public/packages/jquery-ui@1.13.3.js +19070 -0
  30. data/public/sequenceserver-report.min.js +3 -2481
  31. data/public/sequenceserver-report.min.js.LICENSE.txt +300 -0
  32. data/public/sequenceserver-report.min.js.map +1 -0
  33. data/public/sequenceserver-search.min.js +3 -2382
  34. data/public/sequenceserver-search.min.js.LICENSE.txt +292 -0
  35. data/public/sequenceserver-search.min.js.map +1 -0
  36. data/views/layout.erb +3 -7
  37. data/views/search.erb +1 -1
  38. data/views/search_layout.erb +1 -1
  39. metadata +11 -5
  40. data/public/config.js +0 -147
  41. data/public/packages/jquery-ui@1.11.4.js +0 -16624
@@ -63,48 +63,38 @@ export default class SequenceModal extends React.Component {
63
63
  /**
64
64
  * Loads sequence using AJAX and updates modal state.
65
65
  */
66
- loadJSON(url) {
66
+ async loadJSON(url) {
67
67
  // Fetch sequence and update state.
68
- $.getJSON(url)
69
- .done(
70
- _.bind(function (response) {
71
- this.setState({
72
- sequences: response.sequences,
73
- error_msgs: response.error_msgs,
74
- requestCompleted: true,
75
- });
76
- }, this)
77
- )
78
- .fail((jqXHR, status, error) => {
79
- this.hide();
80
- this.props.showErrorModal(jqXHR.responseJSON);
68
+ try {
69
+ const response = await $.getJSON(url);
70
+ this.setState({
71
+ sequences: response.sequences,
72
+ error_msgs: response.error_msgs,
73
+ requestCompleted: true,
81
74
  });
75
+ } catch (error) {
76
+ console.log('Error fetching sequence:', error);
77
+ this.hide();
78
+ this.props.showErrorModal(error.responseJSON);
79
+ }
82
80
  }
83
81
 
84
82
  resultsJSX() {
85
83
  return (
86
84
  <div className="modal-body">
87
- {_.map(
88
- this.state.error_msgs,
89
- _.bind(function (error_msg) {
90
- return (
91
- <div className="fastan">
92
- <div className="section-header">
93
- <h4>{error_msg[0]}</h4>
94
- </div>
95
- <div className="section-content">
96
- <pre className="pre-reset">{error_msg[1]}</pre>
97
- </div>
98
- </div>
99
- );
100
- }, this)
101
- )}
102
- {_.map(
103
- this.state.sequences,
104
- _.bind(function (sequence) {
105
- return <SequenceViewer sequence={sequence} />;
106
- }, this)
107
- )}
85
+ {this.state.error_msgs.map((error_msg, index) => (
86
+ <div key={`error-message-${index}`} className="fastan">
87
+ <div className="section-header">
88
+ <h4>{error_msg[0]}</h4>
89
+ </div>
90
+ <div className="section-content">
91
+ <pre className="pre-reset">{error_msg[1]}</pre>
92
+ </div>
93
+ </div>
94
+ ))}
95
+ {this.state.sequences.map((sequence, index) => (
96
+ <SequenceViewer key={`sequence-viewer-${index}`} sequence={sequence} />
97
+ ))}
108
98
  </div>
109
99
  );
110
100
  }
@@ -160,6 +150,8 @@ class SequenceViewer extends React.Component {
160
150
  footer: false,
161
151
  },
162
152
  });
163
- widget.hideFormatSelector();
153
+ setTimeout(function() {
154
+ requestAnimationFrame(() => { widget.hideFormatSelector() }); // ensure React is done painting the DOM of the element before calling a function on it.
155
+ });
164
156
  }
165
157
  }
data/public/js/sidebar.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Component } from 'react';
1
+ import React, { Component } from 'react';
2
2
  import _ from 'underscore';
3
3
 
4
4
  import downloadFASTA from './download_fasta';
@@ -32,6 +32,7 @@ export default class extends Component {
32
32
  this.copyURL = this.copyURL.bind(this);
33
33
  this.shareCloudInit = this.shareCloudInit.bind(this);
34
34
  this.sharingPanelJSX = this.sharingPanelJSX.bind(this);
35
+ this.cloudShareModal = React.createRef();
35
36
  this.timeout = null;
36
37
  this.queryElems = [];
37
38
  this.state = {
@@ -203,7 +204,7 @@ export default class extends Component {
203
204
  }
204
205
 
205
206
  shareCloudInit() {
206
- this.refs.cloudShareModal.show();
207
+ this.cloudShareModal.current.show();
207
208
  }
208
209
 
209
210
  topPanelJSX() {
@@ -353,9 +354,9 @@ export default class extends Component {
353
354
  {
354
355
  !this.props.data.imported_xml && <li>
355
356
  <a className="btn-link download" data-toggle="tooltip"
356
- title="Results in pairwise format."
357
+ title="Results in text format."
357
358
  href={'download/' + this.props.data.search_id + '.pairwise'}>
358
- Full Pairwise report
359
+ Full Text report
359
360
  </a>
360
361
  </li>
361
362
  }
@@ -403,7 +404,7 @@ export default class extends Component {
403
404
  </ul>
404
405
  {
405
406
  <CloudShareModal
406
- ref="cloudShareModal"
407
+ ref={this.cloudShareModal}
407
408
  querydb={this.props.data.querydb}
408
409
  program={this.props.data.program}
409
410
  queryLength={this.props.data.queries.length}
@@ -422,7 +423,7 @@ export default class extends Component {
422
423
  <div className="referral-panel">
423
424
  <div className="section-header-sidebar">
424
425
  <h4>Recommend SequenceServer</h4>
425
- <p><a href="https://sequenceserver.com/referral-program" target="_blank">Earn up to $100 per signup</a></p>
426
+ <p><a href="https://sequenceserver.com/referral-program" target="_blank">Earn up to $400 per signup</a></p>
426
427
  </div>
427
428
  </div>
428
429
  </div>
@@ -0,0 +1,38 @@
1
+ import AlignmentExporter from '../alignment_exporter';
2
+
3
+ describe('AlignmentExporter', () => {
4
+ let exporter;
5
+
6
+ beforeEach(() => {
7
+ exporter = new AlignmentExporter();
8
+ });
9
+
10
+ describe('wrap_string', () => {
11
+ it('wraps a string to a specified width', () => {
12
+ const str = 'abcdefghijklmnopqrstuvwxyz';
13
+ const width = 5;
14
+ const expected = 'abcde\nfghij\nklmno\npqrst\nuvwxy\nz';
15
+ expect(exporter.wrap_string(str, width)).toBe(expected);
16
+ });
17
+ });
18
+
19
+ describe('generate_fasta', () => {
20
+ it('generates a fasta string from hsps', () => {
21
+ const hsps = [
22
+ {
23
+ query_id: 'query1',
24
+ qstart: 1,
25
+ qend: 10,
26
+ qseq: 'ATGCATGCAT',
27
+ hit_id: 'hit1',
28
+ sstart: 1,
29
+ send: 10,
30
+ midline: '||||||||||',
31
+ sseq: 'ATGCATGCAT',
32
+ },
33
+ ];
34
+ const expected = '>query1:1-10\nATGCATGCAT\n>query1:1-10_alignment_hit1:1-10\n||||||||||\n>hit1:1-10\nATGCATGCAT\n';
35
+ expect(exporter.generate_fasta(hsps)).toBe(expected);
36
+ });
37
+ });
38
+ });
@@ -0,0 +1,75 @@
1
+ import { render, fireEvent, waitFor } from '@testing-library/react';
2
+ import CloudShareModal, { handleSubmit } from '../cloud_share_modal';
3
+
4
+ describe('CloudShareModal', () => {
5
+ let component;
6
+
7
+ beforeEach(() => {
8
+ global.fetch = jest.fn(() =>
9
+ Promise.resolve({
10
+ json: () => Promise.resolve({ shareable_url: 'http://test.com' }),
11
+ ok: true
12
+ })
13
+ );
14
+ component = render(<CloudShareModal querydb="" program="" queryLength="0" />);
15
+ });
16
+
17
+ it('renders without crashing', () => {
18
+ expect(component).toBeTruthy();
19
+ });
20
+
21
+ it('initial state is correct', () => {
22
+ const { getByLabelText } = component;
23
+ const emailInput = getByLabelText('Your Email Address');
24
+ const tosCheckbox = getByLabelText('I agree to the Terms and Conditions of Service');
25
+
26
+ expect(emailInput.value).toBe('');
27
+ expect(tosCheckbox.checked).toBe(false);
28
+ });
29
+
30
+ it('handles form submission', async () => {
31
+ const { getByLabelText, getByText, getByDisplayValue } = component;
32
+
33
+ // Fill out the form
34
+ fireEvent.change(getByLabelText(/Your Email Address/i), { target: { value: 'test@test.com' } });
35
+ fireEvent.click(getByLabelText(/I agree to the/i));
36
+
37
+ // Submit the form
38
+ fireEvent.click(getByText('Submit'));
39
+
40
+ // Wait for the loading state to finish
41
+ await waitFor(() => getByText('Uploading the job to SequenceServer Cloud, please wait...'));
42
+
43
+ // Check that the results are displayed
44
+ await waitFor(() => getByText('Copy to Clipboard'));
45
+
46
+ expect(getByDisplayValue(/http:\/\/test.com/i)).toBeTruthy()
47
+ });
48
+
49
+ it('handles form submission errors', async () => {
50
+ // Override the fetch mock to simulate a server error
51
+ global.fetch = jest.fn(() =>
52
+ Promise.resolve({
53
+ json: () => Promise.resolve({ errors: ['Test error'] }),
54
+ ok: false
55
+ })
56
+ );
57
+
58
+ const { getByLabelText, getByText } = component;
59
+
60
+ // Fill out the form
61
+ fireEvent.change(getByLabelText(/Your Email Address/i), { target: { value: 'test@test.com' } });
62
+ fireEvent.click(getByLabelText(/I agree to the/i));
63
+
64
+ // Submit the form
65
+ fireEvent.click(getByText('Submit'));
66
+
67
+ // Wait for the loading state to finish
68
+ await waitFor(() => getByText('Uploading the job to SequenceServer Cloud, please wait...'));
69
+
70
+ // Check that the results are displayed
71
+ await waitFor(() => getByText('Network response was not ok'));
72
+
73
+ expect(getByText(/Network response was not ok/i)).toBeTruthy();
74
+ });
75
+ });
@@ -1,13 +1,20 @@
1
1
  /* eslint-disable no-undef */
2
2
  /* eslint-disable no-unused-vars */
3
3
  import { render, screen, fireEvent } from '@testing-library/react';
4
+ import { act } from 'react';
4
5
  import Report from '../report';
5
6
  import Sidebar from '../sidebar';
6
7
  import shortResponseJSON from './mock_data/short_response.json';
7
8
  import longResponseJSON from './mock_data/long_response.json';
8
9
 
9
10
  const setMockJSONResult = (result) => {
10
- global.$.getJSON = () => ({ complete: jest.fn((callback) => callback(result)) });
11
+ global.fetch = jest.fn(() =>
12
+ Promise.resolve({
13
+ ok: result.status === 200,
14
+ status: result.status,
15
+ text: () => Promise.resolve(JSON.stringify(result.responseJSON)),
16
+ })
17
+ );
11
18
  };
12
19
 
13
20
  const nextQueryButton = () => screen.queryByRole('button', { name: /next query/i });
@@ -38,19 +45,30 @@ describe('REPORT PAGE', () => {
38
45
  expect(screen.getByRole('heading', { name: 'BLAST-ing' })).toBeInTheDocument();
39
46
  });
40
47
 
41
- it('should show error modal if error occurs while fetching queries', () => {
48
+ it('should show error modal if error occurs while fetching queries', async () => {
42
49
  const showErrorModal = jest.fn();
43
- setMockJSONResult({ status: 500 });
44
- render(<Report showErrorModal={showErrorModal} />);
50
+
51
+ setMockJSONResult({ status: 500, responseJSON: { error: "Internal Server Error" }});
52
+
53
+ await act(async () => {
54
+ render(<Report showErrorModal={showErrorModal} />);
55
+ });
56
+
45
57
  expect(showErrorModal).toHaveBeenCalledTimes(1);
46
58
  });
47
59
 
48
- it('it should render the report page correctly if there\'s a response provided', () => {
60
+ it('it should render the report page correctly if there\'s a response provided', async () => {
49
61
  setMockJSONResult({ status: 200, responseJSON: shortResponseJSON });
50
- const { container } = render(<Report getCharacterWidth={jest.fn()} />);
51
- expect(container.querySelector('#results')).toBeInTheDocument();
52
62
 
63
+ let container;
64
+ await act(async () => {
65
+ const result = render(<Report getCharacterWidth={jest.fn()} />);
66
+ container = result.container;
67
+ });
68
+
69
+ expect(container.querySelector('#results')).toBeInTheDocument();
53
70
  });
71
+
54
72
  describe('SIDEBAR', () => {
55
73
  it('should render the sidebar component with correct heading', () => {
56
74
  setMockJSONResult({ status: 200, responseJSON: shortResponseJSON });
@@ -105,23 +123,27 @@ describe('REPORT PAGE', () => {
105
123
 
106
124
  describe('DOWNLOAD LINKS', () => {
107
125
  let container;
108
- beforeEach(() => {
126
+ beforeEach(async () => {
109
127
  setMockJSONResult({ status: 200, responseJSON: shortResponseJSON });
110
- container = render(<Report getCharacterWidth={jest.fn()} />).container;
128
+ await act(async () => {
129
+ container = render(<Report getCharacterWidth={jest.fn()} />).container;
130
+ });
111
131
  });
112
132
  describe('ALIGNMENT DOWNLOAD', () => {
113
133
  it('should generate a blob url and filename for downloading alignment of all hits on render', () => {
114
- const alignment_download_link = container.querySelector('.download-alignment-of-all');
115
- const expected_num_hits = container.querySelectorAll('.hit-links input[type="checkbox"]').length;
116
- const file_name = `alignment-${expected_num_hits}_hits.txt`;
117
- expect(alignment_download_link.download).toEqual(file_name);
118
- expect(alignment_download_link.hred).not.toEqual('#');
134
+ const alignmentDownloadLink = container.querySelector('.download-alignment-of-all');
135
+ const hitLinks = container.querySelectorAll('.hit-links input[type="checkbox"]');
136
+ const expectedNumHits = hitLinks.length;
137
+ const fileName = `alignment-${expectedNumHits}_hits.txt`;
138
+ expect(alignmentDownloadLink.download).toEqual(fileName);
139
+ expect(alignmentDownloadLink.href).not.toEqual('#');
119
140
  });
141
+
120
142
  it('link for downloading alignment of specific number of selected hits should be disabled on initial load', () => {
121
143
  const alignment_download_link = container.querySelector('.download-alignment-of-selected');
122
144
  expect(alignment_download_link.classList.contains('disabled')).toBeTruthy();
123
-
124
145
  });
146
+
125
147
  it('should generate a blob url and filename for downloading alignment of specific number of selected hits', () => {
126
148
  const alignment_download_link = container.querySelector('.download-alignment-of-selected');
127
149
  // QUERY ALL HIT LINKS CHECKBOXES