sequenceserver 3.1.1 → 3.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/lib/sequenceserver/api_errors.rb +24 -0
  3. data/lib/sequenceserver/blast/tasks.rb +1 -1
  4. data/lib/sequenceserver/blast.rb +6 -0
  5. data/lib/sequenceserver/database.rb +13 -0
  6. data/lib/sequenceserver/routes.rb +1 -1
  7. data/lib/sequenceserver/sequence.rb +1 -2
  8. data/lib/sequenceserver/version.rb +1 -1
  9. data/lib/sequenceserver.rb +1 -1
  10. data/public/css/app.min.css +1 -1
  11. data/public/css/sequenceserver.css +0 -18
  12. data/public/css/sequenceserver.min.css +2 -2
  13. data/public/js/alignment_exporter.js +16 -28
  14. data/public/js/cloud_share_modal.js +42 -42
  15. data/public/js/form.js +12 -10
  16. data/public/js/grapher.js +4 -4
  17. data/public/js/hit.js +3 -3
  18. data/public/js/hits.js +276 -0
  19. data/public/js/jquery_world.js +1 -1
  20. data/public/js/mailto.js +1 -3
  21. data/public/js/null_plugins/report_plugins.js +1 -0
  22. data/public/js/options.js +2 -6
  23. data/public/js/query.js +1 -1
  24. data/public/js/report.js +68 -252
  25. data/public/js/report_root.js +7 -5
  26. data/public/js/search.js +28 -11
  27. data/public/js/sequence.js +158 -158
  28. data/public/js/sequence_modal.js +28 -36
  29. data/public/js/sidebar.js +7 -6
  30. data/public/js/tests/alignment_exporter.spec.js +38 -0
  31. data/public/js/tests/cloud_share_modal.spec.js +75 -0
  32. data/public/js/tests/report.spec.js +37 -15
  33. data/public/packages/jquery-ui@1.13.3.js +19070 -0
  34. data/public/sequenceserver-report.min.js +3 -2481
  35. data/public/sequenceserver-report.min.js.LICENSE.txt +300 -0
  36. data/public/sequenceserver-report.min.js.map +1 -0
  37. data/public/sequenceserver-search.min.js +3 -2382
  38. data/public/sequenceserver-search.min.js.LICENSE.txt +292 -0
  39. data/public/sequenceserver-search.min.js.map +1 -0
  40. data/views/layout.erb +3 -7
  41. data/views/search.erb +1 -1
  42. data/views/search_layout.erb +1 -1
  43. metadata +11 -5
  44. data/public/config.js +0 -147
  45. 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