diff --git a/assets/src/bundles/add_forge/create-request.js b/assets/src/bundles/add_forge/create-request.js
index d9026f3e..75ff23c6 100644
--- a/assets/src/bundles/add_forge/create-request.js
+++ b/assets/src/bundles/add_forge/create-request.js
@@ -1,143 +1,131 @@
/**
* Copyright (C) 2022 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 {handleFetchError, csrfPost,
+import {handleFetchError, errorMessageFromResponse, csrfPost,
getHumanReadableDate} from 'utils/functions';
import userRequestsFilterCheckboxFn from 'utils/requests-filter-checkbox.ejs';
import {swhSpinnerSrc} from 'utils/constants';
let requestBrowseTable;
const addForgeCheckboxId = 'swh-add-forge-user-filter';
const userRequestsFilterCheckbox = userRequestsFilterCheckboxFn({
'inputId': addForgeCheckboxId,
'checked': true // by default, display only user requests
});
export function onCreateRequestPageLoad() {
$(document).ready(() => {
$('#requestCreateForm').submit(async function(event) {
event.preventDefault();
try {
const response = await csrfPost($(this).attr('action'),
{'Content-Type': 'application/x-www-form-urlencoded'},
$(this).serialize());
handleFetchError(response);
$('#userMessageDetail').empty();
$('#userMessage').text('Your request has been submitted');
$('#userMessage').removeClass('badge-danger');
$('#userMessage').addClass('badge-success');
requestBrowseTable.draw(); // redraw the table to update the list
} catch (errorResponse) {
$('#userMessageDetail').empty();
let errorMessage;
- let errorMessageDetail = '';
const errorData = await errorResponse.json();
// if (errorResponse.content_type === 'text/plain') { // does not work?
if (errorResponse.status === 409) {
errorMessage = errorData;
} else { // assuming json response
// const exception = errorData['exception'];
- errorMessage = 'An unknown error occurred during the request creation';
- try {
- const reason = JSON.parse(errorData['reason']);
- Object.entries(reason).forEach((keys, _) => {
- const key = keys[0];
- const message = keys[1][0]; // take only the first issue
- errorMessageDetail += `\n${key}: ${message}`;
- });
- } catch (_) {
- errorMessageDetail = errorData['reason']; // can't parse it, leave it raw
- }
+ errorMessage = errorMessageFromResponse(
+ errorData, 'An unknown error occurred during the request creation');
}
- $('#userMessage').text(
- errorMessageDetail ? `Error: ${errorMessageDetail}` : errorMessage
- );
+ $('#userMessage').text(errorMessage);
$('#userMessage').removeClass('badge-success');
$('#userMessage').addClass('badge-danger');
}
});
populateRequestBrowseList(); // Load existing requests
});
}
export function populateRequestBrowseList() {
requestBrowseTable = $('#add-forge-request-browse')
.on('error.dt', (e, settings, techNote, message) => {
$('#add-forge-browse-request-error').text(message);
})
.DataTable({
serverSide: true,
processing: true,
language: {
processing: `
`
},
retrieve: true,
searching: true,
info: false,
// Layout configuration, see [1] for more details
// [1] https://datatables.net/reference/option/dom
dom: '<"row"<"col-sm-3"l><"col-sm-6 text-left user-requests-filter"><"col-sm-3"f>>' +
'<"row"<"col-sm-12"tr>>' +
'<"row"<"col-sm-5"i><"col-sm-7"p>>',
ajax: {
'url': Urls.add_forge_request_list_datatables(),
data: (d) => {
const checked = $(`#${addForgeCheckboxId}`).prop('checked');
// If this function is called while the page is loading, 'checked' is
// undefined. As the checkbox defaults to being checked, coerce this to true.
if (swh.webapp.isUserLoggedIn() && (checked === undefined || checked)) {
d.user_requests_only = '1';
}
}
},
fnInitComplete: function() {
if (swh.webapp.isUserLoggedIn()) {
$('div.user-requests-filter').html(userRequestsFilterCheckbox);
$(`#${addForgeCheckboxId}`).on('change', () => {
requestBrowseTable.draw();
});
}
},
columns: [
{
data: 'submission_date',
name: 'submission_date',
render: getHumanReadableDate
},
{
data: 'forge_type',
name: 'forge_type',
render: $.fn.dataTable.render.text()
},
{
data: 'forge_url',
name: 'forge_url',
render: function(data, type, row) {
if (type === 'display') {
let html = '';
const sanitizedURL = $.fn.dataTable.render.text().display(data);
html += sanitizedURL;
html += ` ` +
'';
return html;
}
return data;
}
},
{
data: 'status',
name: 'status',
render: function(data, type, row, meta) {
return swh.add_forge.formatRequestStatusName(data);
}
}
]
});
}
diff --git a/assets/src/bundles/browse/origin-search.js b/assets/src/bundles/browse/origin-search.js
index 2f571f85..06ac97f7 100644
--- a/assets/src/bundles/browse/origin-search.js
+++ b/assets/src/bundles/browse/origin-search.js
@@ -1,268 +1,270 @@
/**
* 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 {handleFetchError, isArchivedOrigin} from 'utils/functions';
+import {handleFetchError, errorMessageFromResponse, isArchivedOrigin} from 'utils/functions';
const limit = 100;
const linksPrev = [];
let linkNext = null;
let linkCurrent = null;
let inSearch = false;
function parseLinkHeader(s) {
const re = /<(.+)>; rel="next"/;
return s.match(re)[1];
}
function fixTableRowsStyle() {
setTimeout(() => {
$('#origin-search-results tbody tr').removeAttr('style');
});
}
function clearOriginSearchResultsTable() {
$('#origin-search-results tbody tr').remove();
}
async function populateOriginSearchResultsTable(origins) {
if (origins.length > 0) {
$('#swh-origin-search-results').show();
$('#swh-no-result').hide();
clearOriginSearchResultsTable();
const table = $('#origin-search-results tbody');
const promises = [];
for (const [i, origin] of origins.entries()) {
const browseUrl = `${Urls.browse_origin()}?origin_url=${encodeURIComponent(origin.url)}`;
let tableRow =
`
`;
tableRow +=
`` +
'' +
'Checking | ';
tableRow +=
'' +
`${origin.url} | `;
tableRow +=
`` +
'' +
'Checking | ';
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';
promises.push(fetch(latestSnapshotUrl));
}
const responses = await Promise.all(promises);
const responsesData = await Promise.all(responses.map(r => r.json()));
for (let i = 0; i < responses.length; ++i) {
const response = responses[i];
const data = responsesData[i];
if (response.status !== 404 && data.type) {
$(`#visit-type-origin-${i}`).html(data.type);
$(`#visit-status-origin-${i}`).html(
'Archived');
} else {
$(`#visit-type-origin-${i}`).html('unknown');
$(`#visit-status-origin-${i}`).html(
'Pending archival');
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 (linkNext === null) {
$('#origins-next-results-button').addClass('disabled');
} else {
$('#origins-next-results-button').removeClass('disabled');
}
if (linksPrev.length === 0) {
$('#origins-prev-results-button').addClass('disabled');
} else {
$('#origins-prev-results-button').removeClass('disabled');
}
inSearch = false;
setTimeout(() => {
window.scrollTo(0, 0);
});
}
function searchOriginsFirst(searchQueryText, limit) {
let baseSearchUrl;
const searchMetadata = $('#swh-search-origin-metadata').prop('checked');
if (searchMetadata) {
baseSearchUrl = new URL(Urls.api_1_origin_metadata_search(), window.location);
baseSearchUrl.searchParams.append('fulltext', searchQueryText);
} else {
const useSearchQL = $('#swh-search-use-ql').prop('checked');
baseSearchUrl = new URL(Urls.api_1_origin_search(searchQueryText), window.location);
baseSearchUrl.searchParams.append('use_ql', useSearchQL ?? false);
}
const withVisit = $('#swh-search-origins-with-visit').prop('checked');
baseSearchUrl.searchParams.append('limit', limit);
baseSearchUrl.searchParams.append('with_visit', withVisit);
const visitType = $('#swh-search-visit-type').val();
if (visitType !== 'any') {
baseSearchUrl.searchParams.append('visit_type', visitType);
}
const searchUrl = baseSearchUrl.toString();
searchOrigins(searchUrl);
}
async function searchOrigins(searchUrl) {
clearOriginSearchResultsTable();
$('.swh-loading').addClass('show');
try {
const response = await fetch(searchUrl);
handleFetchError(response);
const data = await response.json();
// Save link to the current results page
linkCurrent = searchUrl;
// Save link to the next results page.
linkNext = null;
if (response.headers.has('Link')) {
const parsedLink = parseLinkHeader(response.headers.get('Link'));
if (parsedLink !== undefined) {
linkNext = parsedLink;
}
}
// prevLinks is updated by the caller, which is the one to know if
// we're going forward or backward in the pages.
$('.swh-loading').removeClass('show');
populateOriginSearchResultsTable(data);
- } catch (response) {
+ } catch (errorResponse) {
+ const errorData = await errorResponse.json();
$('.swh-loading').removeClass('show');
inSearch = false;
$('#swh-origin-search-results').hide();
- $('#swh-no-result').text(`Error ${response.status}: ${response.statusText}`);
+ $('#swh-no-result').text(errorMessageFromResponse(
+ errorData, 'An unknown error occurred while searching origins'));
$('#swh-no-result').show();
}
}
async function doSearch() {
$('#swh-no-result').hide();
const searchQueryText = $('#swh-origins-url-patterns').val();
inSearch = true;
if (searchQueryText.startsWith('swh:')) {
try {
// searchQueryText may be a PID so sending search queries to PID resolve endpoint
const resolveSWHIDUrl = Urls.api_1_resolve_swhid(searchQueryText);
const response = await fetch(resolveSWHIDUrl);
handleFetchError(response);
const data = await response.json();
// SWHID has been successfully resolved,
// so redirect to browse page
window.location = data.browse_url;
} catch (response) {
// display a useful error message if the input
// looks like a SWHID
const data = await response.json();
$('#swh-origin-search-results').hide();
$('.swh-search-pagination').hide();
$('#swh-no-result').text(data.reason);
$('#swh-no-result').show();
}
} else if (await isArchivedOrigin(searchQueryText)) {
// redirect to the browse origin
window.location.href =
`${Urls.browse_origin()}?origin_url=${encodeURIComponent(searchQueryText)}`;
} else {
// otherwise, proceed with origins search irrespective of the error
$('#swh-origin-search-results').show();
$('.swh-search-pagination').show();
searchOriginsFirst(searchQueryText, limit);
}
}
export function initOriginSearch() {
$(document).ready(() => {
$('#swh-search-origins').submit(event => {
event.preventDefault();
if (event.target.checkValidity()) {
$(event.target).removeClass('was-validated');
const searchQueryText = $('#swh-origins-url-patterns').val().trim();
const withVisit = $('#swh-search-origins-with-visit').prop('checked');
const withContent = $('#swh-filter-empty-visits').prop('checked');
const useSearchQL = $('#swh-search-use-ql').prop('checked');
const searchMetadata = $('#swh-search-origin-metadata').prop('checked');
const visitType = $('#swh-search-visit-type').val();
const queryParameters = new URLSearchParams();
queryParameters.append('q', searchQueryText);
if (withVisit) {
queryParameters.append('with_visit', withVisit);
}
if (withContent) {
queryParameters.append('with_content', withContent);
}
if (useSearchQL) {
queryParameters.append('use_ql', useSearchQL ?? false);
}
if (searchMetadata) {
queryParameters.append('search_metadata', searchMetadata);
}
if (visitType !== 'any') {
queryParameters.append('visit_type', visitType);
}
// Update the url, triggering page reload and effective search
window.location = `${Urls.browse_search()}?${queryParameters.toString()}`;
} else {
$(event.target).addClass('was-validated');
}
});
$('#origins-next-results-button').click(event => {
if ($('#origins-next-results-button').hasClass('disabled') || inSearch) {
return;
}
inSearch = true;
linksPrev.push(linkCurrent);
searchOrigins(linkNext);
event.preventDefault();
});
$('#origins-prev-results-button').click(event => {
if ($('#origins-prev-results-button').hasClass('disabled') || inSearch) {
return;
}
inSearch = true;
searchOrigins(linksPrev.pop());
event.preventDefault();
});
const urlParams = new URLSearchParams(window.location.search);
const query = urlParams.get('q');
const withVisit = urlParams.has('with_visit');
const useSearchQL = urlParams.has('use_ql');
const withContent = urlParams.has('with_content');
const searchMetadata = urlParams.has('search_metadata');
const visitType = urlParams.get('visit_type');
if (query) {
$('#swh-origins-url-patterns').val(query);
$('#swh-search-origins-with-visit').prop('checked', withVisit);
$('#swh-search-use-ql').prop('checked', useSearchQL ?? false);
$('#swh-filter-empty-visits').prop('checked', withContent);
$('#swh-search-origin-metadata').prop('checked', searchMetadata);
if (visitType) {
$('#swh-search-visit-type').val(visitType);
}
doSearch();
}
});
}
diff --git a/assets/src/utils/functions.js b/assets/src/utils/functions.js
index ae42d335..da8bc73a 100644
--- a/assets/src/utils/functions.js
+++ b/assets/src/utils/functions.js
@@ -1,153 +1,168 @@
/**
* Copyright (C) 2018-2020 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
*/
// utility functions
import Cookies from 'js-cookie';
export function handleFetchError(response) {
if (!response.ok) {
throw response;
}
return response;
}
export function handleFetchErrors(responses) {
for (let i = 0; i < responses.length; ++i) {
if (!responses[i].ok) {
throw responses[i];
}
}
return responses;
}
+export function errorMessageFromResponse(errorData, defaultMessage) {
+ let errorMessage = '';
+ try {
+ const reason = JSON.parse(errorData['reason']);
+ Object.entries(reason).forEach((keys, _) => {
+ const key = keys[0];
+ const message = keys[1][0]; // take only the first issue
+ errorMessage += `\n${key}: ${message}`;
+ });
+ } catch (_) {
+ errorMessage = errorData['reason']; // can't parse it, leave it raw
+ }
+ return errorMessage ? `Error: ${errorMessage}` : defaultMessage;
+}
+
export function staticAsset(asset) {
return `${__STATIC__}${asset}`;
}
export function csrfPost(url, headers = {}, body = null) {
headers['X-CSRFToken'] = Cookies.get('csrftoken');
return fetch(url, {
credentials: 'include',
headers: headers,
method: 'POST',
body: body
});
}
export function isGitRepoUrl(url, pathPrefix = '/') {
const allowedProtocols = ['http:', 'https:', 'git:'];
if (allowedProtocols.find(protocol => protocol === url.protocol) === undefined) {
return false;
}
if (!url.pathname.startsWith(pathPrefix)) {
return false;
}
const re = new RegExp('[\\w\\.-]+\\/?(?!=.git)(?:\\.git\\/?)?$');
return re.test(url.pathname.slice(pathPrefix.length));
};
export function removeUrlFragment() {
history.replaceState('', document.title, window.location.pathname + window.location.search);
}
export function selectText(startNode, endNode) {
const selection = window.getSelection();
selection.removeAllRanges();
const range = document.createRange();
range.setStart(startNode, 0);
if (endNode.nodeName !== '#text') {
range.setEnd(endNode, endNode.childNodes.length);
} else {
range.setEnd(endNode, endNode.textContent.length);
}
selection.addRange(range);
}
export function htmlAlert(type, message, closable = false) {
let closeButton = '';
let extraClasses = '';
if (closable) {
closeButton =
``;
extraClasses = 'alert-dismissible';
}
return ``;
}
export function isValidURL(string) {
try {
new URL(string);
} catch (_) {
return false;
}
return true;
}
export async function isArchivedOrigin(originPath) {
if (!isValidURL(originPath)) {
// Not a valid URL, return immediately
return false;
} else {
const response = await fetch(Urls.api_1_origin(originPath));
return response.ok && response.status === 200; // Success response represents an archived origin
}
}
async function getCanonicalGithubOriginURL(ownerRepo) {
const ghApiResponse = await fetch(`https://api.github.com/repos/${ownerRepo}`);
if (ghApiResponse.ok && ghApiResponse.status === 200) {
const ghApiResponseData = await ghApiResponse.json();
return ghApiResponseData.html_url;
}
}
export async function getCanonicalOriginURL(originUrl) {
let originUrlLower = originUrl.toLowerCase();
// github.com URL processing
const ghUrlRegex = /^http[s]*:\/\/github.com\//;
if (originUrlLower.match(ghUrlRegex)) {
// remove trailing .git
if (originUrlLower.endsWith('.git')) {
originUrlLower = originUrlLower.slice(0, -4);
}
// remove trailing slash
if (originUrlLower.endsWith('/')) {
originUrlLower = originUrlLower.slice(0, -1);
}
// extract {owner}/{repo}
const ownerRepo = originUrlLower.replace(ghUrlRegex, '');
// fetch canonical URL from github Web API
const url = getCanonicalGithubOriginURL(ownerRepo);
if (url) {
return url;
}
}
const ghpagesUrlRegex = /^http[s]*:\/\/(?[^/]+).github.io\/(?[^/]+)\/?.*/;
const parsedUrl = originUrlLower.match(ghpagesUrlRegex);
if (parsedUrl) {
const ownerRepo = `${parsedUrl.groups.owner}/${parsedUrl.groups.repo}`;
// fetch canonical URL from github Web API
const url = getCanonicalGithubOriginURL(ownerRepo);
if (url) {
return url;
}
}
return originUrl;
}
export function getHumanReadableDate(data) {
// Display iso format date string into a human readable date
// This is expected to be used by date field in datatable listing views
// Example: 3/24/2022, 10:31:08 AM
const date = new Date(data);
return date.toLocaleString();
}
diff --git a/cypress/integration/origin-search.spec.js b/cypress/integration/origin-search.spec.js
index 93fdc2c7..4589adbf 100644
--- a/cypress/integration/origin-search.spec.js
+++ b/cypress/integration/origin-search.spec.js
@@ -1,579 +1,608 @@
/**
* 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.slice(0, -1);
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);
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), {delay: 0, force: true});
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');
});
+ it('should show error messages when using the query language', function() {
+ cy.adminLogin();
+ cy.visit(url);
+
+ cy.intercept('GET', `${this.Urls.api_1_origin_search('**')}**`,
+ {
+ body: {
+ 'exception': 'BadInputExc',
+ 'reason': 'Syntax error in search query: Invalid query'
+ },
+ statusCode: 400
+ })
+ .as('searchOrigin');
+
+ cy.get('#swh-search-use-ql')
+ .should('exist')
+ .click({force: true}); // Covered by label
+
+ cy.get('#swh-origins-url-patterns')
+ .type('this is not a valid query')
+ .type('{enter}');
+
+ cy.wait('@searchOrigin').then((xhr) => {
+ cy.get('#swh-no-result')
+ .should('contain', 'Syntax error in search query');
+ });
+
+ });
+
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, {delay: 0, force: true});
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());
});
});
});