From 5a332b2d7d88ffdfc9ac1ca1aff8e0d441240ffa Mon Sep 17 00:00:00 2001 From: Tadas Tamosauskas Date: Fri, 7 Jul 2023 18:44:46 +0100 Subject: [PATCH 1/5] gitignore spec dirs that are left dirty after running specs --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 83e881e62..c1523b18c 100644 --- a/.gitignore +++ b/.gitignore @@ -15,7 +15,7 @@ doc # jeweler generated pkg -# Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore: +# Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore: # # * Create a file at ~/.gitignore # * Include files you want ignored @@ -54,3 +54,6 @@ pkg .gems/* Gemfile.lock node_modules + +spec/dotdir/* +spec/downloads/* \ No newline at end of file From 7f89a885fdff1ca5e63169be3c5b098cbddb3839 Mon Sep 17 00:00:00 2001 From: Tadas Tamosauskas Date: Fri, 7 Jul 2023 18:46:28 +0100 Subject: [PATCH 2/5] Update Capybara config to use chrome Bump capybara and selenium versions, new selenium comes with a driver manager so it's a lot easier to ensure test drivers are installed correctly. Chrome is easier to set up and more common amongst users. --- Gemfile.lock | 24 ++++++++-------- spec/download_helper.rb | 4 ++- spec/spec_helper.rb | 63 +++++++++++++++++++++-------------------- 3 files changed, 48 insertions(+), 43 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index c3cbd2c59..0a54d6aa2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -14,7 +14,7 @@ GEM addressable (2.8.4) public_suffix (>= 2.0.2, < 6.0) ast (2.4.2) - capybara (3.39.0) + capybara (3.39.2) addressable matrix mini_mime (>= 0.1.3) @@ -23,7 +23,7 @@ GEM rack-test (>= 0.6.3) regexp_parser (>= 1.5, < 3.0) xpath (~> 3.2) - capybara-screenshot (1.0.24) + capybara-screenshot (1.0.26) capybara (>= 1.0, < 4) launchy childprocess (3.0.0) @@ -32,16 +32,16 @@ GEM docile (1.3.2) json (2.3.0) json_pure (2.6.3) - launchy (2.4.3) - addressable (~> 2.3) + launchy (2.5.2) + addressable (~> 2.8) matrix (0.4.2) method_source (1.0.0) mini_mime (1.1.2) - mini_portile2 (2.8.1) + mini_portile2 (2.8.2) mustermann (2.0.2) ruby2_keywords (~> 0.0.1) - nokogiri (1.14.3) - mini_portile2 (~> 2.8.0) + nokogiri (1.15.3) + mini_portile2 (~> 2.8.2) racc (~> 1.4) ox (2.14.16) parallel (1.23.0) @@ -51,8 +51,8 @@ GEM coderay (~> 1.1) method_source (~> 1.0) public_suffix (5.0.1) - racc (1.6.2) - rack (2.2.6.4) + racc (1.7.1) + rack (2.2.7) rack-protection (2.2.4) rack rack-test (1.1.0) @@ -64,15 +64,15 @@ GEM rspec-core (~> 3.9.0) rspec-expectations (~> 3.9.0) rspec-mocks (~> 3.9.0) - rspec-core (3.9.1) - rspec-support (~> 3.9.1) + rspec-core (3.9.3) + rspec-support (~> 3.9.3) rspec-expectations (3.9.0) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.9.0) rspec-mocks (3.9.1) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.9.0) - rspec-support (3.9.2) + rspec-support (3.9.4) rubocop (1.50.2) json (~> 2.3) parallel (~> 1.10) diff --git a/spec/download_helper.rb b/spec/download_helper.rb index b48f25740..e868831a7 100644 --- a/spec/download_helper.rb +++ b/spec/download_helper.rb @@ -1,7 +1,9 @@ # Based on https://stackoverflow.com/a/29544674 module DownloadHelpers + DOWNLOADS_DIR = File.join(__dir__, 'downloads') + def downloads_dir - File.join(__dir__, 'downloads') + DOWNLOADS_DIR end def wait_for_download diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 3246fe2e7..ebea66d6e 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,7 +1,7 @@ require 'simplecov' require 'capybara/rspec' -require 'capybara-screenshot/rspec' require 'selenium-webdriver' +require 'capybara-screenshot/rspec' require_relative 'download_helper' @@ -15,6 +15,37 @@ # For the purpose of testing, set DOTDIR to spec/dotdir. SequenceServer::DOTDIR = File.join(__dir__, 'dotdir') +Capybara.app = SequenceServer +Capybara.server = :webrick +Capybara.default_max_wait_time = 15 + +chrome_options = Selenium::WebDriver::Chrome::Options.new +chrome_options.add_preference('download.default_directory', DownloadHelpers::DOWNLOADS_DIR) +chrome_options.add_preference('profile.default_content_setting_values.automatic_downloads', 1) +chrome_options.add_argument("--window-size=1920,1200") + +Capybara.register_driver :chrome do |app| + Capybara::Selenium::Driver.new( + app, + browser: :chrome, + options: chrome_options + ) +end + +Capybara.register_driver :headless_chrome do |app| + options = chrome_options.dup + options.add_argument('--headless') + + Capybara::Selenium::Driver.new( + app, + browser: :chrome, + options: options + ) +end + +Capybara.default_driver = ENV['BROWSER_DEBUG'] ? :chrome : :headless_chrome +Capybara.javascript_driver = ENV['BROWSER_DEBUG'] ? :chrome : :headless_chrome + RSpec.configure do |config| # Explicitly enable should syntax of rspec. config.expect_with :rspec do |expectations| @@ -27,37 +58,9 @@ # For file downloading. config.include DownloadHelpers, type: :feature - # Check if geckodriver is installed for capybara tests. - def geckodriver_installed? - # geckodriver 0.31.0 - system('geckodriver -V') - end - # Setup capybara tests. config.before :context, type: :feature do |context| - context.skip('Install geckodriver first. Try: sudo apt install firefox-geckodriver.') unless geckodriver_installed? - Capybara.app = SequenceServer - Capybara.server = :webrick - Capybara.default_max_wait_time = 30 - - Capybara.register_driver :selenium do |app| - options = Selenium::WebDriver::Firefox::Options.new - - # Run the browser in headless mode. - options.args << '--headless' - - # Tell the browser where to save downloaded files. - options.profile = Selenium::WebDriver::Firefox::Profile.new - options.profile['browser.download.dir'] = downloads_dir - options.profile['browser.download.folderList'] = 2 - - # Suppress "open with / save" dialog for FASTA, XML, TSV and PNG file types. - options.profile['browser.helperApps.neverAsk.saveToDisk'] = - 'text/fasta,text/xml,text/tsv,image/png' - Capybara::Selenium::Driver.new(app, browser: :firefox, options: options) - end - - FileUtils.mkdir_p downloads_dir + FileUtils.mkdir_p DownloadHelpers::DOWNLOADS_DIR end config.after :example, type: :feature do From 1ed1e6354905fe23e1d70ba0bfda0f7914b1fda5 Mon Sep 17 00:00:00 2001 From: Tadas Tamosauskas Date: Fri, 7 Jul 2023 18:48:11 +0100 Subject: [PATCH 3/5] Replace flaky JS assertions with reliable Capybara methods There's a few places in code where JS evals are used for assertions. While they _sometimes_ work, they _sometimes_ also flake, because of unfortunate timings. Update to use capybara's built-in matchers that are smarter and can wait a bit for requests and animations to before jumping to bad conclusions about the DOM state. There are a few more of these in the codebase. --- .rubocop.yml | 2 + spec/capybara_spec.rb | 150 ++++++++++++++++++++++-------------------- spec/spec_helper.rb | 8 +-- 3 files changed, 86 insertions(+), 74 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 89f099ee4..f3b8f5437 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -8,6 +8,7 @@ Metrics/AbcSize: Exclude: # TODO - 'lib/sequenceserver.rb' + - 'spec/**' Metrics/MethodLength: # Because 15 lines in a method is just about as good as 10 (default). And we # have a couple that can't be helped. @@ -30,6 +31,7 @@ Metrics/BlockLength: Exclude: - 'bin/sequenceserver' - 'sequenceserver.gemspec' + - 'spec/**' Style/UnlessElse: # TODO: diff --git a/spec/capybara_spec.rb b/spec/capybara_spec.rb index f178dafb9..271b68cdc 100644 --- a/spec/capybara_spec.rb +++ b/spec/capybara_spec.rb @@ -7,67 +7,70 @@ visit '/' fill_in('sequence', with: nucleotide_query) - prot = page.evaluate_script("$('.protein .database').text().trim()") - prot.should eq("2020-11 Swiss-Prot insecta 2020-11-Swiss-Prot insecta (subset taxid 102803) Sinvicta 2-2-3 prot subset without_parse_seqids.fa") - - nucl = page.evaluate_script("$('.nucleotide .database').text().trim()") - nucl.should eq("Sinvicta 2-2-3 cdna subset Solenopsis invicta gnG subset funky ids (v5)") + expect(page).to have_selector('.protein .database', text: /\A2020-11 Swiss-Prot insecta\z/) + expect(page).to have_selector('.protein .database', text: '2020-11-Swiss-Prot insecta (subset taxid 102803)') + expect(page).to have_selector('.protein .database', text: 'Sinvicta 2-2-3 prot subset') + expect(page).to have_selector('.protein .database', text: 'without_parse_seqids.fa') + + expect(page).to have_selector('.nucleotide .database', text: 'Sinvicta 2-2-3 cdna subset') + expect(page).to have_selector('.nucleotide .database', text: 'Solenopsis invicta gnG subset') + expect(page).to have_selector('.nucleotide .database', text: 'funky ids (v5)') end - it 'properly controls blast button' do + it 'keeps the CTA disabled until a sequence is provided and database selected' do visit '/' fill_in('sequence', with: nucleotide_query) - page.evaluate_script("$('#method').is(':disabled')").should eq(true) + expect(page).to have_selector('#method:disabled') check(nucleotide_databases.first) - page.evaluate_script("$('#method').is(':disabled')").should eq(false) + expect(page).to have_selector('#method:enabled') end - it 'properly controls interaction with database listing' do + it 'disables protein DB selection if a nucleotide DB is already selected' do visit '/' fill_in('sequence', with: nucleotide_query) check(nucleotide_databases.first) - page.evaluate_script("$('.protein .database').first().hasClass('disabled')") - .should eq(true) + + expect(page).to have_no_selector('.protein .database:enabled') end it 'shows a dropdown menu when other blast methods are available' do visit '/' fill_in('sequence', with: nucleotide_query) check(nucleotide_databases.first) - page.has_css?('#methods button.dropdown-toggle').should eq(true) + expect(page).to have_selector('#methods button.dropdown-toggle') end it 'can run a simple blastn search' do perform_search query: nucleotide_query, databases: nucleotide_databases - page.should have_content('BLASTN') + expect(page).to have_content('BLASTN') end it 'can run a simple blastp search' do perform_search query: protein_query, databases: protein_databases - page.should have_content('BLASTP') + expect(page).to have_content('BLASTP') end it 'can run a simple blastx search' do perform_search query: nucleotide_query, databases: protein_databases - page.should have_content('BLASTX') + expect(page).to have_content('BLASTX') end it 'can run a simple tblastx search' do perform_search query: nucleotide_query, databases: nucleotide_databases, method: 'tblastx' - page.should have_content('TBLASTX') + expect(page).to have_content('TBLASTX') end it 'can run a simple tblastn search' do perform_search query: protein_query, databases: nucleotide_databases - page.should have_content('TBLASTN') + expect(page).to have_content('TBLASTN') end ### Test aspects of the generated report. @@ -80,7 +83,7 @@ # Click on the first FASTA download button on the page and wait for the # download to finish. - page.execute_script("$('.download-fa:eq(0)').click()") + page.first('.download-fa').click wait_for_download # Test name and content of the downloaded file. @@ -141,7 +144,7 @@ # Click on the first Alignment download button on the page and wait for the # download to finish. - page.execute_script("$('.download-aln:eq(0)').click()") + page.first('.download-aln').click wait_for_download # Test name and content of the downloaded file. @@ -222,7 +225,7 @@ href = page.find('#sendEmail')['href'] expect(href).to include('mailto:?subject=SequenceServer%20BLASTP%20analysis') expect(href).to include(page.current_url) - expect(href).to include(protein_databases.values_at(0).join() && '%20') + expect(href).to include(protein_databases.values_at(0).join && '%20') end it 'can show hit sequences in a modal' do @@ -232,7 +235,7 @@ databases: protein_databases.values_at(0)) # Click on the first sequence viewer link in the report. - page.execute_script("$('.view-sequence:eq(0)').click()") + page.first('.view-sequence').click within('.sequence-viewer') do page.should have_content('SI2.2.0_06267') @@ -245,11 +248,12 @@ SEQ end - # Dismiss the first modal. - page.execute_script("$('.sequence-viewer').modal('hide')") + # Dismiss the modal. + page.find('.sequence-viewer').send_keys(:escape) + expect(page).to have_no_css('.sequence-viewer') # Click on the second sequence viewer link in the report. - page.execute_script("$('.view-sequence:eq(1)').click()") + page.find_all('.view-sequence')[1].click within('.sequence-viewer') do page.should have_content('SI2.2.0_13722') @@ -272,7 +276,7 @@ databases: nucleotide_databases.values_at(0)) # Check that the sequence viewer links are disabled. - page.evaluate_script("$('.view-sequence').is(':disabled')").should eq(true) + expect(page).to have_selector('.view-sequence:disabled') end it 'can download visualisations in svg and png format' do @@ -282,60 +286,66 @@ databases: protein_databases.values_at(0)) ## Check that there is a circos vis and unfold it. - page.should have_content('Queries and their top hits: chord diagram') - page.execute_script("$('.circos > .grapher-header > h4').click()") - sleep 1 + page.find('.circos > .grapher-header > h4', text: 'Queries and their top hits: chord diagram').click - page.execute_script("$('.export-to-svg:eq(0)').click()") - wait_for_download - expect(File.basename(downloaded_file)).to eq('Circos-visualisation.svg') - clear_downloads + within('.circos.grapher') do + page.click_on('SVG') + wait_for_download + expect(File.basename(downloaded_file)).to eq('Circos-visualisation.svg') + clear_downloads - page.execute_script("$('.export-to-png:eq(0)').click()") - wait_for_download - expect(File.basename(downloaded_file)).to eq('Circos-visualisation.png') + page.click_on('PNG') + wait_for_download + expect(File.basename(downloaded_file)).to eq('Circos-visualisation.png') + end clear_downloads ## Check that there is a graphical overview of hits. - page.should have_content('Graphical overview of hits') - - page.execute_script("$('.export-to-svg:eq(1)').click()") - wait_for_download - expect(File.basename(downloaded_file)).to eq('Alignment-Overview-Query_1.svg') - clear_downloads - - page.execute_script("$('.export-to-png:eq(1)').click()") - wait_for_download - expect(File.basename(downloaded_file)).to eq('Alignment-Overview-Query_1.png') - clear_downloads + expect(page).to have_content('Graphical overview of hits') + + within('#Query_1 .alignment-overview.grapher') do + page.click_on('SVG') + wait_for_download + expect(File.basename(downloaded_file)).to eq('Alignment-Overview-Query_1.svg') + clear_downloads + + page.click_on('PNG') + wait_for_download + expect(File.basename(downloaded_file)).to eq('Alignment-Overview-Query_1.png') + clear_downloads + end ## Check that there is a length distribution of matching sequences. - page.should have_content('Length distribution of matching sequences') - page.execute_script("$('.length-distribution > .grapher-header > h4').click()") - sleep 1 - - page.execute_script("$('.export-to-svg:eq(2)').click()") - wait_for_download - expect(File.basename(downloaded_file)).to eq('length-distribution-Query_1.svg') - clear_downloads - - page.execute_script("$('.export-to-png:eq(2)').click()") - wait_for_download - expect(File.basename(downloaded_file)).to eq('length-distribution-Query_1.png') - clear_downloads + expect(page).to have_content('Length distribution of matching sequences') + page.find('#Query_1 .length-distribution > .grapher-header > h4', + text: 'Length distribution of matching sequences').click + + within('#Query_1 .length-distribution.grapher') do + page.click_on('SVG') + wait_for_download + expect(File.basename(downloaded_file)).to eq('length-distribution-Query_1.svg') + clear_downloads + + page.click_on('PNG') + wait_for_download + expect(File.basename(downloaded_file)).to eq('length-distribution-Query_1.png') + clear_downloads + end ## Check that there is a kablammo vis of query vs hit. - page.should have_content('Graphical overview of aligning region(s)') - - page.execute_script("$('.export-to-svg:eq(3)').click()") - wait_for_download - expect(File.basename(downloaded_file)).to eq('Kablammo-Query_1-SI2_2_0_06267.svg') - clear_downloads - - page.execute_script("$('.export-to-png:eq(3)').click()") - wait_for_download - expect(File.basename(downloaded_file)).to eq('Kablammo-Query_1-SI2_2_0_06267.png') - clear_downloads + expect(page).to have_content('Graphical overview of aligning region(s)') + + within('#Query_1_hit_1 .kablammo.grapher') do + page.click_on('SVG') + wait_for_download + expect(File.basename(downloaded_file)).to eq('Kablammo-Query_1-SI2_2_0_06267.svg') + clear_downloads + + page.click_on('PNG') + wait_for_download + expect(File.basename(downloaded_file)).to eq('Kablammo-Query_1-SI2_2_0_06267.png') + clear_downloads + end end ## Helpers ## diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index ebea66d6e..e64ddf5a6 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -22,7 +22,7 @@ chrome_options = Selenium::WebDriver::Chrome::Options.new chrome_options.add_preference('download.default_directory', DownloadHelpers::DOWNLOADS_DIR) chrome_options.add_preference('profile.default_content_setting_values.automatic_downloads', 1) -chrome_options.add_argument("--window-size=1920,1200") +chrome_options.add_argument('--window-size=1920,1200') Capybara.register_driver :chrome do |app| Capybara::Selenium::Driver.new( @@ -49,7 +49,7 @@ RSpec.configure do |config| # Explicitly enable should syntax of rspec. config.expect_with :rspec do |expectations| - expectations.syntax = [:should, :expect] + expectations.syntax = %i[should expect] end # To use url_encode function in import_spec. @@ -59,7 +59,7 @@ config.include DownloadHelpers, type: :feature # Setup capybara tests. - config.before :context, type: :feature do |context| + config.before :context, type: :feature do |_context| FileUtils.mkdir_p DownloadHelpers::DOWNLOADS_DIR end @@ -68,7 +68,7 @@ end config.after :context, type: :feature do - FileUtils.rm_rf Dir[SequenceServer::DOTDIR + '/*-*-*-*-*'] + FileUtils.rm_rf Dir[File.join(SequenceServer::DOTDIR, '*-*-*-*-*')] end config.after :context do From 5ec0fba22fc6cbefd2659e5b9464782594cbca7d Mon Sep 17 00:00:00 2001 From: Tadas Tamosauskas Date: Mon, 10 Jul 2023 10:15:08 +0100 Subject: [PATCH 4/5] Explicitly add .txt extensions to alignment downloads Previously tests where run with Firefox which was probably adding txt extension itself and making the tests pass. Chrome does not seem to do that, add the .txt extension explicitly so that users can easily open them --- public/js/report.js | 6 +++--- public/sequenceserver-report.min.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/public/js/report.js b/public/js/report.js index 61249a2ea..38060cb71 100644 --- a/public/js/report.js +++ b/public/js/report.js @@ -487,7 +487,7 @@ class Report extends Component { // remove attributes from link if sequence_ids array is empty $('.download-alignment-of-selected').attr('href', '#').removeAttr('download'); return; - + } if(this.state.alignment_blob_url){ // always revoke existing url if any because this method will always create a new url @@ -503,7 +503,7 @@ class Report extends Component { } }); }, this)); - const filename = 'alignment-' + sequence_ids.length + '_hits'; + const filename = 'alignment-' + sequence_ids.length + '_hits.txt'; const blob_url = aln_exporter.prepare_alignments_for_export(hsps_arr, filename); // set required download attributes for link $('.download-alignment-of-selected').attr('href', blob_url).attr('download', filename); @@ -528,7 +528,7 @@ class Report extends Component { ); var aln_exporter = new AlignmentExporter(); - var file_name = `alignment-${num_hits}_hits`; + var file_name = `alignment-${num_hits}_hits.txt`; const blob_url = aln_exporter.prepare_alignments_for_export(hsps_arr, file_name); $('.download-alignment-of-all') .attr('href', blob_url) diff --git a/public/sequenceserver-report.min.js b/public/sequenceserver-report.min.js index 56fda6e43..6d35c51cb 100644 --- a/public/sequenceserver-report.min.js +++ b/public/sequenceserver-report.min.js @@ -159,7 +159,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpac /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; -eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony import */ var _jquery_world__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./jquery_world */ \"./public/js/jquery_world.js\");\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! react */ \"./node_modules/react/index.js\");\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_1__);\n/* harmony import */ var underscore__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! underscore */ \"./node_modules/underscore/modules/index-all.js\");\n/* harmony import */ var _sidebar__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./sidebar */ \"./public/js/sidebar.js\");\n/* harmony import */ var _circos__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./circos */ \"./public/js/circos.js\");\n/* harmony import */ var _query__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./query */ \"./public/js/query.js\");\n/* harmony import */ var _hit__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./hit */ \"./public/js/hit.js\");\n/* harmony import */ var _hsp__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./hsp */ \"./public/js/hsp.js\");\n/* harmony import */ var _alignment_exporter__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! ./alignment_exporter */ \"./public/js/alignment_exporter.js\");\n/* harmony import */ var react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! react/jsx-runtime */ \"./node_modules/react/jsx-runtime.js\");\n/* provided dependency */ var $ = __webpack_require__(/*! jquery */ \"./node_modules/jquery/dist/jquery.js\");\nfunction _typeof(obj) { \"@babel/helpers - typeof\"; return _typeof = \"function\" == typeof Symbol && \"symbol\" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && \"function\" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; }, _typeof(obj); }\n\nfunction ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }\n\nfunction _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }\n\nfunction _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }\n\nfunction _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if (\"value\" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }\n\nfunction _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, \"prototype\", { writable: false }); return Constructor; }\n\nfunction _inherits(subClass, superClass) { if (typeof superClass !== \"function\" && superClass !== null) { throw new TypeError(\"Super expression must either be null or a function\"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); Object.defineProperty(subClass, \"prototype\", { writable: false }); if (superClass) _setPrototypeOf(subClass, superClass); }\n\nfunction _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }\n\nfunction _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }\n\nfunction _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === \"object\" || typeof call === \"function\")) { return call; } else if (call !== void 0) { throw new TypeError(\"Derived constructors may only return object or undefined\"); } return _assertThisInitialized(self); }\n\nfunction _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\"); } return self; }\n\nfunction _isNativeReflectConstruct() { if (typeof Reflect === \"undefined\" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === \"function\") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } }\n\nfunction _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }\n\n // for custom $.tooltip function\n\n\n\n\n\n\n\n\n\n/**\n * Renders entire report.\n *\n * Composed of Query and Sidebar components.\n */\n\n\n\n\nvar Report = /*#__PURE__*/function (_Component) {\n _inherits(Report, _Component);\n\n var _super = _createSuper(Report);\n\n function Report(props) {\n var _this;\n\n _classCallCheck(this, Report);\n\n _this = _super.call(this, props); // Properties below are internal state used to render results in small\n // slices (see updateState).\n\n _this.numUpdates = 0;\n _this.nextQuery = 0;\n _this.nextHit = 0;\n _this.nextHSP = 0;\n _this.maxHSPs = 3; // max HSPs to render in a cycle\n\n _this.state = {\n search_id: '',\n seqserv_version: '',\n program: '',\n program_version: '',\n submitted_at: '',\n queries: [],\n results: [],\n querydb: [],\n params: [],\n stats: [],\n alignment_blob_url: '',\n allQueriesLoaded: false\n };\n _this.prepareAlignmentOfSelectedHits = _this.prepareAlignmentOfSelectedHits.bind(_assertThisInitialized(_this));\n _this.prepareAlignmentOfAllHits = _this.prepareAlignmentOfAllHits.bind(_assertThisInitialized(_this));\n _this.setStateFromJSON = _this.setStateFromJSON.bind(_assertThisInitialized(_this));\n return _this;\n }\n /**\n * Fetch results.\n */\n\n\n _createClass(Report, [{\n key: \"fetchResults\",\n value: function fetchResults() {\n var intervals = [200, 400, 800, 1200, 2000, 3000, 5000];\n var component = this;\n\n function poll() {\n $.getJSON(location.pathname + '.json').complete(function (jqXHR) {\n switch (jqXHR.status) {\n case 202:\n var interval;\n\n if (intervals.length === 1) {\n interval = intervals[0];\n } else {\n interval = intervals.shift();\n }\n\n setTimeout(poll, interval);\n break;\n\n case 200:\n component.setStateFromJSON(jqXHR.responseJSON);\n break;\n\n case 404:\n case 400:\n case 500:\n component.props.showErrorModal(jqXHR.responseJSON);\n break;\n }\n });\n }\n\n poll();\n }\n /**\n * Calls setState after any required modification to responseJSON.\n */\n\n }, {\n key: \"setStateFromJSON\",\n value: function setStateFromJSON(responseJSON) {\n this.lastTimeStamp = Date.now(); // the callback prepares the download link for all alignments\n\n this.setState(responseJSON, this.prepareAlignmentOfAllHits);\n }\n /**\n * Called as soon as the page has loaded and the user sees the loading spinner.\n * We use this opportunity to setup services that make use of delegated events\n * bound to the window, document, or body.\n */\n\n }, {\n key: \"componentDidMount\",\n value: function componentDidMount() {\n this.fetchResults(); // This sets up an event handler which enables users to select text from\n // hit header without collapsing the hit.\n\n this.preventCollapseOnSelection();\n this.toggleTable();\n }\n /**\n * Called for the first time after as BLAST results have been retrieved from\n * the server and added to this.state by fetchResults. Only summary overview\n * and circos would have been rendered at this point. At this stage we kick\n * start iteratively adding 1 HSP to the page every 25 milli-seconds.\n */\n\n }, {\n key: \"componentDidUpdate\",\n value: function componentDidUpdate() {\n var _this2 = this;\n\n // Log to console how long the last update take?\n console.log((Date.now() - this.lastTimeStamp) / 1000); // Lock sidebar in its position on the first update.\n\n if (this.nextQuery == 0 && this.nextHit == 0 && this.nextHSP == 0) {\n this.affixSidebar();\n } // Queue next update if we have not rendered all results yet.\n\n\n if (this.nextQuery < this.state.queries.length) {\n // setTimeout is used to clear call stack and space out\n // the updates giving the browser a chance to respond\n // to user interactions.\n setTimeout(function () {\n return _this2.updateState();\n }, 25);\n } else {\n this.componentFinishedUpdating();\n }\n }\n /**\n * Push next slice of results to React for rendering.\n */\n\n }, {\n key: \"updateState\",\n value: function updateState() {\n var results = [];\n var numHSPsProcessed = 0;\n\n while (this.nextQuery < this.state.queries.length) {\n var query = this.state.queries[this.nextQuery]; // We may see a query multiple times during rendering because only\n // 3 hsps or are rendered in each cycle, but we want to create the\n // corresponding Query component only the first time we see it.\n\n if (this.nextHit == 0 && this.nextHSP == 0) {\n results.push( /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(_query__WEBPACK_IMPORTED_MODULE_5__.ReportQuery, {\n query: query,\n program: this.state.program,\n querydb: this.state.querydb,\n showQueryCrumbs: this.state.queries.length > 1,\n non_parse_seqids: this.state.non_parse_seqids,\n imported_xml: this.state.imported_xml,\n veryBig: this.state.veryBig\n }, 'Query_' + query.number));\n }\n\n while (this.nextHit < query.hits.length) {\n var hit = query.hits[this.nextHit]; // We may see a hit multiple times during rendering because only\n // 10 hsps are rendered in each cycle, but we want to create the\n // corresponding Hit component only the first time we see it.\n\n if (this.nextHSP == 0) {\n results.push( /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(_hit__WEBPACK_IMPORTED_MODULE_6__[\"default\"], _objectSpread({\n query: query,\n hit: hit,\n algorithm: this.state.program,\n querydb: this.state.querydb,\n selectHit: this.selectHit,\n imported_xml: this.state.imported_xml,\n non_parse_seqids: this.state.non_parse_seqids,\n showQueryCrumbs: this.state.queries.length > 1,\n showHitCrumbs: query.hits.length > 1,\n veryBig: this.state.veryBig,\n onChange: this.prepareAlignmentOfSelectedHits\n }, this.props), 'Query_' + query.number + '_Hit_' + hit.number));\n }\n\n while (this.nextHSP < hit.hsps.length) {\n // Get nextHSP and increment the counter.\n var hsp = hit.hsps[this.nextHSP++];\n results.push( /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(_hsp__WEBPACK_IMPORTED_MODULE_7__[\"default\"], _objectSpread({\n query: query,\n hit: hit,\n hsp: hsp,\n algorithm: this.state.program,\n showHSPNumbers: hit.hsps.length > 1\n }, this.props), 'Query_' + query.number + '_Hit_' + hit.number + '_HSP_' + hsp.number));\n numHSPsProcessed++;\n if (numHSPsProcessed == this.maxHSPs) break;\n } // Are we here because we have iterated over all hsps of a hit,\n // or because of the break clause in the inner loop?\n\n\n if (this.nextHSP == hit.hsps.length) {\n this.nextHit = this.nextHit + 1;\n this.nextHSP = 0;\n }\n\n if (numHSPsProcessed == this.maxHSPs) break;\n } // Are we here because we have iterated over all hits of a query,\n // or because of the break clause in the inner loop?\n\n\n if (this.nextHit == query.hits.length) {\n this.nextQuery = this.nextQuery + 1;\n this.nextHit = 0;\n }\n\n if (numHSPsProcessed == this.maxHSPs) break;\n } // Push the components to react for rendering.\n\n\n this.numUpdates++;\n this.lastTimeStamp = Date.now();\n this.setState({\n results: this.state.results.concat(results),\n veryBig: this.numUpdates >= 250\n });\n }\n /**\n * Called after all results have been rendered.\n */\n\n }, {\n key: \"componentFinishedUpdating\",\n value: function componentFinishedUpdating() {\n if (this.state.allQueriesLoaded) return;\n this.shouldShowIndex() && this.setupScrollSpy();\n this.setState({\n allQueriesLoaded: true\n });\n }\n /**\n * Returns loading message\n */\n\n }, {\n key: \"loadingJSX\",\n value: function loadingJSX() {\n return /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(\"div\", {\n className: \"row\",\n children: /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsxs)(\"div\", {\n className: \"col-md-6 col-md-offset-3 text-center\",\n children: [/*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsxs)(\"h1\", {\n children: [/*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(\"i\", {\n className: \"fa fa-cog fa-spin\"\n }), \"\\xA0 BLAST-ing\"]\n }), /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsxs)(\"p\", {\n children: [/*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(\"br\", {}), \"This can take some time depending on the size of your query and database(s). The page will update automatically when BLAST is done.\", /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(\"br\", {}), /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(\"br\", {}), \"You can bookmark the page and come back to it later or share the link with someone.\"]\n })]\n })\n });\n }\n /**\n * Return results JSX.\n */\n\n }, {\n key: \"resultsJSX\",\n value: function resultsJSX() {\n return /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsxs)(\"div\", {\n className: \"row\",\n id: \"results\",\n children: [/*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(\"div\", {\n className: \"col-md-3 hidden-sm hidden-xs\",\n children: /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(_sidebar__WEBPACK_IMPORTED_MODULE_3__[\"default\"], {\n data: this.state,\n atLeastOneHit: this.atLeastOneHit(),\n shouldShowIndex: this.shouldShowIndex(),\n allQueriesLoaded: this.state.allQueriesLoaded\n })\n }), /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsxs)(\"div\", {\n className: \"col-md-9\",\n children: [this.overviewJSX(), this.circosJSX(), this.state.results]\n })]\n });\n }\n /**\n * Renders report overview.\n */\n\n }, {\n key: \"overviewJSX\",\n value: function overviewJSX() {\n return /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsxs)(\"div\", {\n className: \"overview\",\n children: [/*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsxs)(\"p\", {\n children: [/*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsxs)(\"strong\", {\n children: [\"SequenceServer \", this.state.seqserv_version]\n }), \" using\", ' ', /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(\"strong\", {\n children: this.state.program_version\n }), this.state.submitted_at && \", query submitted on \".concat(this.state.submitted_at)]\n }), /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsxs)(\"p\", {\n children: [/*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(\"strong\", {\n children: \" Databases: \"\n }), this.state.querydb.map(function (db) {\n return db.title;\n }).join(', '), ' ', \"(\", this.state.stats.nsequences, \" sequences,\\xA0\", this.state.stats.ncharacters, \" characters)\"]\n }), /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsxs)(\"p\", {\n children: [/*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(\"strong\", {\n children: \"Parameters: \"\n }), ' ', underscore__WEBPACK_IMPORTED_MODULE_2__[\"default\"].map(this.state.params, function (val, key) {\n return key + ' ' + val;\n }).join(', ')]\n }), /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsxs)(\"p\", {\n children: [\"Please cite:\", ' ', /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(\"a\", {\n href: \"https://doi.org/10.1093/molbev/msz185\",\n children: \"https://doi.org/10.1093/molbev/msz185\"\n })]\n })]\n });\n }\n /**\n * Return JSX for circos if we have at least one hit.\n */\n\n }, {\n key: \"circosJSX\",\n value: function circosJSX() {\n return this.atLeastTwoHits() ? /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(_circos__WEBPACK_IMPORTED_MODULE_4__[\"default\"], {\n queries: this.state.queries,\n program: this.state.program,\n collapsed: \"true\"\n }) : /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(\"span\", {});\n } // Controller //\n\n /**\n * Returns true if results have been fetched.\n *\n * A holding message is shown till results are fetched.\n */\n\n }, {\n key: \"isResultAvailable\",\n value: function isResultAvailable() {\n return this.state.queries.length >= 1;\n }\n /**\n * Returns true if we have at least one hit.\n */\n\n }, {\n key: \"atLeastOneHit\",\n value: function atLeastOneHit() {\n return this.state.queries.some(function (query) {\n return query.hits.length > 0;\n });\n }\n /**\n * Does the report have at least two hits? This is used to determine\n * whether Circos should be enabled or not.\n */\n\n }, {\n key: \"atLeastTwoHits\",\n value: function atLeastTwoHits() {\n var hit_num = 0;\n return this.state.queries.some(function (query) {\n hit_num += query.hits.length;\n return hit_num > 1;\n });\n }\n /**\n * Returns true if index should be shown in the sidebar. Index is shown\n * only for 2 and 8 queries.\n */\n\n }, {\n key: \"shouldShowIndex\",\n value: function shouldShowIndex() {\n var num_queries = this.state.queries.length;\n return num_queries >= 2 && num_queries <= 12;\n }\n /**\n * Prevents folding of hits during text-selection.\n */\n\n }, {\n key: \"preventCollapseOnSelection\",\n value: function preventCollapseOnSelection() {\n $('body').on('mousedown', '.hit > .section-header > h4', function (event) {\n var $this = $(this);\n $this.on('mouseup mousemove', function handler(event) {\n if (event.type === 'mouseup') {\n // user wants to toggle\n var hitID = $this.parents('.hit').attr('id');\n $(\"div[data-parent-hit=\".concat(hitID, \"]\")).toggle();\n $this.find('i').toggleClass('fa-minus-square-o fa-plus-square-o');\n } else {\n // user wants to select\n $this.attr('data-toggle', '');\n }\n\n $this.off('mouseup mousemove', handler);\n });\n });\n }\n /* Handling the fa icon when Hit Table is collapsed */\n\n }, {\n key: \"toggleTable\",\n value: function toggleTable() {\n $('body').on('mousedown', '.resultn > .section-content > .table-hit-overview > .caption', function (event) {\n var $this = $(this);\n $this.on('mouseup mousemove', function handler(event) {\n $this.find('i').toggleClass('fa-minus-square-o fa-plus-square-o');\n $this.off('mouseup mousemove', handler);\n });\n });\n }\n /**\n * Affixes the sidebar.\n */\n\n }, {\n key: \"affixSidebar\",\n value: function affixSidebar() {\n var $sidebar = $('.sidebar');\n var sidebarOffset = $sidebar.offset();\n\n if (sidebarOffset) {\n $sidebar.affix({\n offset: {\n top: sidebarOffset.top\n }\n });\n }\n }\n /**\n * For the query in viewport, highlights corresponding entry in the index.\n */\n\n }, {\n key: \"setupScrollSpy\",\n value: function setupScrollSpy() {\n $('body').scrollspy({\n target: '.sidebar'\n });\n }\n /**\n * Event-handler when hit is selected\n * Adds glow to hit component.\n * Updates number of Fasta that can be downloaded\n */\n\n }, {\n key: \"selectHit\",\n value: function selectHit(id) {\n var checkbox = $('#' + id);\n var num_checked = $('.hit-links :checkbox:checked').length;\n\n if (!checkbox || !checkbox.val()) {\n return;\n }\n\n var $hit = $(checkbox.data('target')); // Highlight selected hit and enable 'Download FASTA/Alignment of\n // selected' links.\n\n if (checkbox.is(':checked')) {\n $hit.addClass('glow');\n $hit.next('.hsp').addClass('glow');\n $('.download-fasta-of-selected').enable();\n $('.download-alignment-of-selected').enable();\n } else {\n $hit.removeClass('glow');\n $hit.next('.hsp').removeClass('glow');\n }\n\n var $a = $('.download-fasta-of-selected');\n var $b = $('.download-alignment-of-selected');\n\n if (num_checked >= 1) {\n $a.find('.text-bold').html(num_checked);\n $b.find('.text-bold').html(num_checked);\n }\n\n if (num_checked == 0) {\n $a.addClass('disabled').find('.text-bold').html('');\n $b.addClass('disabled').find('.text-bold').html('');\n }\n }\n }, {\n key: \"populate_hsp_array\",\n value: function populate_hsp_array(hit, query_id) {\n return hit.hsps.map(function (hsp) {\n return Object.assign(hsp, {\n hit_id: hit.id,\n query_id: query_id\n });\n });\n }\n }, {\n key: \"prepareAlignmentOfSelectedHits\",\n value: function prepareAlignmentOfSelectedHits() {\n var sequence_ids = $('.hit-links :checkbox:checked').map(function () {\n return this.value;\n }).get();\n\n if (!sequence_ids.length) {\n // remove attributes from link if sequence_ids array is empty\n $('.download-alignment-of-selected').attr('href', '#').removeAttr('download');\n return;\n }\n\n if (this.state.alignment_blob_url) {\n // always revoke existing url if any because this method will always create a new url\n window.URL.revokeObjectURL(this.state.alignment_blob_url);\n }\n\n var hsps_arr = [];\n var aln_exporter = new _alignment_exporter__WEBPACK_IMPORTED_MODULE_8__[\"default\"]();\n var self = this;\n\n underscore__WEBPACK_IMPORTED_MODULE_2__[\"default\"].each(this.state.queries, underscore__WEBPACK_IMPORTED_MODULE_2__[\"default\"].bind(function (query) {\n underscore__WEBPACK_IMPORTED_MODULE_2__[\"default\"].each(query.hits, function (hit) {\n if (underscore__WEBPACK_IMPORTED_MODULE_2__[\"default\"].indexOf(sequence_ids, hit.id) != -1) {\n hsps_arr = hsps_arr.concat(self.populate_hsp_array(hit, query.id));\n }\n });\n }, this));\n\n var filename = 'alignment-' + sequence_ids.length + '_hits';\n var blob_url = aln_exporter.prepare_alignments_for_export(hsps_arr, filename); // set required download attributes for link\n\n $('.download-alignment-of-selected').attr('href', blob_url).attr('download', filename); // track new url for future removal\n\n this.setState({\n alignment_blob_url: blob_url\n });\n }\n }, {\n key: \"prepareAlignmentOfAllHits\",\n value: function prepareAlignmentOfAllHits() {\n var _this3 = this;\n\n // Get number of hits and array of all hsps.\n var num_hits = 0;\n var hsps_arr = [];\n\n if (!this.state.queries.length) {\n return;\n }\n\n this.state.queries.forEach(function (query) {\n return query.hits.forEach(function (hit) {\n num_hits++;\n hsps_arr = hsps_arr.concat(_this3.populate_hsp_array(hit, query.id));\n });\n });\n var aln_exporter = new _alignment_exporter__WEBPACK_IMPORTED_MODULE_8__[\"default\"]();\n var file_name = \"alignment-\".concat(num_hits, \"_hits\");\n var blob_url = aln_exporter.prepare_alignments_for_export(hsps_arr, file_name);\n $('.download-alignment-of-all').attr('href', blob_url).attr('download', file_name);\n return false;\n }\n }, {\n key: \"render\",\n value: function render() {\n return this.isResultAvailable() ? this.resultsJSX() : this.loadingJSX();\n }\n }]);\n\n return Report;\n}(react__WEBPACK_IMPORTED_MODULE_1__.Component);\n\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (Report);\n\n//# sourceURL=webpack://SequenceServer/./public/js/report.js?"); +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony import */ var _jquery_world__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./jquery_world */ \"./public/js/jquery_world.js\");\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! react */ \"./node_modules/react/index.js\");\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_1__);\n/* harmony import */ var underscore__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! underscore */ \"./node_modules/underscore/modules/index-all.js\");\n/* harmony import */ var _sidebar__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./sidebar */ \"./public/js/sidebar.js\");\n/* harmony import */ var _circos__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./circos */ \"./public/js/circos.js\");\n/* harmony import */ var _query__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./query */ \"./public/js/query.js\");\n/* harmony import */ var _hit__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./hit */ \"./public/js/hit.js\");\n/* harmony import */ var _hsp__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./hsp */ \"./public/js/hsp.js\");\n/* harmony import */ var _alignment_exporter__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! ./alignment_exporter */ \"./public/js/alignment_exporter.js\");\n/* harmony import */ var react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! react/jsx-runtime */ \"./node_modules/react/jsx-runtime.js\");\n/* provided dependency */ var $ = __webpack_require__(/*! jquery */ \"./node_modules/jquery/dist/jquery.js\");\nfunction _typeof(obj) { \"@babel/helpers - typeof\"; return _typeof = \"function\" == typeof Symbol && \"symbol\" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && \"function\" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; }, _typeof(obj); }\n\nfunction ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }\n\nfunction _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }\n\nfunction _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }\n\nfunction _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if (\"value\" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }\n\nfunction _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, \"prototype\", { writable: false }); return Constructor; }\n\nfunction _inherits(subClass, superClass) { if (typeof superClass !== \"function\" && superClass !== null) { throw new TypeError(\"Super expression must either be null or a function\"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); Object.defineProperty(subClass, \"prototype\", { writable: false }); if (superClass) _setPrototypeOf(subClass, superClass); }\n\nfunction _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }\n\nfunction _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }\n\nfunction _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === \"object\" || typeof call === \"function\")) { return call; } else if (call !== void 0) { throw new TypeError(\"Derived constructors may only return object or undefined\"); } return _assertThisInitialized(self); }\n\nfunction _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\"); } return self; }\n\nfunction _isNativeReflectConstruct() { if (typeof Reflect === \"undefined\" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === \"function\") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } }\n\nfunction _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }\n\n // for custom $.tooltip function\n\n\n\n\n\n\n\n\n\n/**\n * Renders entire report.\n *\n * Composed of Query and Sidebar components.\n */\n\n\n\n\nvar Report = /*#__PURE__*/function (_Component) {\n _inherits(Report, _Component);\n\n var _super = _createSuper(Report);\n\n function Report(props) {\n var _this;\n\n _classCallCheck(this, Report);\n\n _this = _super.call(this, props); // Properties below are internal state used to render results in small\n // slices (see updateState).\n\n _this.numUpdates = 0;\n _this.nextQuery = 0;\n _this.nextHit = 0;\n _this.nextHSP = 0;\n _this.maxHSPs = 3; // max HSPs to render in a cycle\n\n _this.state = {\n search_id: '',\n seqserv_version: '',\n program: '',\n program_version: '',\n submitted_at: '',\n queries: [],\n results: [],\n querydb: [],\n params: [],\n stats: [],\n alignment_blob_url: '',\n allQueriesLoaded: false\n };\n _this.prepareAlignmentOfSelectedHits = _this.prepareAlignmentOfSelectedHits.bind(_assertThisInitialized(_this));\n _this.prepareAlignmentOfAllHits = _this.prepareAlignmentOfAllHits.bind(_assertThisInitialized(_this));\n _this.setStateFromJSON = _this.setStateFromJSON.bind(_assertThisInitialized(_this));\n return _this;\n }\n /**\n * Fetch results.\n */\n\n\n _createClass(Report, [{\n key: \"fetchResults\",\n value: function fetchResults() {\n var intervals = [200, 400, 800, 1200, 2000, 3000, 5000];\n var component = this;\n\n function poll() {\n $.getJSON(location.pathname + '.json').complete(function (jqXHR) {\n switch (jqXHR.status) {\n case 202:\n var interval;\n\n if (intervals.length === 1) {\n interval = intervals[0];\n } else {\n interval = intervals.shift();\n }\n\n setTimeout(poll, interval);\n break;\n\n case 200:\n component.setStateFromJSON(jqXHR.responseJSON);\n break;\n\n case 404:\n case 400:\n case 500:\n component.props.showErrorModal(jqXHR.responseJSON);\n break;\n }\n });\n }\n\n poll();\n }\n /**\n * Calls setState after any required modification to responseJSON.\n */\n\n }, {\n key: \"setStateFromJSON\",\n value: function setStateFromJSON(responseJSON) {\n this.lastTimeStamp = Date.now(); // the callback prepares the download link for all alignments\n\n this.setState(responseJSON, this.prepareAlignmentOfAllHits);\n }\n /**\n * Called as soon as the page has loaded and the user sees the loading spinner.\n * We use this opportunity to setup services that make use of delegated events\n * bound to the window, document, or body.\n */\n\n }, {\n key: \"componentDidMount\",\n value: function componentDidMount() {\n this.fetchResults(); // This sets up an event handler which enables users to select text from\n // hit header without collapsing the hit.\n\n this.preventCollapseOnSelection();\n this.toggleTable();\n }\n /**\n * Called for the first time after as BLAST results have been retrieved from\n * the server and added to this.state by fetchResults. Only summary overview\n * and circos would have been rendered at this point. At this stage we kick\n * start iteratively adding 1 HSP to the page every 25 milli-seconds.\n */\n\n }, {\n key: \"componentDidUpdate\",\n value: function componentDidUpdate() {\n var _this2 = this;\n\n // Log to console how long the last update take?\n console.log((Date.now() - this.lastTimeStamp) / 1000); // Lock sidebar in its position on the first update.\n\n if (this.nextQuery == 0 && this.nextHit == 0 && this.nextHSP == 0) {\n this.affixSidebar();\n } // Queue next update if we have not rendered all results yet.\n\n\n if (this.nextQuery < this.state.queries.length) {\n // setTimeout is used to clear call stack and space out\n // the updates giving the browser a chance to respond\n // to user interactions.\n setTimeout(function () {\n return _this2.updateState();\n }, 25);\n } else {\n this.componentFinishedUpdating();\n }\n }\n /**\n * Push next slice of results to React for rendering.\n */\n\n }, {\n key: \"updateState\",\n value: function updateState() {\n var results = [];\n var numHSPsProcessed = 0;\n\n while (this.nextQuery < this.state.queries.length) {\n var query = this.state.queries[this.nextQuery]; // We may see a query multiple times during rendering because only\n // 3 hsps or are rendered in each cycle, but we want to create the\n // corresponding Query component only the first time we see it.\n\n if (this.nextHit == 0 && this.nextHSP == 0) {\n results.push( /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(_query__WEBPACK_IMPORTED_MODULE_5__.ReportQuery, {\n query: query,\n program: this.state.program,\n querydb: this.state.querydb,\n showQueryCrumbs: this.state.queries.length > 1,\n non_parse_seqids: this.state.non_parse_seqids,\n imported_xml: this.state.imported_xml,\n veryBig: this.state.veryBig\n }, 'Query_' + query.number));\n }\n\n while (this.nextHit < query.hits.length) {\n var hit = query.hits[this.nextHit]; // We may see a hit multiple times during rendering because only\n // 10 hsps are rendered in each cycle, but we want to create the\n // corresponding Hit component only the first time we see it.\n\n if (this.nextHSP == 0) {\n results.push( /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(_hit__WEBPACK_IMPORTED_MODULE_6__[\"default\"], _objectSpread({\n query: query,\n hit: hit,\n algorithm: this.state.program,\n querydb: this.state.querydb,\n selectHit: this.selectHit,\n imported_xml: this.state.imported_xml,\n non_parse_seqids: this.state.non_parse_seqids,\n showQueryCrumbs: this.state.queries.length > 1,\n showHitCrumbs: query.hits.length > 1,\n veryBig: this.state.veryBig,\n onChange: this.prepareAlignmentOfSelectedHits\n }, this.props), 'Query_' + query.number + '_Hit_' + hit.number));\n }\n\n while (this.nextHSP < hit.hsps.length) {\n // Get nextHSP and increment the counter.\n var hsp = hit.hsps[this.nextHSP++];\n results.push( /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(_hsp__WEBPACK_IMPORTED_MODULE_7__[\"default\"], _objectSpread({\n query: query,\n hit: hit,\n hsp: hsp,\n algorithm: this.state.program,\n showHSPNumbers: hit.hsps.length > 1\n }, this.props), 'Query_' + query.number + '_Hit_' + hit.number + '_HSP_' + hsp.number));\n numHSPsProcessed++;\n if (numHSPsProcessed == this.maxHSPs) break;\n } // Are we here because we have iterated over all hsps of a hit,\n // or because of the break clause in the inner loop?\n\n\n if (this.nextHSP == hit.hsps.length) {\n this.nextHit = this.nextHit + 1;\n this.nextHSP = 0;\n }\n\n if (numHSPsProcessed == this.maxHSPs) break;\n } // Are we here because we have iterated over all hits of a query,\n // or because of the break clause in the inner loop?\n\n\n if (this.nextHit == query.hits.length) {\n this.nextQuery = this.nextQuery + 1;\n this.nextHit = 0;\n }\n\n if (numHSPsProcessed == this.maxHSPs) break;\n } // Push the components to react for rendering.\n\n\n this.numUpdates++;\n this.lastTimeStamp = Date.now();\n this.setState({\n results: this.state.results.concat(results),\n veryBig: this.numUpdates >= 250\n });\n }\n /**\n * Called after all results have been rendered.\n */\n\n }, {\n key: \"componentFinishedUpdating\",\n value: function componentFinishedUpdating() {\n if (this.state.allQueriesLoaded) return;\n this.shouldShowIndex() && this.setupScrollSpy();\n this.setState({\n allQueriesLoaded: true\n });\n }\n /**\n * Returns loading message\n */\n\n }, {\n key: \"loadingJSX\",\n value: function loadingJSX() {\n return /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(\"div\", {\n className: \"row\",\n children: /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsxs)(\"div\", {\n className: \"col-md-6 col-md-offset-3 text-center\",\n children: [/*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsxs)(\"h1\", {\n children: [/*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(\"i\", {\n className: \"fa fa-cog fa-spin\"\n }), \"\\xA0 BLAST-ing\"]\n }), /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsxs)(\"p\", {\n children: [/*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(\"br\", {}), \"This can take some time depending on the size of your query and database(s). The page will update automatically when BLAST is done.\", /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(\"br\", {}), /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(\"br\", {}), \"You can bookmark the page and come back to it later or share the link with someone.\"]\n })]\n })\n });\n }\n /**\n * Return results JSX.\n */\n\n }, {\n key: \"resultsJSX\",\n value: function resultsJSX() {\n return /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsxs)(\"div\", {\n className: \"row\",\n id: \"results\",\n children: [/*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(\"div\", {\n className: \"col-md-3 hidden-sm hidden-xs\",\n children: /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(_sidebar__WEBPACK_IMPORTED_MODULE_3__[\"default\"], {\n data: this.state,\n atLeastOneHit: this.atLeastOneHit(),\n shouldShowIndex: this.shouldShowIndex(),\n allQueriesLoaded: this.state.allQueriesLoaded\n })\n }), /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsxs)(\"div\", {\n className: \"col-md-9\",\n children: [this.overviewJSX(), this.circosJSX(), this.state.results]\n })]\n });\n }\n /**\n * Renders report overview.\n */\n\n }, {\n key: \"overviewJSX\",\n value: function overviewJSX() {\n return /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsxs)(\"div\", {\n className: \"overview\",\n children: [/*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsxs)(\"p\", {\n children: [/*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsxs)(\"strong\", {\n children: [\"SequenceServer \", this.state.seqserv_version]\n }), \" using\", ' ', /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(\"strong\", {\n children: this.state.program_version\n }), this.state.submitted_at && \", query submitted on \".concat(this.state.submitted_at)]\n }), /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsxs)(\"p\", {\n children: [/*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(\"strong\", {\n children: \" Databases: \"\n }), this.state.querydb.map(function (db) {\n return db.title;\n }).join(', '), ' ', \"(\", this.state.stats.nsequences, \" sequences,\\xA0\", this.state.stats.ncharacters, \" characters)\"]\n }), /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsxs)(\"p\", {\n children: [/*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(\"strong\", {\n children: \"Parameters: \"\n }), ' ', underscore__WEBPACK_IMPORTED_MODULE_2__[\"default\"].map(this.state.params, function (val, key) {\n return key + ' ' + val;\n }).join(', ')]\n }), /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsxs)(\"p\", {\n children: [\"Please cite:\", ' ', /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(\"a\", {\n href: \"https://doi.org/10.1093/molbev/msz185\",\n children: \"https://doi.org/10.1093/molbev/msz185\"\n })]\n })]\n });\n }\n /**\n * Return JSX for circos if we have at least one hit.\n */\n\n }, {\n key: \"circosJSX\",\n value: function circosJSX() {\n return this.atLeastTwoHits() ? /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(_circos__WEBPACK_IMPORTED_MODULE_4__[\"default\"], {\n queries: this.state.queries,\n program: this.state.program,\n collapsed: \"true\"\n }) : /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_9__.jsx)(\"span\", {});\n } // Controller //\n\n /**\n * Returns true if results have been fetched.\n *\n * A holding message is shown till results are fetched.\n */\n\n }, {\n key: \"isResultAvailable\",\n value: function isResultAvailable() {\n return this.state.queries.length >= 1;\n }\n /**\n * Returns true if we have at least one hit.\n */\n\n }, {\n key: \"atLeastOneHit\",\n value: function atLeastOneHit() {\n return this.state.queries.some(function (query) {\n return query.hits.length > 0;\n });\n }\n /**\n * Does the report have at least two hits? This is used to determine\n * whether Circos should be enabled or not.\n */\n\n }, {\n key: \"atLeastTwoHits\",\n value: function atLeastTwoHits() {\n var hit_num = 0;\n return this.state.queries.some(function (query) {\n hit_num += query.hits.length;\n return hit_num > 1;\n });\n }\n /**\n * Returns true if index should be shown in the sidebar. Index is shown\n * only for 2 and 8 queries.\n */\n\n }, {\n key: \"shouldShowIndex\",\n value: function shouldShowIndex() {\n var num_queries = this.state.queries.length;\n return num_queries >= 2 && num_queries <= 12;\n }\n /**\n * Prevents folding of hits during text-selection.\n */\n\n }, {\n key: \"preventCollapseOnSelection\",\n value: function preventCollapseOnSelection() {\n $('body').on('mousedown', '.hit > .section-header > h4', function (event) {\n var $this = $(this);\n $this.on('mouseup mousemove', function handler(event) {\n if (event.type === 'mouseup') {\n // user wants to toggle\n var hitID = $this.parents('.hit').attr('id');\n $(\"div[data-parent-hit=\".concat(hitID, \"]\")).toggle();\n $this.find('i').toggleClass('fa-minus-square-o fa-plus-square-o');\n } else {\n // user wants to select\n $this.attr('data-toggle', '');\n }\n\n $this.off('mouseup mousemove', handler);\n });\n });\n }\n /* Handling the fa icon when Hit Table is collapsed */\n\n }, {\n key: \"toggleTable\",\n value: function toggleTable() {\n $('body').on('mousedown', '.resultn > .section-content > .table-hit-overview > .caption', function (event) {\n var $this = $(this);\n $this.on('mouseup mousemove', function handler(event) {\n $this.find('i').toggleClass('fa-minus-square-o fa-plus-square-o');\n $this.off('mouseup mousemove', handler);\n });\n });\n }\n /**\n * Affixes the sidebar.\n */\n\n }, {\n key: \"affixSidebar\",\n value: function affixSidebar() {\n var $sidebar = $('.sidebar');\n var sidebarOffset = $sidebar.offset();\n\n if (sidebarOffset) {\n $sidebar.affix({\n offset: {\n top: sidebarOffset.top\n }\n });\n }\n }\n /**\n * For the query in viewport, highlights corresponding entry in the index.\n */\n\n }, {\n key: \"setupScrollSpy\",\n value: function setupScrollSpy() {\n $('body').scrollspy({\n target: '.sidebar'\n });\n }\n /**\n * Event-handler when hit is selected\n * Adds glow to hit component.\n * Updates number of Fasta that can be downloaded\n */\n\n }, {\n key: \"selectHit\",\n value: function selectHit(id) {\n var checkbox = $('#' + id);\n var num_checked = $('.hit-links :checkbox:checked').length;\n\n if (!checkbox || !checkbox.val()) {\n return;\n }\n\n var $hit = $(checkbox.data('target')); // Highlight selected hit and enable 'Download FASTA/Alignment of\n // selected' links.\n\n if (checkbox.is(':checked')) {\n $hit.addClass('glow');\n $hit.next('.hsp').addClass('glow');\n $('.download-fasta-of-selected').enable();\n $('.download-alignment-of-selected').enable();\n } else {\n $hit.removeClass('glow');\n $hit.next('.hsp').removeClass('glow');\n }\n\n var $a = $('.download-fasta-of-selected');\n var $b = $('.download-alignment-of-selected');\n\n if (num_checked >= 1) {\n $a.find('.text-bold').html(num_checked);\n $b.find('.text-bold').html(num_checked);\n }\n\n if (num_checked == 0) {\n $a.addClass('disabled').find('.text-bold').html('');\n $b.addClass('disabled').find('.text-bold').html('');\n }\n }\n }, {\n key: \"populate_hsp_array\",\n value: function populate_hsp_array(hit, query_id) {\n return hit.hsps.map(function (hsp) {\n return Object.assign(hsp, {\n hit_id: hit.id,\n query_id: query_id\n });\n });\n }\n }, {\n key: \"prepareAlignmentOfSelectedHits\",\n value: function prepareAlignmentOfSelectedHits() {\n var sequence_ids = $('.hit-links :checkbox:checked').map(function () {\n return this.value;\n }).get();\n\n if (!sequence_ids.length) {\n // remove attributes from link if sequence_ids array is empty\n $('.download-alignment-of-selected').attr('href', '#').removeAttr('download');\n return;\n }\n\n if (this.state.alignment_blob_url) {\n // always revoke existing url if any because this method will always create a new url\n window.URL.revokeObjectURL(this.state.alignment_blob_url);\n }\n\n var hsps_arr = [];\n var aln_exporter = new _alignment_exporter__WEBPACK_IMPORTED_MODULE_8__[\"default\"]();\n var self = this;\n\n underscore__WEBPACK_IMPORTED_MODULE_2__[\"default\"].each(this.state.queries, underscore__WEBPACK_IMPORTED_MODULE_2__[\"default\"].bind(function (query) {\n underscore__WEBPACK_IMPORTED_MODULE_2__[\"default\"].each(query.hits, function (hit) {\n if (underscore__WEBPACK_IMPORTED_MODULE_2__[\"default\"].indexOf(sequence_ids, hit.id) != -1) {\n hsps_arr = hsps_arr.concat(self.populate_hsp_array(hit, query.id));\n }\n });\n }, this));\n\n var filename = 'alignment-' + sequence_ids.length + '_hits.txt';\n var blob_url = aln_exporter.prepare_alignments_for_export(hsps_arr, filename); // set required download attributes for link\n\n $('.download-alignment-of-selected').attr('href', blob_url).attr('download', filename); // track new url for future removal\n\n this.setState({\n alignment_blob_url: blob_url\n });\n }\n }, {\n key: \"prepareAlignmentOfAllHits\",\n value: function prepareAlignmentOfAllHits() {\n var _this3 = this;\n\n // Get number of hits and array of all hsps.\n var num_hits = 0;\n var hsps_arr = [];\n\n if (!this.state.queries.length) {\n return;\n }\n\n this.state.queries.forEach(function (query) {\n return query.hits.forEach(function (hit) {\n num_hits++;\n hsps_arr = hsps_arr.concat(_this3.populate_hsp_array(hit, query.id));\n });\n });\n var aln_exporter = new _alignment_exporter__WEBPACK_IMPORTED_MODULE_8__[\"default\"]();\n var file_name = \"alignment-\".concat(num_hits, \"_hits.txt\");\n var blob_url = aln_exporter.prepare_alignments_for_export(hsps_arr, file_name);\n $('.download-alignment-of-all').attr('href', blob_url).attr('download', file_name);\n return false;\n }\n }, {\n key: \"render\",\n value: function render() {\n return this.isResultAvailable() ? this.resultsJSX() : this.loadingJSX();\n }\n }]);\n\n return Report;\n}(react__WEBPACK_IMPORTED_MODULE_1__.Component);\n\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (Report);\n\n//# sourceURL=webpack://SequenceServer/./public/js/report.js?"); /***/ }), From cbb8483e95258f45118e61dc7dbbaeafebefa258 Mon Sep 17 00:00:00 2001 From: Tadas Tamosauskas Date: Mon, 10 Jul 2023 10:26:56 +0100 Subject: [PATCH 5/5] Clean up spec downloads folder after the suite runs There sometimes are a couple of download files left after running the specs, depending on the test order, which is mildly annoying. --- spec/spec_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index e64ddf5a6..93cc8e755 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -17,7 +17,7 @@ Capybara.app = SequenceServer Capybara.server = :webrick -Capybara.default_max_wait_time = 15 +Capybara.default_max_wait_time = 10 chrome_options = Selenium::WebDriver::Chrome::Options.new chrome_options.add_preference('download.default_directory', DownloadHelpers::DOWNLOADS_DIR)