diff --git a/assets/src/bundles/origin/visits-calendar.js b/assets/src/bundles/origin/visits-calendar.js index b7345451..4686bac9 100644 --- a/assets/src/bundles/origin/visits-calendar.js +++ b/assets/src/bundles/origin/visits-calendar.js @@ -1,147 +1,146 @@ /** * Copyright (C) 2018-2021 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 Calendar from 'js-year-calendar'; import 'js-year-calendar/dist/js-year-calendar.css'; import hexRgb from 'hex-rgb'; import {visitStatusColor} from './utils'; const minSize = 15; const maxSize = 28; let currentPopover = null; let visitsByDate = {}; function closePopover() { if (currentPopover) { $(currentPopover).popover('dispose'); currentPopover = null; } } // function to update the visits calendar view based on the selected year export function updateCalendar(year, filteredVisits, yearClickedCallback) { visitsByDate = {}; let maxNbVisitsByDate = 0; let minDate, maxDate; for (let i = 0; i < filteredVisits.length; ++i) { filteredVisits[i]['startDate'] = filteredVisits[i]['date']; filteredVisits[i]['endDate'] = filteredVisits[i]['startDate']; const date = new Date(filteredVisits[i]['date']); date.setHours(0, 0, 0, 0); const dateStr = date.toDateString(); if (!visitsByDate.hasOwnProperty(dateStr)) { visitsByDate[dateStr] = [filteredVisits[i]]; } else { visitsByDate[dateStr].push(filteredVisits[i]); } maxNbVisitsByDate = Math.max(maxNbVisitsByDate, visitsByDate[dateStr].length); if (i === 0) { minDate = maxDate = date; } else { if (date.getTime() < minDate.getTime()) { minDate = date; } if (date.getTime() > maxDate.getTime()) { maxDate = date; } } } closePopover(); new Calendar('#swh-visits-calendar', { dataSource: filteredVisits, style: 'custom', minDate: minDate, maxDate: maxDate, startYear: year, renderEnd: e => yearClickedCallback(e.currentYear), customDataSourceRenderer: (element, date, events) => { const dateStr = date.toDateString(); const nbVisits = visitsByDate[dateStr].length; let t = nbVisits / maxNbVisitsByDate; if (maxNbVisitsByDate === 1) { t = 0; } const size = minSize + t * (maxSize - minSize); const offsetX = (maxSize - size) / 2 - parseInt($(element).css('padding-left')); const offsetY = (maxSize - size) / 2 - parseInt($(element).css('padding-top')) + 1; const cellWrapper = $('
'); cellWrapper.css('position', 'relative'); const dayNumber = $('
'); dayNumber.text($(element).text()); const circle = $('
'); const color = {red: 0, green: 0, blue: 0, alpha: 0.4}; for (let i = 0; i < nbVisits; ++i) { const visit = visitsByDate[dateStr][i]; const visitColor = hexRgb(visitStatusColor[visit.status]); color.red += visitColor.red; color.green += visitColor.green; color.blue += visitColor.blue; } color.red /= nbVisits; color.green /= nbVisits; color.blue /= nbVisits; - console.log(color); circle.css('background-color', `rgba(${color.red}, ${color.green}, ${color.blue}, ${color.alpha})`); circle.css('width', size + 'px'); circle.css('height', size + 'px'); circle.css('border-radius', size + 'px'); circle.css('position', 'absolute'); circle.css('top', offsetY + 'px'); circle.css('left', offsetX + 'px'); cellWrapper.append(dayNumber); cellWrapper.append(circle); $(element)[0].innerHTML = $(cellWrapper)[0].outerHTML; }, mouseOnDay: e => { if (currentPopover !== e.element) { closePopover(); } const dateStr = e.date.toDateString(); if (visitsByDate.hasOwnProperty(dateStr)) { const visits = visitsByDate[dateStr]; let content = '
' + e.date.toDateString() + '
'; content += ''; $(e.element).popover({ trigger: 'manual', container: 'body', html: true, content: content }).on('mouseleave', () => { if (!$('.popover:hover').length) { // close popover when leaving day in calendar // except if the pointer is hovering it closePopover(); } }); $(e.element).on('shown.bs.popover', () => { $('.popover').mouseleave(() => { // close popover when pointer leaves it closePopover(); }); }); $(e.element).popover('show'); currentPopover = e.element; } } }); $('#swh-visits-calendar.calendar table td').css('width', maxSize + 'px'); $('#swh-visits-calendar.calendar table td').css('height', maxSize + 'px'); $('#swh-visits-calendar.calendar table td').css('padding', '0px'); } diff --git a/cypress/integration/origin-search.spec.js b/cypress/integration/origin-search.spec.js index 60a249e3..90e9190b 100644 --- a/cypress/integration/origin-search.spec.js +++ b/cypress/integration/origin-search.spec.js @@ -1,580 +1,579 @@ /** * Copyright (C) 2019-2021 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 nonExistentText = 'NoMatchExists'; let origin; let url; function doSearch(searchText, searchInputElt = '#swh-origins-url-patterns') { if (searchText.startsWith('swh:')) { cy.intercept('**/api/1/resolve/**') .as('swhidResolve'); } cy.get(searchInputElt) // to avoid sending too much SWHID validation requests // as cypress insert character one by one when using type .invoke('val', searchText.slice(0, -1)) .type(searchText.slice(-1)) .get('.swh-search-icon') .click({force: true}); if (searchText.startsWith('swh:')) { cy.wait('@swhidResolve'); } } function searchShouldRedirect(searchText, redirectUrl) { doSearch(searchText); cy.location('pathname') .should('equal', redirectUrl); } function searchShouldShowNotFound(searchText, msg) { doSearch(searchText); if (searchText.startsWith('swh:')) { cy.get('.invalid-feedback') .should('be.visible') .and('contain', msg); } } function stubOriginVisitLatestRequests(status = 200, response = {type: 'tar'}, aliasSuffix = '') { cy.intercept({url: '**/visit/latest/**'}, { body: response, statusCode: status }).as(`originVisitLatest${aliasSuffix}`); } describe('Test origin-search', function() { before(function() { origin = this.origin[0]; url = this.Urls.browse_search(); }); beforeEach(function() { cy.visit(url); }); it('should have focus on search form after page load', function() { cy.get('#swh-origins-url-patterns') .should('have.attr', 'autofocus'); // for some reason, autofocus is not honored when running cypress tests // while it is in non controlled browsers // .should('have.focus'); }); it('should redirect to browse when archived URL is searched', function() { cy.get('#swh-origins-url-patterns') .type(origin.url); cy.get('.swh-search-icon') .click(); cy.location('pathname') .should('eq', this.Urls.browse_origin_directory()); cy.location('search') .should('eq', `?origin_url=${origin.url}`); }); it('should not redirect for non valid URL', function() { cy.get('#swh-origins-url-patterns') .type('www.example'); // Invalid URL cy.get('.swh-search-icon') .click(); cy.location('pathname') .should('eq', this.Urls.browse_search()); // Stay in the current page }); it('should not redirect for valid non archived URL', function() { cy.get('#swh-origins-url-patterns') .type('http://eaxmple.com/test/'); // Valid URL, but not archived cy.get('.swh-search-icon') .click(); cy.location('pathname') .should('eq', this.Urls.browse_search()); // Stay in the current page }); it('should remove origin URL with no archived content', function() { stubOriginVisitLatestRequests(404); // Using a non full origin URL here // This is because T3354 redirects to the origin in case of a valid, archived URL cy.get('#swh-origins-url-patterns') .type(origin.url.slice(0, -1)); cy.get('.swh-search-icon') .click(); cy.wait('@originVisitLatest'); cy.get('#origin-search-results') .should('be.visible') .find('tbody tr').should('have.length', 0); stubOriginVisitLatestRequests(200, {}, '2'); cy.get('.swh-search-icon') .click(); cy.wait('@originVisitLatest2'); cy.get('#origin-search-results') .should('be.visible') .find('tbody tr').should('have.length', 0); }); it('should filter origins by visit type', function() { cy.intercept('**/visit/latest/**').as('checkOriginVisits'); cy.get('#swh-origins-url-patterns') .type('http'); for (const visitType of ['git', 'tar']) { cy.get('#swh-search-visit-type') .select(visitType); cy.get('.swh-search-icon') .click(); cy.wait('@checkOriginVisits'); cy.get('#origin-search-results') .should('be.visible'); cy.get('tbody tr td.swh-origin-visit-type').then(elts => { for (const elt of elts) { cy.get(elt).should('have.text', visitType); } }); } }); it('should show not found message when no repo matches', function() { searchShouldShowNotFound(nonExistentText, 'No origins matching the search criteria were found.'); }); it('should add appropriate URL parameters', function() { // Check all three checkboxes and check if // correct url params are added cy.get('#swh-search-origins-with-visit') .check({force: true}) .get('#swh-filter-empty-visits') .check({force: true}) .get('#swh-search-origin-metadata') .check({force: true}) .then(() => { const searchText = origin.url; doSearch(searchText); cy.location('search').then(locationSearch => { const urlParams = new URLSearchParams(locationSearch); const query = urlParams.get('q'); const withVisit = urlParams.has('with_visit'); const withContent = urlParams.has('with_content'); const searchMetadata = urlParams.has('search_metadata'); assert.strictEqual(query, searchText); assert.strictEqual(withVisit, true); assert.strictEqual(withContent, true); assert.strictEqual(searchMetadata, true); }); }); }); it('should search in origin intrinsic metadata', function() { cy.intercept('GET', '**/origin/metadata-search/**').as( 'originMetadataSearch' ); cy.get('#swh-search-origins-with-visit') .check({force: true}) .get('#swh-filter-empty-visits') .check({force: true}) .get('#swh-search-origin-metadata') .check({force: true}) .then(() => { const searchText = 'plugin'; doSearch(searchText); - console.log(searchText); cy.wait('@originMetadataSearch').then((req) => { expect(req.response.body[0].metadata.metadata.description).to.equal( 'Line numbering plugin for Highlight.js' // metadata is defined in _TEST_ORIGINS variable in swh/web/tests/data.py ); }); }); }); it('should not send request to the resolve endpoint', function() { cy.intercept(`${this.Urls.api_1_resolve_swhid('').slice(0, -1)}**`) .as('resolveSWHID'); cy.intercept(`${this.Urls.api_1_origin_search(origin.url.slice(0, -1))}**`) .as('searchOrigin'); cy.get('#swh-origins-url-patterns') .type(origin.url.slice(0, -1)); cy.get('.swh-search-icon') .click(); cy.wait('@searchOrigin'); cy.xhrShouldBeCalled('resolveSWHID', 0); cy.xhrShouldBeCalled('searchOrigin', 1); }); it('should add query language support for staff users', function() { cy.get('#swh-search-use-ql') .should('not.exist'); cy.adminLogin(); cy.visit(url); cy.get('#swh-search-use-ql') .should('exist'); }); context('Test pagination', function() { it('should not paginate if there are not many results', function() { // Setup search cy.get('#swh-search-origins-with-visit') .uncheck({force: true}) .get('#swh-filter-empty-visits') .uncheck({force: true}) .then(() => { const searchText = 'libtess'; // Get first page of results doSearch(searchText); cy.get('.swh-search-result-entry') .should('have.length', 1); cy.get('.swh-search-result-entry#origin-0 td a') .should('have.text', 'https://github.com/memononen/libtess2'); cy.get('#origins-prev-results-button') .should('have.class', 'disabled'); cy.get('#origins-next-results-button') .should('have.class', 'disabled'); }); }); it('should paginate forward when there are many results', function() { stubOriginVisitLatestRequests(); // Setup search cy.get('#swh-search-origins-with-visit') .uncheck({force: true}) .get('#swh-filter-empty-visits') .uncheck({force: true}) .then(() => { const searchText = 'many.origins'; // Get first page of results doSearch(searchText); cy.wait('@originVisitLatest'); cy.get('.swh-search-result-entry') .should('have.length', 100); cy.get('.swh-search-result-entry#origin-0 td a') .should('have.text', 'https://many.origins/1'); cy.get('.swh-search-result-entry#origin-99 td a') .should('have.text', 'https://many.origins/100'); cy.get('#origins-prev-results-button') .should('have.class', 'disabled'); cy.get('#origins-next-results-button') .should('not.have.class', 'disabled'); // Get second page of results cy.get('#origins-next-results-button a') .click(); cy.wait('@originVisitLatest'); cy.get('.swh-search-result-entry') .should('have.length', 100); cy.get('.swh-search-result-entry#origin-0 td a') .should('have.text', 'https://many.origins/101'); cy.get('.swh-search-result-entry#origin-99 td a') .should('have.text', 'https://many.origins/200'); cy.get('#origins-prev-results-button') .should('not.have.class', 'disabled'); cy.get('#origins-next-results-button') .should('not.have.class', 'disabled'); // Get third (and last) page of results cy.get('#origins-next-results-button a') .click(); cy.wait('@originVisitLatest'); cy.get('.swh-search-result-entry') .should('have.length', 50); cy.get('.swh-search-result-entry#origin-0 td a') .should('have.text', 'https://many.origins/201'); cy.get('.swh-search-result-entry#origin-49 td a') .should('have.text', 'https://many.origins/250'); cy.get('#origins-prev-results-button') .should('not.have.class', 'disabled'); cy.get('#origins-next-results-button') .should('have.class', 'disabled'); }); }); it('should paginate backward from a middle page', function() { stubOriginVisitLatestRequests(); // Setup search cy.get('#swh-search-origins-with-visit') .uncheck({force: true}) .get('#swh-filter-empty-visits') .uncheck({force: true}) .then(() => { const searchText = 'many.origins'; // Get first page of results doSearch(searchText); cy.wait('@originVisitLatest'); cy.get('#origins-prev-results-button') .should('have.class', 'disabled'); cy.get('#origins-next-results-button') .should('not.have.class', 'disabled'); // Get second page of results cy.get('#origins-next-results-button a') .click(); cy.wait('@originVisitLatest'); cy.get('#origins-prev-results-button') .should('not.have.class', 'disabled'); cy.get('#origins-next-results-button') .should('not.have.class', 'disabled'); // Get first page of results again cy.get('#origins-prev-results-button a') .click(); cy.wait('@originVisitLatest'); cy.get('.swh-search-result-entry') .should('have.length', 100); cy.get('.swh-search-result-entry#origin-0 td a') .should('have.text', 'https://many.origins/1'); cy.get('.swh-search-result-entry#origin-99 td a') .should('have.text', 'https://many.origins/100'); cy.get('#origins-prev-results-button') .should('have.class', 'disabled'); cy.get('#origins-next-results-button') .should('not.have.class', 'disabled'); }); }); it('should paginate backward from the last page', function() { stubOriginVisitLatestRequests(); // Setup search cy.get('#swh-search-origins-with-visit') .uncheck({force: true}) .get('#swh-filter-empty-visits') .uncheck({force: true}) .then(() => { const searchText = 'many.origins'; // Get first page of results doSearch(searchText); cy.wait('@originVisitLatest'); cy.get('#origins-prev-results-button') .should('have.class', 'disabled'); cy.get('#origins-next-results-button') .should('not.have.class', 'disabled'); // Get second page of results cy.get('#origins-next-results-button a') .click(); cy.wait('@originVisitLatest'); cy.get('#origins-prev-results-button') .should('not.have.class', 'disabled'); cy.get('#origins-next-results-button') .should('not.have.class', 'disabled'); // Get third (and last) page of results cy.get('#origins-next-results-button a') .click(); cy.get('#origins-prev-results-button') .should('not.have.class', 'disabled'); cy.get('#origins-next-results-button') .should('have.class', 'disabled'); // Get second page of results again cy.get('#origins-prev-results-button a') .click(); cy.wait('@originVisitLatest'); cy.get('.swh-search-result-entry') .should('have.length', 100); cy.get('.swh-search-result-entry#origin-0 td a') .should('have.text', 'https://many.origins/101'); cy.get('.swh-search-result-entry#origin-99 td a') .should('have.text', 'https://many.origins/200'); cy.get('#origins-prev-results-button') .should('not.have.class', 'disabled'); cy.get('#origins-next-results-button') .should('not.have.class', 'disabled'); // Get first page of results again cy.get('#origins-prev-results-button a') .click(); cy.wait('@originVisitLatest'); cy.get('.swh-search-result-entry') .should('have.length', 100); cy.get('.swh-search-result-entry#origin-0 td a') .should('have.text', 'https://many.origins/1'); cy.get('.swh-search-result-entry#origin-99 td a') .should('have.text', 'https://many.origins/100'); cy.get('#origins-prev-results-button') .should('have.class', 'disabled'); cy.get('#origins-next-results-button') .should('not.have.class', 'disabled'); }); }); }); context('Test valid SWHIDs', function() { it('should resolve directory', function() { const redirectUrl = this.Urls.browse_directory(origin.content[0].directory); const swhid = `swh:1:dir:${origin.content[0].directory}`; searchShouldRedirect(swhid, redirectUrl); }); it('should resolve revision', function() { const redirectUrl = this.Urls.browse_revision(origin.revisions[0]); const swhid = `swh:1:rev:${origin.revisions[0]}`; searchShouldRedirect(swhid, redirectUrl); }); it('should resolve snapshot', function() { const redirectUrl = this.Urls.browse_snapshot_directory(origin.snapshot); const swhid = `swh:1:snp:${origin.snapshot}`; searchShouldRedirect(swhid, redirectUrl); }); it('should resolve content', function() { const redirectUrl = this.Urls.browse_content(`sha1_git:${origin.content[0].sha1git}`); const swhid = `swh:1:cnt:${origin.content[0].sha1git}`; searchShouldRedirect(swhid, redirectUrl); }); it('should not send request to the search endpoint', function() { const swhid = `swh:1:rev:${origin.revisions[0]}`; cy.intercept(this.Urls.api_1_resolve_swhid(swhid)) .as('resolveSWHID'); cy.intercept(`${this.Urls.api_1_origin_search('').slice(0, -1)}**`) .as('searchOrigin'); cy.get('#swh-origins-url-patterns') .type(swhid); cy.get('.swh-search-icon') .click(); cy.wait('@resolveSWHID'); cy.xhrShouldBeCalled('resolveSWHID', 1); cy.xhrShouldBeCalled('searchOrigin', 0); }); }); context('Test invalid SWHIDs', function() { it('should show not found for directory', function() { const swhid = `swh:1:dir:${this.unarchivedRepo.rootDirectory}`; const msg = `Directory with sha1_git ${this.unarchivedRepo.rootDirectory} not found`; searchShouldShowNotFound(swhid, msg); }); it('should show not found for snapshot', function() { const swhid = `swh:1:snp:${this.unarchivedRepo.snapshot}`; const msg = `Snapshot with id ${this.unarchivedRepo.snapshot} not found!`; searchShouldShowNotFound(swhid, msg); }); it('should show not found for revision', function() { const swhid = `swh:1:rev:${this.unarchivedRepo.revision}`; const msg = `Revision with sha1_git ${this.unarchivedRepo.revision} not found.`; searchShouldShowNotFound(swhid, msg); }); it('should show not found for content', function() { const swhid = `swh:1:cnt:${this.unarchivedRepo.content[0].sha1git}`; const msg = `Content with sha1_git checksum equals to ${this.unarchivedRepo.content[0].sha1git} not found!`; searchShouldShowNotFound(swhid, msg); }); function checkInvalidSWHIDReport(url, searchInputElt, swhidInput, validationMessagePattern = '') { cy.visit(url); doSearch(swhidInput, searchInputElt); cy.get(searchInputElt) .then($el => $el[0].checkValidity()).should('be.false'); cy.get(searchInputElt) .invoke('prop', 'validationMessage') .should('not.equal', '') .should('contain', validationMessagePattern); } it('should report invalid SWHID in search page input', function() { const swhidInput = `swh:1:cnt:${this.unarchivedRepo.content[0].sha1git};lines=45-60/`; checkInvalidSWHIDReport(this.Urls.browse_search(), '#swh-origins-url-patterns', swhidInput); cy.get('.invalid-feedback') .should('be.visible'); }); it('should report invalid SWHID in top right search input', function() { const swhidInput = `swh:1:cnt:${this.unarchivedRepo.content[0].sha1git};lines=45-60/`; checkInvalidSWHIDReport(this.Urls.browse_help(), '#swh-origins-search-top-input', swhidInput); }); it('should report SWHID with uppercase chars in search page input', function() { const swhidInput = `swh:1:cnt:${this.unarchivedRepo.content[0].sha1git}`.toUpperCase(); checkInvalidSWHIDReport(this.Urls.browse_search(), '#swh-origins-url-patterns', swhidInput, swhidInput.toLowerCase()); cy.get('.invalid-feedback') .should('be.visible'); }); it('should report SWHID with uppercase chars in top right search input', function() { let swhidInput = `swh:1:cnt:${this.unarchivedRepo.content[0].sha1git}`.toUpperCase(); swhidInput += ';lines=45-60/'; checkInvalidSWHIDReport(this.Urls.browse_help(), '#swh-origins-search-top-input', swhidInput.toLowerCase()); }); }); });