diff --git a/cypress/integration/origin-search.spec.js b/cypress/integration/origin-search.spec.js index 609f1960..4c8b99bc 100644 --- a/cypress/integration/origin-search.spec.js +++ b/cypress/integration/origin-search.spec.js @@ -1,152 +1,152 @@ /** * Copyright (C) 2019 The Software Heritage developers * See the AUTHORS file at the top-level directory of this distribution * License: GNU Affero General Public License version 3, or any later version * See top-level LICENSE file for more information */ const unarchivedRepo = { url: 'https://github.com/SoftwareHeritage/swh-web', revision: '7bf1b2f489f16253527807baead7957ca9e8adde', snapshot: 'd9829223095de4bb529790de8ba4e4813e38672d', rootDirectory: '7d887d96c0047a77e2e8c4ee9bb1528463677663', readmeSha1git: 'b203ec39300e5b7e97b6e20986183cbd0b797859' }; const archivedRepo = { url: 'https://github.com/memononen/libtess2', readme: { path: 'README.md' } }; const nonExistentText = 'NoMatchExists'; let url; function searchShouldRedirect(searchText, redirectUrl) { cy.get('#origins-url-patterns') .type(searchText); cy.get('.swh-search-icon') .click(); cy.location('pathname') .should('equal', redirectUrl); } function searchShouldShowNotFound(searchText, msg) { cy.get('#origins-url-patterns') .type(searchText); cy.get('.swh-search-icon') .click(); cy.get('#swh-no-result') .should('be.visible') .and('contain', msg); } describe('Test origin-search', function() { before(function() { url = this.Urls.browse_search(); cy.visit(this.Urls.browse_origin_content(archivedRepo.url, archivedRepo.readme.path)); cy.window().then(win => { const metadata = win.swh.webapp.getBrowsedSwhObjectMetadata(); archivedRepo.revision = metadata.revision; archivedRepo.snapshot = metadata.snapshot; archivedRepo.readme.directory = metadata.directory; archivedRepo.readme.sha1git = metadata.sha1_git; }); }); beforeEach(function() { cy.visit(url); }); it('should show in result for any url substring', function() { // Randomly select substring of origin url const startIndex = Math.floor(Math.random() * archivedRepo.url.length); const length = Math.floor(Math.random() * (archivedRepo.url.length - startIndex - 1)) + 1; const originSubstring = archivedRepo.url.substr(startIndex, length); cy.get('#origins-url-patterns') .type(originSubstring); cy.get('.swh-search-icon') .click(); cy.get('#origin-search-results') .should('be.visible'); cy.contains('tr', archivedRepo.url) .should('be.visible') - .children('#visit-status-origin-0') - .children('i') + .find('.swh-visit-status') + .find('i') .should('have.class', 'fa-check') .and('have.attr', 'title', 'Origin has at least one full visit by Software Heritage'); }); it('should show not found message when no repo matches', function() { searchShouldShowNotFound(nonExistentText, 'No origins matching the search criteria were found.'); }); context('Test valid persistent ids', function() { it('should resolve directory', function() { const redirectUrl = this.Urls.browse_directory(archivedRepo.readme.directory); const persistentId = `swh:1:dir:${archivedRepo.readme.directory}`; searchShouldRedirect(persistentId, redirectUrl); }); it('should resolve revision', function() { const redirectUrl = this.Urls.browse_revision(archivedRepo.revision); const persistentId = `swh:1:rev:${archivedRepo.revision}`; searchShouldRedirect(persistentId, redirectUrl); }); it('should resolve snapshot', function() { const redirectUrl = this.Urls.browse_snapshot_directory(archivedRepo.snapshot); const persistentId = `swh:1:snp:${archivedRepo.snapshot}`; searchShouldRedirect(persistentId, redirectUrl); }); it('should resolve content', function() { const redirectUrl = this.Urls.browse_content(`sha1_git:${archivedRepo.readme.sha1git}`); const persistentId = `swh:1:cnt:${archivedRepo.readme.sha1git}`; searchShouldRedirect(persistentId, redirectUrl); }); }); context('Test invalid persistent ids', function() { it('should show not found for directory', function() { const persistentId = `swh:1:dir:${unarchivedRepo.rootDirectory}`; const msg = `Directory with sha1_git ${unarchivedRepo.rootDirectory} not found`; searchShouldShowNotFound(persistentId, msg); }); it('should show not found for snapshot', function() { const persistentId = `swh:1:snp:${unarchivedRepo.snapshot}`; const msg = `Snapshot with id ${unarchivedRepo.snapshot} not found!`; searchShouldShowNotFound(persistentId, msg); }); it('should show not found for revision', function() { const persistentId = `swh:1:rev:${unarchivedRepo.revision}`; const msg = `Revision with sha1_git ${unarchivedRepo.revision} not found.`; searchShouldShowNotFound(persistentId, msg); }); it('should show not found for content', function() { const persistentId = `swh:1:cnt:${unarchivedRepo.readmeSha1git}`; const msg = `Content with sha1_git checksum equals to ${unarchivedRepo.readmeSha1git} not found!`; searchShouldShowNotFound(persistentId, msg); }); }); }); diff --git a/swh/web/assets/src/bundles/browse/origin-search.js b/swh/web/assets/src/bundles/browse/origin-search.js index 9dd9b1fb..78a9bafa 100644 --- a/swh/web/assets/src/bundles/browse/origin-search.js +++ b/swh/web/assets/src/bundles/browse/origin-search.js @@ -1,238 +1,238 @@ /** * Copyright (C) 2018-2019 The Software Heritage developers * See the AUTHORS file at the top-level directory of this distribution * License: GNU Affero General Public License version 3, or any later version * See top-level LICENSE file for more information */ import {heapsPermute} from 'utils/heaps-permute'; import {handleFetchError} from 'utils/functions'; let originPatterns; let perPage = 100; let limit = perPage * 2; let offset = 0; let currentData = null; let inSearch = false; function fixTableRowsStyle() { setTimeout(() => { $('#origin-search-results tbody tr').removeAttr('style'); }); } function clearOriginSearchResultsTable() { $('#origin-search-results tbody tr').remove(); } function populateOriginSearchResultsTable(origins, offset) { let localOffset = offset % limit; if (origins.length > 0) { $('#swh-origin-search-results').show(); $('#swh-no-result').hide(); clearOriginSearchResultsTable(); let table = $('#origin-search-results tbody'); for (let i = localOffset; i < localOffset + perPage && i < origins.length; ++i) { let origin = origins[i]; let browseUrl = Urls.browse_origin(origin.url); let tableRow = ``; tableRow += `${origin.type}`; tableRow += `${encodeURI(origin.url)}`; - tableRow += ``; + tableRow += ``; tableRow += ''; table.append(tableRow); // get async latest visit snapshot and update visit status icon let latestSnapshotUrl = Urls.api_1_origin_visit_latest(origin.url); latestSnapshotUrl += "?require_snapshot=true"; fetch(latestSnapshotUrl) .then(response => response.json()) .then(data => { $(`#visit-status-origin-${i}`).children().remove(); if (data) { $(`#visit-status-origin-${i}`).append(''); } else { $(`#visit-status-origin-${i}`).append(''); if ($('#swh-filter-empty-visits').prop('checked')) { $(`#origin-${i}`).remove(); } } }); } fixTableRowsStyle(); } else { $('#swh-origin-search-results').hide(); $('#swh-no-result').text('No origins matching the search criteria were found.'); $('#swh-no-result').show(); } if (origins.length - localOffset < perPage || (origins.length < limit && (localOffset + perPage) === origins.length)) { $('#origins-next-results-button').addClass('disabled'); } else { $('#origins-next-results-button').removeClass('disabled'); } if (offset > 0) { $('#origins-prev-results-button').removeClass('disabled'); } else { $('#origins-prev-results-button').addClass('disabled'); } inSearch = false; setTimeout(() => { window.scrollTo(0, 0); }); } function escapeStringRegexp(str) { let matchOperatorsRe = /[|\\{}()[\]^$+*?.]/g; return str.replace(matchOperatorsRe, '\\\\\\$&'); } function searchOrigins(patterns, limit, searchOffset, offset) { let baseSearchUrl; let searchMetadata = $('#swh-search-origin-metadata').prop('checked'); if (searchMetadata) { baseSearchUrl = Urls.api_1_origin_metadata_search() + `?fulltext=${patterns}`; } else { originPatterns = patterns; let patternsArray = patterns.trim().replace(/\s+/g, ' ').split(' '); for (let i = 0; i < patternsArray.length; ++i) { patternsArray[i] = escapeStringRegexp(patternsArray[i]); } // url length must be less than 4096 for modern browsers // assuming average word length, 6 is max patternArray.length if (patternsArray.length < 7) { let patternsPermut = []; heapsPermute(patternsArray, p => patternsPermut.push(p.join('.*'))); let regex = patternsPermut.join('|'); baseSearchUrl = Urls.browse_origin_search(regex) + `?regexp=true`; } else { baseSearchUrl = Urls.browse_origin_search(patternsArray.join('.*')) + `?regexp=true`; } } let withVisit = $('#swh-search-origins-with-visit').prop('checked'); let searchUrl = baseSearchUrl + `&limit=${limit}&offset=${searchOffset}&with_visit=${withVisit}`; clearOriginSearchResultsTable(); $('.swh-loading').addClass('show'); fetch(searchUrl) .then(handleFetchError) .then(response => response.json()) .then(data => { currentData = data; $('.swh-loading').removeClass('show'); populateOriginSearchResultsTable(data, offset); }) .catch(response => { $('.swh-loading').removeClass('show'); inSearch = false; $('#swh-origin-search-results').hide(); $('#swh-no-result').text(`Error ${response.status}: ${response.statusText}`); $('#swh-no-result').show(); }); } function doSearch() { $('#swh-no-result').hide(); let patterns = $('#origins-url-patterns').val(); offset = 0; inSearch = true; // first try to resolve a swh persistent identifier let resolvePidUrl = Urls.api_1_resolve_swh_pid(patterns); fetch(resolvePidUrl) .then(handleFetchError) .then(response => response.json()) .then(data => { // pid has been successfully resolved, // so redirect to browse page window.location = data.browse_url; }) .catch(response => { // pid resolving failed if (patterns.startsWith('swh:')) { // display a useful error message if the input // looks like a swh pid response.json().then(data => { $('#swh-origin-search-results').hide(); $('.swh-search-pagination').hide(); $('#swh-no-result').text(data.reason); $('#swh-no-result').show(); }); } else { // otherwise, proceed with origins search $('#swh-origin-search-results').show(); $('.swh-search-pagination').show(); searchOrigins(patterns, limit, offset, offset); } }); } export function initOriginSearch() { $(document).ready(() => { $('#swh-search-origins').submit(event => { event.preventDefault(); let patterns = $('#origins-url-patterns').val().trim(); let withVisit = $('#swh-search-origins-with-visit').prop('checked'); let withContent = $('#swh-filter-empty-visits').prop('checked'); let searchMetadata = $('#swh-search-origin-metadata').prop('checked'); let queryParameters = '?q=' + encodeURIComponent(patterns); if (withVisit) { queryParameters += '&with_visit'; } if (withContent) { queryParameters += '&with_content'; } if (searchMetadata) { queryParameters += '&search_metadata'; } // Update the url, triggering page reload and effective search window.location.search = queryParameters; }); $('#origins-next-results-button').click(event => { if ($('#origins-next-results-button').hasClass('disabled') || inSearch) { return; } inSearch = true; offset += perPage; if (!currentData || (offset >= limit && offset % limit === 0)) { searchOrigins(originPatterns, limit, offset, offset); } else { populateOriginSearchResultsTable(currentData, offset); } event.preventDefault(); }); $('#origins-prev-results-button').click(event => { if ($('#origins-prev-results-button').hasClass('disabled') || inSearch) { return; } inSearch = true; offset -= perPage; if (!currentData || (offset > 0 && (offset + perPage) % limit === 0)) { searchOrigins(originPatterns, limit, (offset + perPage) - limit, offset); } else { populateOriginSearchResultsTable(currentData, offset); } event.preventDefault(); }); $(document).on('shown.bs.tab', 'a[data-toggle="tab"]', e => { if (e.currentTarget.text.trim() === 'Search') { fixTableRowsStyle(); } }); let urlParams = new URLSearchParams(window.location.search); let query = urlParams.get('q'); let withVisit = urlParams.has('with_visit'); let withContent = urlParams.has('with_content'); let searchMetadata = urlParams.has('search_metadata'); if (query) { $('#origins-url-patterns').val(query); $('#swh-search-origins-with-visit').prop('checked', withVisit); $('#swh-filter-empty-visits').prop('checked', withContent); $('#swh-search-origin-metadata').prop('checked', searchMetadata); doSearch(); } }); }