diff --git a/cypress/integration/origin-search.spec.js b/cypress/integration/origin-search.spec.js index 61158630..0771a4e1 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 nonExistentText = 'NoMatchExists'; let origin; let url; function doSearch(searchText) { cy.get('#origins-url-patterns') .type(searchText) .get('.swh-search-icon') .click(); } function searchShouldRedirect(searchText, redirectUrl) { doSearch(searchText); cy.location('pathname') .should('equal', redirectUrl); } function searchShouldShowNotFound(searchText, msg) { doSearch(searchText); cy.get('#swh-no-result') .should('be.visible') .and('contain', msg); } describe('Test origin-search', function() { before(function() { origin = this.origin[0]; url = this.Urls.browse_search(); }); beforeEach(function() { cy.visit(url); }); it('should show in result when url is searched', function() { cy.get('#origins-url-patterns') .type(origin.url); cy.get('.swh-search-icon') .click(); cy.get('#origin-search-results') .should('be.visible'); cy.contains('tr', origin.url) .should('be.visible') .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.'); }); 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() .get('#swh-filter-empty-visits') .check() .get('#swh-search-origin-metadata') .check() .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); }); }); }); context('Test valid persistent ids', function() { it('should resolve directory', function() { const redirectUrl = this.Urls.browse_directory(origin.content[0].directory); const persistentId = `swh:1:dir:${origin.content[0].directory}`; searchShouldRedirect(persistentId, redirectUrl); }); it('should resolve revision', function() { - const redirectUrl = this.Urls.browse_revision(origin.revision); - const persistentId = `swh:1:rev:${origin.revision}`; + const redirectUrl = this.Urls.browse_revision(origin.revisions[0]); + const persistentId = `swh:1:rev:${origin.revisions[0]}`; searchShouldRedirect(persistentId, redirectUrl); }); it('should resolve snapshot', function() { const redirectUrl = this.Urls.browse_snapshot_directory(origin.snapshot); const persistentId = `swh:1:snp:${origin.snapshot}`; searchShouldRedirect(persistentId, redirectUrl); }); it('should resolve content', function() { const redirectUrl = this.Urls.browse_content(`sha1_git:${origin.content[0].sha1git}`); const persistentId = `swh:1:cnt:${origin.content[0].sha1git}`; searchShouldRedirect(persistentId, redirectUrl); }); }); context('Test invalid persistent ids', function() { it('should show not found for directory', function() { const persistentId = `swh:1:dir:${this.unarchivedRepo.rootDirectory}`; const msg = `Directory with sha1_git ${this.unarchivedRepo.rootDirectory} not found`; searchShouldShowNotFound(persistentId, msg); }); it('should show not found for snapshot', function() { const persistentId = `swh:1:snp:${this.unarchivedRepo.snapshot}`; const msg = `Snapshot with id ${this.unarchivedRepo.snapshot} not found!`; searchShouldShowNotFound(persistentId, msg); }); it('should show not found for revision', function() { const persistentId = `swh:1:rev:${this.unarchivedRepo.revision}`; const msg = `Revision with sha1_git ${this.unarchivedRepo.revision} not found.`; searchShouldShowNotFound(persistentId, msg); }); it('should show not found for content', function() { const persistentId = `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(persistentId, msg); }); }); }); diff --git a/cypress/integration/revision-diff.spec.js b/cypress/integration/revision-diff.spec.js index 5e377f11..c3b78083 100644 --- a/cypress/integration/revision-diff.spec.js +++ b/cypress/integration/revision-diff.spec.js @@ -1,95 +1,95 @@ /** * 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 */ let origin; let diffData; describe('Test Diffs View', function() { before(function() { origin = this.origin[0]; - const url = this.Urls.browse_revision(origin.revision) + `?origin=${origin.url}`; + const url = this.Urls.browse_revision(origin.revisions[0]) + `?origin=${origin.url}`; cy.visit(url).window().then(win => { cy.request(win.diffRevUrl) .then(res => { diffData = res.body; }); }); }); beforeEach(function() { - const url = this.Urls.browse_revision(origin.revision) + `?origin=${origin.url}`; + const url = this.Urls.browse_revision(origin.revisions[0]) + `?origin=${origin.url}`; cy.visit(url); cy.get('a[data-toggle="tab"]') .contains('Changes') .click(); }); it('should list all files with changes', function() { let files = new Set([]); for (let change of diffData.changes) { files.add(change.from_path); files.add(change.to_path); } for (let file of files) { cy.get('#swh-revision-changes-list a') .contains(file) .should('be.visible'); } }); it('should load diffs when scrolled down', function() { cy.get('#swh-revision-changes-list a') .each($el => { cy.get($el.attr('href')) .scrollIntoView() .find('.swh-content') .should('be.visible'); }); }); it('should compute all diffs when selected', function() { cy.get('#swh-compute-all-diffs') .click(); cy.get('#swh-revision-changes-list a') .each($el => { cy.get($el.attr('href')) .find('.swh-content') .should('be.visible'); }); }); it('should have correct links in diff file names', function() { for (let change of diffData.changes) { cy.get(`#swh-revision-changes-list a[href="#panel_${change.id}"`) .should('be.visible'); } }); it('should load unified diff by default', function() { cy.get('#swh-compute-all-diffs') .click(); for (let change of diffData.changes) { cy.get(`#${change.id}-unified-diff`) .should('be.visible'); cy.get(`#${change.id}-splitted-diff`) .should('not.be.visible'); } }); it('should switch between unified and side-by-side diff when selected', function() { // Test for first diff const id = diffData.changes[0].id; cy.get(`#panel_${id}`) .contains('label', 'Side-by-side') .click(); cy.get(`#${id}-splitted-diff`) .should('be.visible') .get(`#${id}-unified-diff`) .should('not.be.visible'); }); }); diff --git a/cypress/integration/vault.spec.js b/cypress/integration/vault.spec.js index bc8771be..80a3e58c 100644 --- a/cypress/integration/vault.spec.js +++ b/cypress/integration/vault.spec.js @@ -1,151 +1,296 @@ /** * 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 */ +let vaultItems = []; + +const progressbarColors = { + 'new': 'rgba(128, 128, 128, 0.5)', + 'pending': 'rgba(0, 0, 255, 0.5)', + 'done': 'rgb(92, 184, 92)' +}; + function createVaultCookingTask(objectType) { cy.contains('button', 'Actions') .click(); cy.contains('.dropdown-item', 'Download') .click(); cy.contains('.dropdown-item', objectType) .click(); cy.get('.modal-dialog') - .contains('button', 'Ok') + .contains('button:visible', 'Ok') .click(); } +// Mocks API response : /api/1/vault/(:objectType)/(:hash) +// objectType : {'directory', 'revision'} function genVaultCookingResponse(objectType, objectId, status, message, fetchUrl) { return { 'obj_type': objectType, 'id': 1, 'progress_message': message, 'status': status, 'obj_id': objectId, 'fetch_url': fetchUrl }; }; +// Tests progressbar color, status +// And status in localStorage +function testStatus(taskId, color, statusMsg, status) { + cy.get(`.swh-vault-table #vault-task-${taskId}`) + .should('be.visible') + .find('.progress-bar') + .should('be.visible') + .and('have.css', 'background-color', color) + .and('contain', statusMsg) + .then(() => { + // Vault item with object_id as taskId should exist in localStorage + const currentVaultItems = JSON.parse(window.localStorage.getItem('swh-vault-cooking-tasks')); + const vaultItem = currentVaultItems.find(obj => obj.object_id === taskId); + + assert.isNotNull(vaultItem); + assert.strictEqual(vaultItem.status, status); + }); +} + describe('Vault Cooking User Interface Tests', function() { before(function() { this.directory = this.origin[0].directory[0].id; this.directoryUrl = this.Urls.browse_directory(this.directory); this.vaultDirectoryUrl = this.Urls.api_1_vault_cook_directory(this.directory); this.vaultFetchDirectoryUrl = this.Urls.api_1_vault_fetch_directory(this.directory); + + this.revision = this.origin[1].revisions[0]; + this.revisionUrl = this.Urls.browse_revision(this.revision); + this.vaultRevisionUrl = this.Urls.api_1_vault_cook_revision_gitfast(this.revision); + this.vaultFetchRevisionUrl = this.Urls.api_1_vault_fetch_revision_gitfast(this.revision); + + vaultItems[0] = { + 'object_type': 'revision', + 'object_id': this.revision, + 'email': '', + 'status': 'done', + 'fetch_url': `/api/1/vault/revision/${this.revision}/gitfast/raw/`, + 'progress_message': null + }; + }); + + beforeEach(function() { this.genVaultDirCookingResponse = (status, message = null) => { return genVaultCookingResponse('directory', this.directory, status, message, this.vaultFetchDirectoryUrl); }; - this.revision = this.origin[1].revision[0]; - this.revisionUrl = this.Urls.browse_revision(this.revision); - this.vaultRevisionUrl = this.Urls.api_1_vault_cook_revision_gitfast(this.revision); - this.vaultFetchRevisionUrl = this.Urls.api_1_vault_fetch_revision_gitfast(this.revision); this.genVaultRevCookingResponse = (status, message = null) => { return genVaultCookingResponse('revision', this.revision, status, message, this.vaultFetchRevisionUrl); }; cy.server(); }); it('should create a directory cooking task and report its status', function() { // Browse a directory cy.visit(this.directoryUrl); // Stub responses when requesting the vault API to simulate // a task has been created cy.route({ method: 'POST', url: this.vaultDirectoryUrl, response: this.genVaultDirCookingResponse('new') }).as('createVaultCookingTask'); cy.route({ method: 'GET', url: this.vaultDirectoryUrl, response: this.genVaultDirCookingResponse('new') }).as('checkVaultCookingTask'); // Create a vault cooking task through the GUI createVaultCookingTask('Directory'); cy.wait('@createVaultCookingTask'); // Check that a redirection to the vault UI has been performed cy.url().should('eq', Cypress.config().baseUrl + this.Urls.browse_vault()); - cy.wait('@checkVaultCookingTask'); - - // TODO: - check that a row has been created for the task in - // the displayed table - // - // - check progress bar state and color + cy.wait('@checkVaultCookingTask').then(() => { + testStatus(this.directory, progressbarColors['new'], 'new', 'new'); + }); // Stub response to the vault API indicating the task is processing cy.route({ method: 'GET', url: this.vaultDirectoryUrl, response: this.genVaultDirCookingResponse('pending', 'Processing...') }).as('checkVaultCookingTask'); - cy.wait('@checkVaultCookingTask'); - - // TODO: check progress bar state and color + cy.wait('@checkVaultCookingTask').then(() => { + testStatus(this.directory, progressbarColors['pending'], 'Processing...', 'pending'); + }); // Stub response to the vault API indicating the task is finished cy.route({ method: 'GET', url: this.vaultDirectoryUrl, response: this.genVaultDirCookingResponse('done') }).as('checkVaultCookingTask'); - cy.wait('@checkVaultCookingTask'); - - // TODO: check progress bar state and color and that the download - // button appeared + cy.wait('@checkVaultCookingTask').then(() => { + testStatus(this.directory, progressbarColors['done'], 'done', 'done'); + }); - // Stub response to the vault API indicating to simulate archive - // download + // Stub response to the vault API to simulate archive download cy.route({ method: 'GET', url: this.vaultFetchDirectoryUrl, response: `fx:${this.directory}.tar.gz,binary`, headers: { 'Content-disposition': `attachment; filename=${this.directory}.tar.gz`, 'Content-Type': 'application/gzip' } }).as('fetchCookedArchive'); cy.get(`#vault-task-${this.directory} .vault-dl-link button`) .click(); cy.wait('@fetchCookedArchive').then((xhr) => { assert.isNotNull(xhr.response.body); }); }); it('should create a revision cooking task and report its status', function() { - // TODO: The above test must be factorized to handle the revision cooking test + // Browse a revision + cy.visit(this.revisionUrl); + + // Stub responses when requesting the vault API to simulate + // a task has been created + cy.route({ + method: 'POST', + url: this.vaultRevisionUrl, + response: this.genVaultRevCookingResponse('new') + }).as('createVaultCookingTask'); + + cy.route({ + method: 'GET', + url: this.vaultRevisionUrl, + response: this.genVaultRevCookingResponse('new') + }).as('checkVaultCookingTask'); + + // Create a vault cooking task through the GUI + createVaultCookingTask('Revision'); + + cy.wait('@createVaultCookingTask'); + + // Check that a redirection to the vault UI has been performed + cy.url().should('eq', Cypress.config().baseUrl + this.Urls.browse_vault()); + + cy.wait('@checkVaultCookingTask').then(() => { + testStatus(this.revision, progressbarColors['new'], 'new', 'new'); + }); + + // Stub response to the vault API indicating the task is processing + cy.route({ + method: 'GET', + url: this.vaultRevisionUrl, + response: this.genVaultRevCookingResponse('pending', 'Processing...') + }).as('checkVaultCookingTask'); + + cy.wait('@checkVaultCookingTask').then(() => { + testStatus(this.revision, progressbarColors['pending'], 'Processing...', 'pending'); + }); + + // Stub response to the vault API indicating the task is finished + cy.route({ + method: 'GET', + url: this.vaultRevisionUrl, + response: this.genVaultRevCookingResponse('done') + }).as('checkVaultCookingTask'); + + cy.wait('@checkVaultCookingTask').then(() => { + testStatus(this.revision, progressbarColors['done'], 'done', 'done'); + }); + + // Stub response to the vault API indicating to simulate archive + // download + cy.route({ + method: 'GET', + url: this.vaultFetchRevisionUrl, + response: `fx:${this.revision}.gitfast.gz,binary`, + headers: { + 'Content-disposition': `attachment; filename=${this.revision}.gitfast.gz`, + 'Content-Type': 'application/gzip' + } + }).as('fetchCookedArchive'); + + cy.get(`#vault-task-${this.revision} .vault-dl-link button`) + .click(); + + cy.wait('@fetchCookedArchive').then((xhr) => { + assert.isNotNull(xhr.response.body); + }); }); it('should offer to recook an archive if no more available to download', function() { - // TODO: - // - Simulate an already executed task by filling the 'swh-vault-cooking-tasks' - // entry in browser localStorage (see vault-ui.js). - // - // - Stub the response to the archive fetch url to return a 404 error. - // - // - Check that the dialog offering to recook the archive is displayed - // and that the cooking task can be created from it. + cy.visit(this.Urls.browse_vault()) + .then(() => { + // Add uncooked task to localStorage + // which updates it in vault items list + window.localStorage.setItem('swh-vault-cooking-tasks', JSON.stringify(vaultItems)); + }); + + // Send 404 when fetching vault item + cy.route({ + method: 'GET', + status: 404, + url: this.vaultFetchRevisionUrl, + response: { + 'exception': 'NotFoundExc', + 'reason': `Revision with ID '${this.revision}' not found.` + }, + headers: { + 'Content-Type': 'json' + } + }).as('fetchCookedArchive'); + + cy.get(`#vault-task-${this.revision} .vault-dl-link button`) + .click(); + + cy.wait('@fetchCookedArchive').then(() => { + cy.route({ + method: 'POST', + url: this.vaultRevisionUrl, + response: this.genVaultRevCookingResponse('new') + }).as('createVaultCookingTask'); + + cy.route({ + method: 'GET', + url: this.vaultRevisionUrl, + response: this.genVaultRevCookingResponse('new') + }).as('checkVaultCookingTask'); + + cy.get('#vault-recook-object-modal > .modal-dialog') + .should('be.visible') + .contains('button:visible', 'Ok') + .click(); + + cy.wait('@createVaultCookingTask') + .wait('@checkVaultCookingTask') + .then(() => { + testStatus(this.revision, progressbarColors['new'], 'new', 'new'); + }); + }); }); }); diff --git a/cypress/support/index.js b/cypress/support/index.js index cbe946d5..a0a6910d 100644 --- a/cypress/support/index.js +++ b/cypress/support/index.js @@ -1,124 +1,124 @@ /** * 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 */ import '@cypress/code-coverage/support'; import {httpGetJson} from '../utils'; Cypress.Screenshot.defaults({ screenshotOnRunFailure: false }); before(function() { this.unarchivedRepo = { url: 'https://github.com/SoftwareHeritage/swh-web', revision: '7bf1b2f489f16253527807baead7957ca9e8adde', snapshot: 'd9829223095de4bb529790de8ba4e4813e38672d', rootDirectory: '7d887d96c0047a77e2e8c4ee9bb1528463677663', content: [{ sha1git: 'b203ec39300e5b7e97b6e20986183cbd0b797859' }] }; this.origin = [{ url: 'https://github.com/memononen/libtess2', content: [{ path: 'Source/tess.h' }, { path: 'premake4.lua' }], directory: [{ path: 'Source', id: 'cd19126d815470b28919d64b2a8e6a3e37f900dd' }], - revision: [], + revisions: [], invalidSubDir: 'Source1' }, { url: 'https://github.com/wcoder/highlightjs-line-numbers.js', content: [], directory: [], - revision: ['1c480a4573d2a003fc2630c21c2b25829de49972'] + revisions: ['1c480a4573d2a003fc2630c21c2b25829de49972'] }]; const getMetadataForOrigin = async originUrl => { const originVisitsApiUrl = this.Urls.api_1_origin_visits(originUrl); const originVisits = await httpGetJson(originVisitsApiUrl); const lastVisit = originVisits[0]; const snapshotApiUrl = this.Urls.api_1_snapshot(lastVisit.snapshot); const lastOriginSnapshot = await httpGetJson(snapshotApiUrl); const revisionApiUrl = this.Urls.api_1_revision(lastOriginSnapshot.branches.HEAD.target); const lastOriginHeadRevision = await httpGetJson(revisionApiUrl); return { 'directory': lastOriginHeadRevision.directory, 'revision': lastOriginHeadRevision.id, 'snapshot': lastOriginSnapshot.id }; }; cy.visit('/').window().then(async win => { this.Urls = win.Urls; for (let origin of this.origin) { const metadata = await getMetadataForOrigin(origin.url); const directoryApiUrl = this.Urls.api_1_directory(metadata.directory); origin.dirContent = await httpGetJson(directoryApiUrl); origin.rootDirectory = metadata.directory; - origin.revision = metadata.revision; + origin.revisions.push(metadata.revision); origin.snapshot = metadata.snapshot; for (let content of origin.content) { cy.visit(this.Urls.browse_origin_content(origin.url, content.path)) .window().then(win => { const contentMetaData = win.swh.webapp.getBrowsedSwhObjectMetadata(); content.name = contentMetaData.filename; content.sha1git = contentMetaData.sha1_git; content.directory = contentMetaData.directory; content.rawFilePath = this.Urls.browse_content_raw(`sha1_git:${content.sha1git}`) + `?filename=${encodeURIComponent(content.name)}`; cy.request(content.rawFilePath) .then((response) => { const fileText = response.body; const fileLines = fileText.split('\n'); content.numberLines = fileLines.length; // If last line is empty its not shown if (!fileLines[content.numberLines - 1]) content.numberLines -= 1; }); }); } } }); }); // force the use of fetch polyfill wrapping XmlHttpRequest // in order for cypress to be able to intercept and stub them Cypress.on('window:before:load', win => { win.fetch = null; }); // Ensure code coverage data do not get lost each time a new // page is loaded during a single test execution let coverage = {}; Cypress.on('window:before:unload', e => { coverage = Object.assign(coverage, e.currentTarget.__coverage__); }); beforeEach(function() { coverage = {}; }); afterEach(function() { cy.task('combineCoverage', coverage); });