';
$(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/assets/src/bundles/origin/visits-histogram.js b/assets/src/bundles/origin/visits-histogram.js
index 6198c918..d349ff35 100644
--- a/assets/src/bundles/origin/visits-histogram.js
+++ b/assets/src/bundles/origin/visits-histogram.js
@@ -1,336 +1,336 @@
/**
* 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
*/
// Creation of a stacked histogram with D3.js for software origin visits history
// Parameters description:
// - container: selector for the div that will contain the histogram
// - visitsData: raw swh origin visits data
// - currentYear: the visits year to display by default
// - yearClickCallback: callback when the user selects a year through the histogram
export async function createVisitsHistogram(container, visitsData, currentYear, yearClickCallback) {
const d3 = await import(/* webpackChunkName: "d3" */ 'utils/d3');
// remove previously created histogram and tooltip if any
d3.select(container).select('svg').remove();
d3.select('div.d3-tooltip').remove();
// histogram size and margins
let width = 1000;
let height = 200;
- let margin = {top: 20, right: 80, bottom: 30, left: 50};
+ const margin = {top: 20, right: 80, bottom: 30, left: 50};
// create responsive svg
- let svg = d3.select(container)
+ const svg = d3.select(container)
.attr('style',
'padding-bottom: ' + Math.ceil(height * 100 / width) + '%')
.append('svg')
.attr('viewBox', '0 0 ' + width + ' ' + height);
// create tooltip div
- let tooltip = d3.select('body')
+ const tooltip = d3.select('body')
.append('div')
.attr('class', 'd3-tooltip')
.style('opacity', 0);
// update width and height without margins
width = width - margin.left - margin.right;
height = height - margin.top - margin.bottom;
// create main svg group element
- let g = svg.append('g').attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
+ const g = svg.append('g').attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
// create x scale
- let x = d3.scaleTime().rangeRound([0, width]);
+ const x = d3.scaleTime().rangeRound([0, width]);
// create y scale
- let y = d3.scaleLinear().range([height, 0]);
+ const y = d3.scaleLinear().range([height, 0]);
// create ordinal colorscale mapping visit status
- let colors = d3.scaleOrdinal()
+ const colors = d3.scaleOrdinal()
.domain(['full', 'partial', 'failed', 'ongoing'])
.range(['#008000', '#edc344', '#ff0000', '#0000ff']);
// first swh crawls were made in 2015
- let startYear = 2015;
+ const startYear = 2015;
// set latest display year as the current one
- let now = new Date();
- let endYear = now.getUTCFullYear() + 1;
- let monthExtent = [new Date(Date.UTC(startYear, 0, 1)), new Date(Date.UTC(endYear, 0, 1))];
+ const now = new Date();
+ const endYear = now.getUTCFullYear() + 1;
+ const monthExtent = [new Date(Date.UTC(startYear, 0, 1)), new Date(Date.UTC(endYear, 0, 1))];
// create months bins based on setup extent
- let monthBins = d3.timeMonths(d3.timeMonth.offset(monthExtent[0], -1), monthExtent[1]);
+ const monthBins = d3.timeMonths(d3.timeMonth.offset(monthExtent[0], -1), monthExtent[1]);
// create years bins based on setup extent
- let yearBins = d3.timeYears(monthExtent[0], monthExtent[1]);
+ const yearBins = d3.timeYears(monthExtent[0], monthExtent[1]);
// set x scale domain
x.domain(d3.extent(monthBins));
// use D3 histogram layout to create a function that will bin the visits by month
- let binByMonth = d3.histogram()
+ const binByMonth = d3.histogram()
.value(d => d.date)
.domain(x.domain())
.thresholds(monthBins);
// use D3 nest function to group the visits by status
- let visitsByStatus = d3.groups(visitsData, d => d['status'])
+ const visitsByStatus = d3.groups(visitsData, d => d['status'])
.sort((a, b) => d3.ascending(a[0], b[0]));
// prepare data in order to be able to stack visit statuses by month
- let statuses = [];
- let histData = [];
+ const statuses = [];
+ const histData = [];
for (let i = 0; i < monthBins.length; ++i) {
histData[i] = {};
}
visitsByStatus.forEach(entry => {
statuses.push(entry[0]);
- let monthsData = binByMonth(entry[1]);
+ const monthsData = binByMonth(entry[1]);
for (let i = 0; i < monthsData.length; ++i) {
histData[i]['x0'] = monthsData[i]['x0'];
histData[i]['x1'] = monthsData[i]['x1'];
histData[i][entry[0]] = monthsData[i];
}
});
// create function to stack visits statuses by month
- let stacked = d3.stack()
+ const stacked = d3.stack()
.keys(statuses)
.value((d, key) => d[key].length);
// compute the maximum amount of visits by month
- let yMax = d3.max(histData, d => {
+ const yMax = d3.max(histData, d => {
let total = 0;
for (let i = 0; i < statuses.length; ++i) {
total += d[statuses[i]].length;
}
return total;
});
// set y scale domain
y.domain([0, yMax]);
// compute ticks values for the y axis
- let step = 5;
- let yTickValues = [];
+ const step = 5;
+ const yTickValues = [];
for (let i = 0; i <= yMax / step; ++i) {
yTickValues.push(i * step);
}
if (yTickValues.length === 0) {
for (let i = 0; i <= yMax; ++i) {
yTickValues.push(i);
}
} else if (yMax % step !== 0) {
yTickValues.push(yMax);
}
// add histogram background grid
g.append('g')
.attr('class', 'grid')
.call(d3.axisLeft(y)
.tickValues(yTickValues)
.tickSize(-width)
.tickFormat(''));
// create one fill only rectangle by displayed year
// each rectangle will be made visible when hovering the mouse over a year range
// user will then be able to select a year by clicking in the rectangle
g.append('g')
.selectAll('rect')
.data(yearBins)
.enter().append('rect')
.attr('class', d => 'year' + d.getUTCFullYear())
.attr('fill', 'red')
.attr('fill-opacity', d => d.getUTCFullYear() === currentYear ? 0.3 : 0)
.attr('stroke', 'none')
.attr('x', d => {
- let date = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
+ const date = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
return x(date);
})
.attr('y', 0)
.attr('height', height)
.attr('width', d => {
- let date = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
- let yearWidth = x(d3.timeYear.offset(date, 1)) - x(date);
+ const date = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
+ const yearWidth = x(d3.timeYear.offset(date, 1)) - x(date);
return yearWidth;
})
// mouse event callbacks used to show rectangle years
// when hovering the mouse over the histograms
.on('mouseover', (event, d) => {
svg.selectAll('rect.year' + d.getUTCFullYear())
.attr('fill-opacity', 0.5);
})
.on('mouseout', (event, d) => {
svg.selectAll('rect.year' + d.getUTCFullYear())
.attr('fill-opacity', 0);
svg.selectAll('rect.year' + currentYear)
.attr('fill-opacity', 0.3);
})
// callback to select a year after a mouse click
// in a rectangle year
.on('click', (event, d) => {
svg.selectAll('rect.year' + currentYear)
.attr('fill-opacity', 0);
svg.selectAll('rect.yearoutline' + currentYear)
.attr('stroke', 'none');
currentYear = d.getUTCFullYear();
svg.selectAll('rect.year' + currentYear)
.attr('fill-opacity', 0.5);
svg.selectAll('rect.yearoutline' + currentYear)
.attr('stroke', 'black');
yearClickCallback(currentYear);
});
// create the stacked histogram of visits
g.append('g')
.selectAll('g')
.data(stacked(histData))
.enter().append('g')
.attr('fill', d => colors(d.key))
.selectAll('rect')
.data(d => d)
.enter().append('rect')
.attr('class', d => 'month' + d.data.x1.getMonth())
.attr('x', d => x(d.data.x0))
.attr('y', d => y(d[1]))
.attr('height', d => y(d[0]) - y(d[1]))
.attr('width', d => x(d.data.x1) - x(d.data.x0) - 1)
// mouse event callbacks used to show rectangle years
// but also to show tooltip when hovering the mouse
// over the histogram bars
.on('mouseover', (event, d) => {
svg.selectAll('rect.year' + d.data.x1.getUTCFullYear())
.attr('fill-opacity', 0.5);
tooltip.transition()
.duration(200)
.style('opacity', 1);
- let ds = d.data.x1.toISOString().substr(0, 7).split('-');
+ const ds = d.data.x1.toISOString().substr(0, 7).split('-');
let tooltipText = '' + ds[1] + ' / ' + ds[0] + ': ';
for (let i = 0; i < statuses.length; ++i) {
- let visitStatus = statuses[i];
- let nbVisits = d.data[visitStatus].length;
+ const visitStatus = statuses[i];
+ const nbVisits = d.data[visitStatus].length;
if (nbVisits === 0) continue;
tooltipText += nbVisits + ' ' + visitStatus + ' visits';
if (i !== statuses.length - 1) tooltipText += ' ';
}
tooltip.html(tooltipText)
.style('left', event.pageX + 15 + 'px')
.style('top', event.pageY + 'px');
})
.on('mouseout', (event, d) => {
svg.selectAll('rect.year' + d.data.x1.getUTCFullYear())
.attr('fill-opacity', 0);
svg.selectAll('rect.year' + currentYear)
.attr('fill-opacity', 0.3);
tooltip.transition()
.duration(500)
.style('opacity', 0);
})
.on('mousemove', (event) => {
tooltip.style('left', event.pageX + 15 + 'px')
.style('top', event.pageY + 'px');
})
// callback to select a year after a mouse click
// inside a histogram bar
.on('click', (event, d) => {
svg.selectAll('rect.year' + currentYear)
.attr('fill-opacity', 0);
svg.selectAll('rect.yearoutline' + currentYear)
.attr('stroke', 'none');
currentYear = d.data.x1.getUTCFullYear();
svg.selectAll('rect.year' + currentYear)
.attr('fill-opacity', 0.5);
svg.selectAll('rect.yearoutline' + currentYear)
.attr('stroke', 'black');
yearClickCallback(currentYear);
});
// create one stroke only rectangle by displayed year
// that will be displayed on top of the histogram when the user has selected a year
g.append('g')
.selectAll('rect')
.data(yearBins)
.enter().append('rect')
.attr('class', d => 'yearoutline' + d.getUTCFullYear())
.attr('fill', 'none')
.attr('stroke', d => d.getUTCFullYear() === currentYear ? 'black' : 'none')
.attr('x', d => {
- let date = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
+ const date = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
return x(date);
})
.attr('y', 0)
.attr('height', height)
.attr('width', d => {
- let date = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
- let yearWidth = x(d3.timeYear.offset(date, 1)) - x(date);
+ const date = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
+ const yearWidth = x(d3.timeYear.offset(date, 1)) - x(date);
return yearWidth;
});
// add x axis with a tick for every 1st day of each year
- let xAxis = g.append('g')
+ const xAxis = g.append('g')
.attr('class', 'axis')
.attr('transform', 'translate(0,' + height + ')')
.call(
d3.axisBottom(x)
.ticks(d3.timeYear.every(1))
.tickFormat(d => d.getUTCFullYear())
);
// shift tick labels in order to display them at the middle
// of each year range
xAxis.selectAll('text')
.attr('transform', d => {
- let year = d.getUTCFullYear();
- let date = new Date(Date.UTC(year, 0, 1));
- let yearWidth = x(d3.timeYear.offset(date, 1)) - x(date);
+ const year = d.getUTCFullYear();
+ const date = new Date(Date.UTC(year, 0, 1));
+ const yearWidth = x(d3.timeYear.offset(date, 1)) - x(date);
return 'translate(' + -yearWidth / 2 + ', 0)';
});
// add y axis for the number of visits
g.append('g')
.attr('class', 'axis')
.call(d3.axisLeft(y).tickValues(yTickValues));
// add legend for visit statuses
- let legendGroup = g.append('g')
+ const legendGroup = g.append('g')
.attr('font-family', 'sans-serif')
.attr('font-size', 10)
.attr('text-anchor', 'end');
legendGroup.append('text')
.attr('x', width + margin.right - 5)
.attr('y', 9.5)
.attr('dy', '0.32em')
.text('visit status:');
- let legend = legendGroup.selectAll('g')
+ const legend = legendGroup.selectAll('g')
.data(statuses.slice().reverse())
.enter().append('g')
.attr('transform', (d, i) => 'translate(0,' + (i + 1) * 20 + ')');
legend.append('rect')
.attr('x', width + 2 * margin.right / 3)
.attr('width', 19)
.attr('height', 19)
.attr('fill', colors);
legend.append('text')
.attr('x', width + 2 * margin.right / 3 - 5)
.attr('y', 9.5)
.attr('dy', '0.32em')
.text(d => d);
// add text label for the y axis
g.append('text')
.attr('transform', 'rotate(-90)')
.attr('y', -margin.left)
.attr('x', -(height / 2))
.attr('dy', '1em')
.style('text-anchor', 'middle')
.text('Number of visits');
}
diff --git a/assets/src/bundles/origin/visits-reporting.js b/assets/src/bundles/origin/visits-reporting.js
index 9f541062..776f9179 100644
--- a/assets/src/bundles/origin/visits-reporting.js
+++ b/assets/src/bundles/origin/visits-reporting.js
@@ -1,138 +1,138 @@
/**
* Copyright (C) 2018 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 {createVisitsHistogram} from './visits-histogram';
import {updateCalendar} from './visits-calendar';
import './visits-reporting.css';
// will hold all visits
let allVisits;
// will hold filtered visits to display
let filteredVisits;
// will hold currently displayed year
let currentYear;
// function to gather full visits
function filterFullVisits(differentSnapshots) {
- let filteredVisits = [];
+ const filteredVisits = [];
for (let i = 0; i < allVisits.length; ++i) {
if (allVisits[i].status !== 'full') continue;
if (!differentSnapshots) {
filteredVisits.push(allVisits[i]);
} else if (filteredVisits.length === 0) {
filteredVisits.push(allVisits[i]);
} else {
- let lastVisit = filteredVisits[filteredVisits.length - 1];
+ const lastVisit = filteredVisits[filteredVisits.length - 1];
if (allVisits[i].snapshot !== lastVisit.snapshot) {
filteredVisits.push(allVisits[i]);
}
}
}
return filteredVisits;
}
// function to update the visits list view based on the selected year
function updateVisitsList(year) {
$('#swh-visits-list').children().remove();
- let visitsByYear = [];
+ const visitsByYear = [];
for (let i = 0; i < filteredVisits.length; ++i) {
if (filteredVisits[i].date.getUTCFullYear() === year) {
visitsByYear.push(filteredVisits[i]);
}
}
let visitsCpt = 0;
- let nbVisitsByRow = 4;
+ const nbVisitsByRow = 4;
let visitsListHtml = '
';
for (let i = 0; i < visitsByYear.length; ++i) {
if (visitsCpt > 0 && visitsCpt % nbVisitsByRow === 0) {
visitsListHtml += '
';
- for (let info of saveRequestInfo) {
+ for (const info of saveRequestInfo) {
content +=
`
${info.key}
${info.value}
`;
}
content += '
';
}
$('.swh-popover').html(content);
$(event.target).popover('update');
}
export function fillSaveRequestFormAndScroll(visitType, originUrl) {
$('#swh-input-origin-url').val(originUrl);
let originTypeFound = false;
$('#swh-input-visit-type option').each(function() {
- let val = $(this).val();
+ const val = $(this).val();
if (val && originUrl.includes(val)) {
$(this).prop('selected', true);
originTypeFound = true;
}
});
if (!originTypeFound) {
$('#swh-input-visit-type option').each(function() {
- let val = $(this).val();
+ const val = $(this).val();
if (val === visitType) {
$(this).prop('selected', true);
}
});
}
window.scrollTo(0, 0);
}
diff --git a/assets/src/bundles/vault/vault-create-tasks.js b/assets/src/bundles/vault/vault-create-tasks.js
index c043a90e..e1aa4062 100644
--- a/assets/src/bundles/vault/vault-create-tasks.js
+++ b/assets/src/bundles/vault/vault-create-tasks.js
@@ -1,155 +1,155 @@
/**
* 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 {handleFetchError, csrfPost, htmlAlert} from 'utils/functions';
const alertStyle = {
'position': 'fixed',
'left': '1rem',
'bottom': '1rem',
'z-index': '100000'
};
export async function vaultRequest(objectType, objectId) {
let vaultUrl;
if (objectType === 'directory') {
vaultUrl = Urls.api_1_vault_cook_directory(objectId);
} else {
vaultUrl = Urls.api_1_vault_cook_revision_gitfast(objectId);
}
// check if object has already been cooked
const response = await fetch(vaultUrl);
const data = await response.json();
// object needs to be cooked
if (data.exception === 'NotFoundExc' || data.status === 'failed') {
// if last cooking has failed, remove previous task info from localStorage
// in order to force the recooking of the object
swh.vault.removeCookingTaskInfo([objectId]);
$(`#vault-cook-${objectType}-modal`).modal('show');
// object has been cooked and should be in the vault cache,
// it will be asked to cook it again if it is not
} else if (data.status === 'done') {
$(`#vault-fetch-${objectType}-modal`).modal('show');
} else {
const cookingServiceDownAlert =
$(htmlAlert('danger',
'Archive cooking service is currently experiencing issues. ' +
'Please try again later.',
true));
cookingServiceDownAlert.css(alertStyle);
$('body').append(cookingServiceDownAlert);
}
}
async function addVaultCookingTask(cookingTask) {
const swhidsContext = swh.webapp.getSwhIdsContext();
cookingTask.origin = swhidsContext[cookingTask.object_type].context.origin;
cookingTask.path = swhidsContext[cookingTask.object_type].context.path;
cookingTask.browse_url = swhidsContext[cookingTask.object_type].swhid_with_context_url;
if (!cookingTask.browse_url) {
cookingTask.browse_url = swhidsContext[cookingTask.object_type].swhid_url;
}
let vaultCookingTasks = JSON.parse(localStorage.getItem('swh-vault-cooking-tasks'));
if (!vaultCookingTasks) {
vaultCookingTasks = [];
}
if (vaultCookingTasks.find(val => {
return val.object_type === cookingTask.object_type &&
val.object_id === cookingTask.object_id;
}) === undefined) {
let cookingUrl;
if (cookingTask.object_type === 'directory') {
cookingUrl = Urls.api_1_vault_cook_directory(cookingTask.object_id);
} else {
cookingUrl = Urls.api_1_vault_cook_revision_gitfast(cookingTask.object_id);
}
if (cookingTask.email) {
cookingUrl += '?email=' + cookingTask.email;
}
try {
const response = await csrfPost(cookingUrl);
handleFetchError(response);
vaultCookingTasks.push(cookingTask);
localStorage.setItem('swh-vault-cooking-tasks', JSON.stringify(vaultCookingTasks));
$('#vault-cook-directory-modal').modal('hide');
$('#vault-cook-revision-modal').modal('hide');
const cookingTaskCreatedAlert =
$(htmlAlert('success',
'Archive cooking request successfully submitted. ' +
`Go to the Downloads page ` +
'to get the download link once it is ready.',
true));
cookingTaskCreatedAlert.css(alertStyle);
$('body').append(cookingTaskCreatedAlert);
} catch (_) {
$('#vault-cook-directory-modal').modal('hide');
$('#vault-cook-revision-modal').modal('hide');
const cookingTaskFailedAlert =
$(htmlAlert('danger',
'Archive cooking request submission failed.',
true));
cookingTaskFailedAlert.css(alertStyle);
$('body').append(cookingTaskFailedAlert);
}
}
}
function validateEmail(email) {
- let re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
+ const re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return re.test(String(email).toLowerCase());
}
export function cookDirectoryArchive(directoryId) {
- let email = $('#swh-vault-directory-email').val().trim();
+ const email = $('#swh-vault-directory-email').val().trim();
if (!email || validateEmail(email)) {
- let cookingTask = {
+ const cookingTask = {
'object_type': 'directory',
'object_id': directoryId,
'email': email,
'status': 'new'
};
addVaultCookingTask(cookingTask);
} else {
$('#invalid-email-modal').modal('show');
}
}
export async function fetchDirectoryArchive(directoryId) {
$('#vault-fetch-directory-modal').modal('hide');
const vaultUrl = Urls.api_1_vault_cook_directory(directoryId);
const response = await fetch(vaultUrl);
const data = await response.json();
swh.vault.fetchCookedObject(data.fetch_url);
}
export function cookRevisionArchive(revisionId) {
- let email = $('#swh-vault-revision-email').val().trim();
+ const email = $('#swh-vault-revision-email').val().trim();
if (!email || validateEmail(email)) {
- let cookingTask = {
+ const cookingTask = {
'object_type': 'revision',
'object_id': revisionId,
'email': email,
'status': 'new'
};
addVaultCookingTask(cookingTask);
} else {
$('#invalid-email-modal').modal('show');
}
}
export async function fetchRevisionArchive(revisionId) {
$('#vault-fetch-directory-modal').modal('hide');
const vaultUrl = Urls.api_1_vault_cook_revision_gitfast(revisionId);
const response = await fetch(vaultUrl);
const data = await response.json();
swh.vault.fetchCookedObject(data.fetch_url);
}
diff --git a/assets/src/bundles/vault/vault-ui.js b/assets/src/bundles/vault/vault-ui.js
index 22ca093c..9cd669c7 100644
--- a/assets/src/bundles/vault/vault-ui.js
+++ b/assets/src/bundles/vault/vault-ui.js
@@ -1,241 +1,241 @@
/**
* 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 {handleFetchError, handleFetchErrors, csrfPost} from 'utils/functions';
import vaultTableRowTemplate from './vault-table-row.ejs';
-let progress =
+const progress =
`
;`;
-let pollingInterval = 5000;
+const pollingInterval = 5000;
let checkVaultId;
function updateProgressBar(progressBar, cookingTask) {
if (cookingTask.status === 'new') {
progressBar.css('background-color', 'rgba(128, 128, 128, 0.5)');
} else if (cookingTask.status === 'pending') {
progressBar.css('background-color', 'rgba(0, 0, 255, 0.5)');
} else if (cookingTask.status === 'done') {
progressBar.css('background-color', '#5cb85c');
} else if (cookingTask.status === 'failed') {
progressBar.css('background-color', 'rgba(255, 0, 0, 0.5)');
progressBar.css('background-image', 'none');
}
progressBar.text(cookingTask.progress_message || cookingTask.status);
if (cookingTask.status === 'new' || cookingTask.status === 'pending') {
progressBar.addClass('progress-bar-animated');
} else {
progressBar.removeClass('progress-bar-striped');
}
}
let recookTask;
// called when the user wants to download a cooked archive
export async function fetchCookedObject(fetchUrl) {
recookTask = null;
// first, check if the link is still available from the vault
const response = await fetch(fetchUrl);
// link is still alive, proceed to download
if (response.ok) {
$('#vault-fetch-iframe').attr('src', fetchUrl);
// link is dead
} else {
// get the associated cooking task
- let vaultCookingTasks = JSON.parse(localStorage.getItem('swh-vault-cooking-tasks'));
+ const vaultCookingTasks = JSON.parse(localStorage.getItem('swh-vault-cooking-tasks'));
for (let i = 0; i < vaultCookingTasks.length; ++i) {
if (vaultCookingTasks[i].fetch_url === fetchUrl) {
recookTask = vaultCookingTasks[i];
break;
}
}
// display a modal asking the user if he wants to recook the archive
$('#vault-recook-object-modal').modal('show');
}
}
// called when the user wants to recook an archive
// for which the download link is not available anymore
export async function recookObject() {
if (recookTask) {
// stop cooking tasks status polling
clearTimeout(checkVaultId);
// build cook request url
let cookingUrl;
if (recookTask.object_type === 'directory') {
cookingUrl = Urls.api_1_vault_cook_directory(recookTask.object_id);
} else {
cookingUrl = Urls.api_1_vault_cook_revision_gitfast(recookTask.object_id);
}
if (recookTask.email) {
cookingUrl += '?email=' + recookTask.email;
}
try {
// request archive cooking
const response = await csrfPost(cookingUrl);
handleFetchError(response);
// update task status
recookTask.status = 'new';
- let vaultCookingTasks = JSON.parse(localStorage.getItem('swh-vault-cooking-tasks'));
+ const vaultCookingTasks = JSON.parse(localStorage.getItem('swh-vault-cooking-tasks'));
for (let i = 0; i < vaultCookingTasks.length; ++i) {
if (vaultCookingTasks[i].object_id === recookTask.object_id) {
vaultCookingTasks[i] = recookTask;
break;
}
}
// save updated tasks to local storage
localStorage.setItem('swh-vault-cooking-tasks', JSON.stringify(vaultCookingTasks));
// restart cooking tasks status polling
checkVaultCookingTasks();
// hide recook archive modal
$('#vault-recook-object-modal').modal('hide');
} catch (_) {
// something went wrong
checkVaultCookingTasks();
$('#vault-recook-object-modal').modal('hide');
}
}
}
async function checkVaultCookingTasks() {
- let vaultCookingTasks = JSON.parse(localStorage.getItem('swh-vault-cooking-tasks'));
+ const vaultCookingTasks = JSON.parse(localStorage.getItem('swh-vault-cooking-tasks'));
if (!vaultCookingTasks || vaultCookingTasks.length === 0) {
$('.swh-vault-table tbody tr').remove();
checkVaultId = setTimeout(checkVaultCookingTasks, pollingInterval);
return;
}
- let cookingTaskRequests = [];
- let tasks = {};
- let currentObjectIds = [];
+ const cookingTaskRequests = [];
+ const tasks = {};
+ const currentObjectIds = [];
for (let i = 0; i < vaultCookingTasks.length; ++i) {
- let cookingTask = vaultCookingTasks[i];
+ const cookingTask = vaultCookingTasks[i];
currentObjectIds.push(cookingTask.object_id);
tasks[cookingTask.object_id] = cookingTask;
let cookingUrl;
if (cookingTask.object_type === 'directory') {
cookingUrl = Urls.api_1_vault_cook_directory(cookingTask.object_id);
} else {
cookingUrl = Urls.api_1_vault_cook_revision_gitfast(cookingTask.object_id);
}
if (cookingTask.status !== 'done' && cookingTask.status !== 'failed') {
cookingTaskRequests.push(fetch(cookingUrl));
}
}
$('.swh-vault-table tbody tr').each((i, row) => {
- let objectId = $(row).find('.vault-object-info').data('object-id');
+ const objectId = $(row).find('.vault-object-info').data('object-id');
if ($.inArray(objectId, currentObjectIds) === -1) {
$(row).remove();
}
});
try {
const responses = await Promise.all(cookingTaskRequests);
handleFetchErrors(responses);
const cookingTasks = await Promise.all(responses.map(r => r.json()));
- let table = $('#vault-cooking-tasks tbody');
+ const table = $('#vault-cooking-tasks tbody');
for (let i = 0; i < cookingTasks.length; ++i) {
- let cookingTask = tasks[cookingTasks[i].obj_id];
+ const cookingTask = tasks[cookingTasks[i].obj_id];
cookingTask.status = cookingTasks[i].status;
cookingTask.fetch_url = cookingTasks[i].fetch_url;
cookingTask.progress_message = cookingTasks[i].progress_message;
}
for (let i = 0; i < vaultCookingTasks.length; ++i) {
- let cookingTask = vaultCookingTasks[i];
- let rowTask = $(`#vault-task-${cookingTask.object_id}`);
+ const cookingTask = vaultCookingTasks[i];
+ const rowTask = $(`#vault-task-${cookingTask.object_id}`);
if (!rowTask.length) {
let browseUrl = cookingTask.browse_url;
if (!browseUrl) {
if (cookingTask.object_type === 'directory') {
browseUrl = Urls.browse_directory(cookingTask.object_id);
} else {
browseUrl = Urls.browse_revision(cookingTask.object_id);
}
}
- let progressBar = $.parseHTML(progress)[0];
- let progressBarContent = $(progressBar).find('.progress-bar');
+ const progressBar = $.parseHTML(progress)[0];
+ const progressBarContent = $(progressBar).find('.progress-bar');
updateProgressBar(progressBarContent, cookingTask);
table.prepend(vaultTableRowTemplate({
browseUrl: browseUrl,
cookingTask: cookingTask,
progressBar: progressBar,
Urls: Urls,
swh: swh
}));
} else {
- let progressBar = rowTask.find('.progress-bar');
+ const progressBar = rowTask.find('.progress-bar');
updateProgressBar(progressBar, cookingTask);
- let downloadLink = rowTask.find('.vault-dl-link');
+ const downloadLink = rowTask.find('.vault-dl-link');
if (cookingTask.status === 'done') {
downloadLink[0].innerHTML =
'';
} else {
downloadLink[0].innerHTML = '';
}
}
}
localStorage.setItem('swh-vault-cooking-tasks', JSON.stringify(vaultCookingTasks));
checkVaultId = setTimeout(checkVaultCookingTasks, pollingInterval);
} catch (error) {
console.log('Error when fetching vault cooking tasks:', error);
}
}
export function removeCookingTaskInfo(tasksToRemove) {
let vaultCookingTasks = JSON.parse(localStorage.getItem('swh-vault-cooking-tasks'));
if (!vaultCookingTasks) {
return;
}
vaultCookingTasks = $.grep(vaultCookingTasks, task => {
return $.inArray(task.object_id, tasksToRemove) === -1;
});
localStorage.setItem('swh-vault-cooking-tasks', JSON.stringify(vaultCookingTasks));
}
export function initUi() {
$('#vault-tasks-toggle-selection').change(event => {
$('.vault-task-toggle-selection').prop('checked', event.currentTarget.checked);
});
$('#vault-remove-tasks').click(() => {
clearTimeout(checkVaultId);
- let tasksToRemove = [];
+ const tasksToRemove = [];
$('.swh-vault-table tbody tr').each((i, row) => {
- let taskSelected = $(row).find('.vault-task-toggle-selection').prop('checked');
+ const taskSelected = $(row).find('.vault-task-toggle-selection').prop('checked');
if (taskSelected) {
- let objectId = $(row).find('.vault-object-info').data('object-id');
+ const objectId = $(row).find('.vault-object-info').data('object-id');
tasksToRemove.push(objectId);
$(row).remove();
}
});
removeCookingTaskInfo(tasksToRemove);
$('#vault-tasks-toggle-selection').prop('checked', false);
checkVaultId = setTimeout(checkVaultCookingTasks, pollingInterval);
});
checkVaultCookingTasks();
window.onfocus = () => {
clearTimeout(checkVaultId);
checkVaultCookingTasks();
};
}
diff --git a/assets/src/bundles/webapp/code-highlighting.js b/assets/src/bundles/webapp/code-highlighting.js
index d9505c30..5a873869 100644
--- a/assets/src/bundles/webapp/code-highlighting.js
+++ b/assets/src/bundles/webapp/code-highlighting.js
@@ -1,114 +1,114 @@
/**
* 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 {removeUrlFragment} from 'utils/functions';
export async function highlightCode(showLineNumbers = true) {
await import(/* webpackChunkName: "highlightjs" */ 'utils/highlightjs');
// keep track of the first highlighted line
let firstHighlightedLine = null;
// highlighting color
- let lineHighlightColor = 'rgb(193, 255, 193)';
+ const lineHighlightColor = 'rgb(193, 255, 193)';
// function to highlight a line
function highlightLine(i) {
- let lineTd = $(`.hljs-ln-line[data-line-number="${i}"]`);
+ const lineTd = $(`.hljs-ln-line[data-line-number="${i}"]`);
lineTd.css('background-color', lineHighlightColor);
return lineTd;
}
// function to reset highlighting
function resetHighlightedLines() {
firstHighlightedLine = null;
$('.hljs-ln-line[data-line-number]').css('background-color', 'inherit');
}
function scrollToLine(lineDomElt) {
if ($(lineDomElt).closest('.swh-content').length > 0) {
$('html, body').animate({
scrollTop: $(lineDomElt).offset().top - 70
}, 500);
}
}
// function to highlight lines based on a url fragment
// in the form '#Lx' or '#Lx-Ly'
function parseUrlFragmentForLinesToHighlight() {
- let lines = [];
- let linesRegexp = new RegExp(/L(\d+)/g);
+ const lines = [];
+ const linesRegexp = new RegExp(/L(\d+)/g);
let line = linesRegexp.exec(window.location.hash);
if (line === null) {
return;
}
while (line) {
lines.push(parseInt(line[1]));
line = linesRegexp.exec(window.location.hash);
}
resetHighlightedLines();
if (lines.length === 1) {
firstHighlightedLine = parseInt(lines[0]);
scrollToLine(highlightLine(lines[0]));
} else if (lines[0] < lines[lines.length - 1]) {
firstHighlightedLine = parseInt(lines[0]);
scrollToLine(highlightLine(lines[0]));
for (let i = lines[0] + 1; i <= lines[lines.length - 1]; ++i) {
highlightLine(i);
}
}
}
$(document).ready(() => {
// highlight code and add line numbers
$('code').each((i, elt) => {
hljs.highlightElement(elt);
if (showLineNumbers) {
hljs.lineNumbersElement(elt, {singleLine: true});
}
});
if (!showLineNumbers) {
return;
}
// click handler to dynamically highlight line(s)
// when the user clicks on a line number (lines range
// can also be highlighted while holding the shift key)
$('.swh-content').click(evt => {
if (evt.target.classList.contains('hljs-ln-n')) {
- let line = parseInt($(evt.target).data('line-number'));
+ const line = parseInt($(evt.target).data('line-number'));
if (evt.shiftKey && firstHighlightedLine && line > firstHighlightedLine) {
- let firstLine = firstHighlightedLine;
+ const firstLine = firstHighlightedLine;
resetHighlightedLines();
for (let i = firstLine; i <= line; ++i) {
highlightLine(i);
}
firstHighlightedLine = firstLine;
window.location.hash = `#L${firstLine}-L${line}`;
} else {
resetHighlightedLines();
highlightLine(line);
window.location.hash = `#L${line}`;
scrollToLine(evt.target);
}
} else if ($(evt.target).closest('.hljs-ln').length) {
resetHighlightedLines();
removeUrlFragment();
}
});
// update lines highlighting when the url fragment changes
$(window).on('hashchange', () => parseUrlFragmentForLinesToHighlight());
// schedule lines highlighting if any as hljs.lineNumbersElement() is async
setTimeout(() => {
parseUrlFragmentForLinesToHighlight();
});
});
}
diff --git a/assets/src/bundles/webapp/notebook-rendering.js b/assets/src/bundles/webapp/notebook-rendering.js
index 6bc1d3d1..73578002 100644
--- a/assets/src/bundles/webapp/notebook-rendering.js
+++ b/assets/src/bundles/webapp/notebook-rendering.js
@@ -1,136 +1,136 @@
/**
* Copyright (C) 2019-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
*/
import 'script-loader!notebookjs';
import AnsiUp from 'ansi_up';
import './notebook.css';
const ansiup = new AnsiUp();
ansiup.escape_for_html = false;
function escapeHTML(text) {
text = text.replace(//g, '>');
return text;
}
function unescapeHTML(text) {
text = text.replace(/</g, '<');
text = text.replace(/>/g, '>');
return text;
}
function escapeLaTeX(text) {
- let blockMath = /\$\$(.+?)\$\$|\\\\\[(.+?)\\\\\]/msg;
- let inlineMath = /\$(.+?)\$|\\\\\((.+?)\\\\\)/g;
- let latexEnvironment = /\\begin\{([a-z]*\*?)\}(.+?)\\end\{\1\}/msg;
+ const blockMath = /\$\$(.+?)\$\$|\\\\\[(.+?)\\\\\]/msg;
+ const inlineMath = /\$(.+?)\$|\\\\\((.+?)\\\\\)/g;
+ const latexEnvironment = /\\begin\{([a-z]*\*?)\}(.+?)\\end\{\1\}/msg;
- let mathTextFound = [];
+ const mathTextFound = [];
let bm;
while ((bm = blockMath.exec(text)) !== null) {
mathTextFound.push(bm[1]);
}
let im;
while ((im = inlineMath.exec(text)) !== null) {
mathTextFound.push(im[1]);
}
let le;
while ((le = latexEnvironment.exec(text)) !== null) {
mathTextFound.push(le[1]);
}
- for (let mathText of mathTextFound) {
+ for (const mathText of mathTextFound) {
// showdown will remove line breaks in LaTex array and
// some escaping sequences when converting md to html.
// So we use the following escaping hacks to keep them in the html
// output and avoid MathJax typesetting errors.
let escapedText = mathText.replace('\\\\', '\\\\\\\\');
- for (let specialLaTexChar of ['{', '}', '#', '%', '&', '_']) {
+ for (const specialLaTexChar of ['{', '}', '#', '%', '&', '_']) {
escapedText = escapedText.replace(new RegExp(`\\\\${specialLaTexChar}`, 'g'),
`\\\\${specialLaTexChar}`);
}
// some html escaping is also needed
escapedText = escapeHTML(escapedText);
// hack to prevent showdown to replace _ characters
// by html em tags as it will break some math typesetting
// (setting the literalMidWordUnderscores option is not
// enough as iy only works for _ characters contained in words)
escapedText = escapedText.replace(/_/g, '{@}underscore{@}');
if (mathText !== escapedText) {
text = text.replace(mathText, escapedText);
}
}
return text;
}
export async function renderNotebook(nbJsonUrl, domElt) {
- let showdown = await import(/* webpackChunkName: "showdown" */ 'utils/showdown');
+ const showdown = await import(/* webpackChunkName: "showdown" */ 'utils/showdown');
await import(/* webpackChunkName: "highlightjs" */ 'utils/highlightjs');
function renderMarkdown(text) {
- let converter = new showdown.Converter({
+ const converter = new showdown.Converter({
tables: true,
simplifiedAutoLink: true,
rawHeaderId: true,
literalMidWordUnderscores: true
});
// some LaTeX escaping is required to get correct math typesetting
text = escapeLaTeX(text);
// render markdown
let rendered = converter.makeHtml(text);
// restore underscores in rendered HTML (see escapeLaTeX function)
rendered = rendered.replace(/{@}underscore{@}/g, '_');
return rendered;
}
function highlightCode(text, preElt, codeElt, lang) {
// no need to unescape text processed by ansiup
if (text.indexOf(' {
$('#pdf-prev').click(onPrevPage);
$('#pdf-next').click(onNextPage);
try {
const pdf = await pdfjs.getDocument(pdfUrl).promise;
pdfDoc = pdf;
$('#pdf-page-count').text(pdfDoc.numPages);
// Initial/first page rendering
renderPage(pageNum);
} catch (reason) {
// PDF loading error
console.error(reason);
}
// Render PDF on resize
$(window).on('resize', function() {
queueRenderPage(pageNum);
});
});
}
diff --git a/assets/src/bundles/webapp/readme-rendering.js b/assets/src/bundles/webapp/readme-rendering.js
index 2651f6c7..3cb56fae 100644
--- a/assets/src/bundles/webapp/readme-rendering.js
+++ b/assets/src/bundles/webapp/readme-rendering.js
@@ -1,118 +1,118 @@
/**
* 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} from 'utils/functions';
import {decode} from 'html-encoder-decoder';
export async function renderMarkdown(domElt, markdownDocUrl) {
- let showdown = await import(/* webpackChunkName: "showdown" */ 'utils/showdown');
+ const showdown = await import(/* webpackChunkName: "showdown" */ 'utils/showdown');
await import(/* webpackChunkName: "highlightjs" */ 'utils/highlightjs');
// Adapted from https://github.com/Bloggify/showdown-highlight
// Copyright (c) 2016-19 Bloggify (https://bloggify.org)
function showdownHighlight() {
return [{
type: 'output',
filter: function(text, converter, options) {
- let left = '
]*>';
- let right = '
';
- let flags = 'g';
- let classAttr = 'class="';
- let replacement = (wholeMatch, match, left, right) => {
+ const left = '
]*>';
+ const right = '
';
+ const flags = 'g';
+ const classAttr = 'class="';
+ const replacement = (wholeMatch, match, left, right) => {
match = decode(match);
- let lang = (left.match(/class="([^ "]+)/) || [])[1];
+ const lang = (left.match(/class="([^ "]+)/) || [])[1];
if (left.includes(classAttr)) {
- let attrIndex = left.indexOf(classAttr) + classAttr.length;
+ const attrIndex = left.indexOf(classAttr) + classAttr.length;
left = left.slice(0, attrIndex) + 'hljs ' + left.slice(attrIndex);
} else {
left = left.slice(0, -1) + ' class="hljs">';
}
if (lang && hljs.getLanguage(lang)) {
return left + hljs.highlight(match, {language: lang}).value + right;
} else {
return left + match + right;
}
};
return showdown.helper.replaceRecursiveRegExp(text, replacement, left, right, flags);
}
}];
}
$(document).ready(async() => {
- let converter = new showdown.Converter({
+ const converter = new showdown.Converter({
tables: true,
extensions: [showdownHighlight]
});
try {
const response = await fetch(markdownDocUrl);
handleFetchError(response);
const data = await response.text();
$(domElt).addClass('swh-showdown');
$(domElt).html(swh.webapp.filterXSS(converter.makeHtml(data)));
} catch (_) {
$(domElt).text('Readme bytes are not available');
}
});
}
export async function renderOrgData(domElt, orgDocData) {
- let org = await import(/* webpackChunkName: "org" */ 'utils/org');
+ const org = await import(/* webpackChunkName: "org" */ 'utils/org');
- let parser = new org.Parser();
- let orgDocument = parser.parse(orgDocData, {toc: false});
- let orgHTMLDocument = orgDocument.convert(org.ConverterHTML, {});
+ const parser = new org.Parser();
+ const orgDocument = parser.parse(orgDocData, {toc: false});
+ const orgHTMLDocument = orgDocument.convert(org.ConverterHTML, {});
$(domElt).addClass('swh-org');
$(domElt).html(swh.webapp.filterXSS(orgHTMLDocument.toString()));
// remove toc and section numbers to get consistent
// with other readme renderings
$('.swh-org ul').first().remove();
$('.section-number').remove();
}
export function renderOrg(domElt, orgDocUrl) {
$(document).ready(async() => {
try {
const response = await fetch(orgDocUrl);
handleFetchError(response);
const data = await response.text();
renderOrgData(domElt, data);
} catch (_) {
$(domElt).text('Readme bytes are not available');
}
});
}
export function renderTxt(domElt, txtDocUrl) {
$(document).ready(async() => {
try {
const response = await fetch(txtDocUrl);
handleFetchError(response);
const data = await response.text();
- let orgMode = '-*- mode: org -*-';
+ const orgMode = '-*- mode: org -*-';
if (data.indexOf(orgMode) !== -1) {
renderOrgData(domElt, data.replace(orgMode, ''));
} else {
$(domElt).addClass('swh-readme-txt');
$(domElt)
.html('')
.append($('').text(data));
}
} catch (_) {
$(domElt).text('Readme bytes are not available');
}
});
}
diff --git a/assets/src/bundles/webapp/status-widget.js b/assets/src/bundles/webapp/status-widget.js
index 7eb31b3b..f78c72dd 100644
--- a/assets/src/bundles/webapp/status-widget.js
+++ b/assets/src/bundles/webapp/status-widget.js
@@ -1,50 +1,50 @@
/**
* Copyright (C) 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
*/
import './status-widget.css';
const statusCodeColor = {
'100': 'green', // Operational
'200': 'blue', // Scheduled Maintenance
'300': 'yellow', // Degraded Performance
'400': 'yellow', // Partial Service Disruption
'500': 'red', // Service Disruption
'600': 'red' // Security Event
};
export function initStatusWidget(statusDataURL) {
$('.swh-current-status-indicator').ready(async() => {
let maxStatusCode = '';
let maxStatusDescription = '';
let sc = '';
let sd = '';
try {
const response = await fetch(statusDataURL);
const data = await response.json();
- for (let s of data.result.status) {
+ for (const s of data.result.status) {
sc = s.status_code;
sd = s.status;
if (maxStatusCode < sc) {
maxStatusCode = sc;
maxStatusDescription = sd;
}
}
if (maxStatusCode === '') {
$('.swh-current-status').remove();
return;
}
$('.swh-current-status-indicator').removeClass('green');
$('.swh-current-status-indicator').addClass(statusCodeColor[maxStatusCode]);
$('#swh-current-status-description').text(maxStatusDescription);
} catch (e) {
console.log(e);
$('.swh-current-status').remove();
}
});
}
diff --git a/assets/src/bundles/webapp/webapp-utils.js b/assets/src/bundles/webapp/webapp-utils.js
index 797a2469..79a65270 100644
--- a/assets/src/bundles/webapp/webapp-utils.js
+++ b/assets/src/bundles/webapp/webapp-utils.js
@@ -1,400 +1,400 @@
/**
* 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 objectFitImages from 'object-fit-images';
import {selectText} from 'utils/functions';
import {BREAKPOINT_MD} from 'utils/constants';
let collapseSidebar = false;
-let previousSidebarState = localStorage.getItem('remember.lte.pushmenu');
+const previousSidebarState = localStorage.getItem('remember.lte.pushmenu');
if (previousSidebarState !== undefined) {
collapseSidebar = previousSidebarState === 'sidebar-collapse';
}
$(document).on('DOMContentLoaded', () => {
// set state to collapsed on smaller devices
if ($(window).width() < BREAKPOINT_MD) {
collapseSidebar = true;
}
// restore previous sidebar state (collapsed/expanded)
if (collapseSidebar) {
// hack to avoid animated transition for collapsing sidebar
// when loading a page
- let sidebarTransition = $('.main-sidebar, .main-sidebar:before').css('transition');
- let sidebarEltsTransition = $('.sidebar .nav-link p, .main-sidebar .brand-text, .sidebar .user-panel .info').css('transition');
+ const sidebarTransition = $('.main-sidebar, .main-sidebar:before').css('transition');
+ const sidebarEltsTransition = $('.sidebar .nav-link p, .main-sidebar .brand-text, .sidebar .user-panel .info').css('transition');
$('.main-sidebar, .main-sidebar:before').css('transition', 'none');
$('.sidebar .nav-link p, .main-sidebar .brand-text, .sidebar .user-panel .info').css('transition', 'none');
$('body').addClass('sidebar-collapse');
$('.swh-words-logo-swh').css('visibility', 'visible');
// restore transitions for user navigation
setTimeout(() => {
$('.main-sidebar, .main-sidebar:before').css('transition', sidebarTransition);
$('.sidebar .nav-link p, .main-sidebar .brand-text, .sidebar .user-panel .info').css('transition', sidebarEltsTransition);
});
}
});
$(document).on('collapsed.lte.pushmenu', event => {
if ($('body').width() >= BREAKPOINT_MD) {
$('.swh-words-logo-swh').css('visibility', 'visible');
}
});
$(document).on('shown.lte.pushmenu', event => {
$('.swh-words-logo-swh').css('visibility', 'hidden');
});
function ensureNoFooterOverflow() {
$('body').css('padding-bottom', $('footer').outerHeight() + 'px');
}
$(document).ready(() => {
// redirect to last browse page if any when clicking on the 'Browse' entry
// in the sidebar
$(`.swh-browse-link`).click(event => {
- let lastBrowsePage = sessionStorage.getItem('last-browse-page');
+ const lastBrowsePage = sessionStorage.getItem('last-browse-page');
if (lastBrowsePage) {
event.preventDefault();
window.location = lastBrowsePage;
}
});
const mainSideBar = $('.main-sidebar');
function updateSidebarState() {
const body = $('body');
if (body.hasClass('sidebar-collapse') &&
!mainSideBar.hasClass('swh-sidebar-collapsed')) {
mainSideBar.removeClass('swh-sidebar-expanded');
mainSideBar.addClass('swh-sidebar-collapsed');
$('.swh-words-logo-swh').css('visibility', 'visible');
} else if (!body.hasClass('sidebar-collapse') &&
!mainSideBar.hasClass('swh-sidebar-expanded')) {
mainSideBar.removeClass('swh-sidebar-collapsed');
mainSideBar.addClass('swh-sidebar-expanded');
$('.swh-words-logo-swh').css('visibility', 'hidden');
}
// ensure correct sidebar state when loading a page
if (body.hasClass('hold-transition')) {
setTimeout(() => {
updateSidebarState();
});
}
}
// set sidebar state after collapse / expand animation
mainSideBar.on('transitionend', evt => {
updateSidebarState();
});
updateSidebarState();
// ensure footer do not overflow main content for mobile devices
// or after resizing the browser window
ensureNoFooterOverflow();
$(window).resize(function() {
ensureNoFooterOverflow();
if ($('body').hasClass('sidebar-collapse') && $('body').width() >= BREAKPOINT_MD) {
$('.swh-words-logo-swh').css('visibility', 'visible');
}
});
// activate css polyfill 'object-fit: contain' in old browsers
objectFitImages();
// reparent the modals to the top navigation div in order to be able
// to display them
$('.swh-browse-top-navigation').append($('.modal'));
let selectedCode = null;
function getCodeOrPreEltUnderPointer(e) {
- let elts = document.elementsFromPoint(e.clientX, e.clientY);
- for (let elt of elts) {
+ const elts = document.elementsFromPoint(e.clientX, e.clientY);
+ for (const elt of elts) {
if (elt.nodeName === 'CODE' || elt.nodeName === 'PRE') {
return elt;
}
}
return null;
}
// click handler to set focus on code block for copy
$(document).click(e => {
selectedCode = getCodeOrPreEltUnderPointer(e);
});
function selectCode(event, selectedCode) {
if (selectedCode) {
- let hljsLnCodeElts = $(selectedCode).find('.hljs-ln-code');
+ const hljsLnCodeElts = $(selectedCode).find('.hljs-ln-code');
if (hljsLnCodeElts.length) {
selectText(hljsLnCodeElts[0], hljsLnCodeElts[hljsLnCodeElts.length - 1]);
} else {
selectText(selectedCode.firstChild, selectedCode.lastChild);
}
event.preventDefault();
}
}
// select the whole text of focused code block when user
// double clicks or hits Ctrl+A
$(document).dblclick(e => {
if ((e.ctrlKey || e.metaKey)) {
selectCode(e, getCodeOrPreEltUnderPointer(e));
}
});
$(document).keydown(e => {
if ((e.ctrlKey || e.metaKey) && e.key === 'a') {
selectCode(e, selectedCode);
}
});
// show/hide back-to-top button
let scrollThreshold = 0;
scrollThreshold += $('.swh-top-bar').height() || 0;
scrollThreshold += $('.navbar').height() || 0;
$(window).scroll(() => {
if ($(window).scrollTop() > scrollThreshold) {
$('#back-to-top').css('display', 'block');
} else {
$('#back-to-top').css('display', 'none');
}
});
// navbar search form submission callback
$('#swh-origins-search-top').submit(event => {
event.preventDefault();
if (event.target.checkValidity()) {
$(event.target).removeClass('was-validated');
- let searchQueryText = $('#swh-origins-search-top-input').val().trim();
- let queryParameters = new URLSearchParams();
+ const searchQueryText = $('#swh-origins-search-top-input').val().trim();
+ const queryParameters = new URLSearchParams();
queryParameters.append('q', searchQueryText);
queryParameters.append('with_visit', true);
queryParameters.append('with_content', true);
window.location = `${Urls.browse_search()}?${queryParameters.toString()}`;
} else {
$(event.target).addClass('was-validated');
}
});
});
export function initPage(page) {
$(document).ready(() => {
// set relevant sidebar link to page active
$(`.swh-${page}-item`).addClass('active');
$(`.swh-${page}-link`).addClass('active');
// triggered when unloading the current page
$(window).on('unload', () => {
// backup current browse page
if (page === 'browse') {
sessionStorage.setItem('last-browse-page', window.location);
}
});
});
}
export function initHomePage() {
$(document).ready(async() => {
$('.swh-coverage-list').iFrameResize({heightCalculationMethod: 'taggedElement'});
const response = await fetch(Urls.stat_counters());
const data = await response.json();
if (data.stat_counters && !$.isEmptyObject(data.stat_counters)) {
- for (let objectType of ['content', 'revision', 'origin', 'directory', 'person', 'release']) {
+ for (const objectType of ['content', 'revision', 'origin', 'directory', 'person', 'release']) {
const count = data.stat_counters[objectType];
if (count !== undefined) {
$(`#swh-${objectType}-count`).html(count.toLocaleString());
} else {
$(`#swh-${objectType}-count`).closest('.swh-counter-container').hide();
}
}
} else {
$('.swh-counter').html('0');
}
if (data.stat_counters_history && !$.isEmptyObject(data.stat_counters_history)) {
- for (let objectType of ['content', 'revision', 'origin']) {
+ for (const objectType of ['content', 'revision', 'origin']) {
const history = data.stat_counters_history[objectType];
if (history) {
swh.webapp.drawHistoryCounterGraph(`#swh-${objectType}-count-history`, history);
} else {
$(`#swh-${objectType}-count-history`).hide();
}
}
} else {
$('.swh-counter-history').hide();
}
});
initPage('home');
}
export function showModalMessage(title, message) {
$('#swh-web-modal-message .modal-title').text(title);
$('#swh-web-modal-message .modal-content p').text(message);
$('#swh-web-modal-message').modal('show');
}
export function showModalConfirm(title, message, callback) {
$('#swh-web-modal-confirm .modal-title').text(title);
$('#swh-web-modal-confirm .modal-content p').text(message);
$('#swh-web-modal-confirm #swh-web-modal-confirm-ok-btn').bind('click', () => {
callback();
$('#swh-web-modal-confirm').modal('hide');
$('#swh-web-modal-confirm #swh-web-modal-confirm-ok-btn').unbind('click');
});
$('#swh-web-modal-confirm').modal('show');
}
export function showModalHtml(title, html) {
$('#swh-web-modal-html .modal-title').text(title);
$('#swh-web-modal-html .modal-body').html(html);
$('#swh-web-modal-html').modal('show');
}
export function addJumpToPagePopoverToDataTable(dataTableElt) {
dataTableElt.on('draw.dt', function() {
$('.paginate_button.disabled').css('cursor', 'pointer');
$('.paginate_button.disabled').on('click', event => {
const pageInfo = dataTableElt.page.info();
let content = ' / ${pageInfo.pages}`;
$(event.target).popover({
'title': 'Jump to page',
'content': content,
'html': true,
'placement': 'top',
'sanitizeFn': swh.webapp.filterXSS
});
$(event.target).popover('show');
$('.jump-to-page').on('change', function() {
$('.paginate_button.disabled').popover('hide');
const pageNumber = parseInt($(this).val()) - 1;
dataTableElt.page(pageNumber).draw('page');
});
});
});
dataTableElt.on('preXhr.dt', () => {
$('.paginate_button.disabled').popover('hide');
});
}
let swhObjectIcons;
export function setSwhObjectIcons(icons) {
swhObjectIcons = icons;
}
export function getSwhObjectIcon(swhObjectType) {
return swhObjectIcons[swhObjectType];
}
let browsedSwhObjectMetadata = {};
export function setBrowsedSwhObjectMetadata(metadata) {
browsedSwhObjectMetadata = metadata;
}
export function getBrowsedSwhObjectMetadata() {
return browsedSwhObjectMetadata;
}
// This will contain a mapping between an archived object type
// and its related SWHID metadata for each object reachable from
// the current browse view.
// SWHID metadata contain the following keys:
// * object_type: type of archived object
// * object_id: sha1 object identifier
// * swhid: SWHID without contextual info
// * swhid_url: URL to resolve SWHID without contextual info
// * context: object describing SWHID context
// * swhid_with_context: SWHID with contextual info
// * swhid_with_context_url: URL to resolve SWHID with contextual info
let swhidsContext_ = {};
export function setSwhIdsContext(swhidsContext) {
swhidsContext_ = {};
- for (let swhidContext of swhidsContext) {
+ for (const swhidContext of swhidsContext) {
swhidsContext_[swhidContext.object_type] = swhidContext;
}
}
export function getSwhIdsContext() {
return swhidsContext_;
}
function setFullWidth(fullWidth) {
if (fullWidth) {
$('#swh-web-content').removeClass('container');
$('#swh-web-content').addClass('container-fluid');
} else {
$('#swh-web-content').removeClass('container-fluid');
$('#swh-web-content').addClass('container');
}
localStorage.setItem('swh-web-full-width', JSON.stringify(fullWidth));
$('#swh-full-width-switch').prop('checked', fullWidth);
}
export function fullWidthToggled(event) {
setFullWidth($(event.target).prop('checked'));
}
export function setContainerFullWidth() {
- let previousFullWidthState = JSON.parse(localStorage.getItem('swh-web-full-width'));
+ const previousFullWidthState = JSON.parse(localStorage.getItem('swh-web-full-width'));
if (previousFullWidthState !== null) {
setFullWidth(previousFullWidthState);
}
}
function coreSWHIDIsLowerCase(swhid) {
const qualifiersPos = swhid.indexOf(';');
let coreSWHID = swhid;
if (qualifiersPos !== -1) {
coreSWHID = swhid.slice(0, qualifiersPos);
}
return coreSWHID.toLowerCase() === coreSWHID;
}
export async function validateSWHIDInput(swhidInputElt) {
const swhidInput = swhidInputElt.value.trim();
let customValidity = '';
if (swhidInput.toLowerCase().startsWith('swh:')) {
if (coreSWHIDIsLowerCase(swhidInput)) {
const resolveSWHIDUrl = Urls.api_1_resolve_swhid(swhidInput);
const response = await fetch(resolveSWHIDUrl);
const responseData = await response.json();
if (responseData.hasOwnProperty('exception')) {
customValidity = responseData.reason;
}
} else {
const qualifiersPos = swhidInput.indexOf(';');
if (qualifiersPos === -1) {
customValidity = 'Invalid SWHID: all characters must be in lowercase. ';
customValidity += `Valid SWHID is ${swhidInput.toLowerCase()}`;
} else {
customValidity = 'Invalid SWHID: the core part must be in lowercase. ';
const coreSWHID = swhidInput.slice(0, qualifiersPos);
customValidity += `Valid SWHID is ${swhidInput.replace(coreSWHID, coreSWHID.toLowerCase())}`;
}
}
}
swhidInputElt.setCustomValidity(customValidity);
$(swhidInputElt).siblings('.invalid-feedback').text(customValidity);
}
export function isUserLoggedIn() {
return JSON.parse($('#swh_user_logged_in').text());
}
diff --git a/assets/src/bundles/webapp/xss-filtering.js b/assets/src/bundles/webapp/xss-filtering.js
index 8eb2a65c..81be4683 100644
--- a/assets/src/bundles/webapp/xss-filtering.js
+++ b/assets/src/bundles/webapp/xss-filtering.js
@@ -1,42 +1,42 @@
/**
* Copyright (C) 2019-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
*/
import DOMPurify from 'dompurify';
// we register a hook when performing XSS filtering in order to
// possibly replace a relative image url with the one for getting
// the image bytes from the archive content
DOMPurify.addHook('uponSanitizeAttribute', function(node, data) {
if (node.nodeName === 'IMG' && data.attrName === 'src') {
// image url does not need any processing here
if (data.attrValue.startsWith('data:image') ||
data.attrValue.startsWith('http:') ||
data.attrValue.startsWith('https:')) {
return;
}
// get currently browsed swh object metadata
- let swhObjectMetadata = swh.webapp.getBrowsedSwhObjectMetadata();
+ const swhObjectMetadata = swh.webapp.getBrowsedSwhObjectMetadata();
// the swh object is provided without any useful context
// to get the image checksums from the web api
if (!swhObjectMetadata.hasOwnProperty('directory')) {
return;
}
// used internal endpoint as image url to possibly get the image data
// from the archive content
let url = Urls.browse_directory_resolve_content_path(swhObjectMetadata.directory);
url += `?path=${data.attrValue}`;
data.attrValue = url;
}
});
export function filterXSS(html) {
return DOMPurify.sanitize(html);
}
diff --git a/assets/src/utils/functions.js b/assets/src/utils/functions.js
index f91856cf..938acda6 100644
--- a/assets/src/utils/functions.js
+++ b/assets/src/utils/functions.js
@@ -1,124 +1,124 @@
/**
* 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
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 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 = '/') {
- let allowedProtocols = ['http:', 'https:', 'git:'];
+ const allowedProtocols = ['http:', 'https:', 'git:'];
if (allowedProtocols.find(protocol => protocol === url.protocol) === undefined) {
return false;
}
if (!url.pathname.startsWith(pathPrefix)) {
return false;
}
- let re = new RegExp('[\\w\\.-]+\\/?(?!=.git)(?:\\.git\\/?)?$');
+ 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) {
- let selection = window.getSelection();
+ const selection = window.getSelection();
selection.removeAllRanges();
- let range = document.createRange();
+ 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 `
${message}${closeButton}
`;
}
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
}
}
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 ghApiResponse = await fetch(`https://api.github.com/repos/${ownerRepo}`);
if (ghApiResponse.ok && ghApiResponse.status === 200) {
const ghApiResponseData = await ghApiResponse.json();
return ghApiResponseData.html_url;
}
}
return originUrl;
}
diff --git a/cypress/integration/code-highlighting.spec.js b/cypress/integration/code-highlighting.spec.js
index 77470f65..9fdb2b56 100644
--- a/cypress/integration/code-highlighting.spec.js
+++ b/cypress/integration/code-highlighting.spec.js
@@ -1,92 +1,92 @@
/**
* Copyright (C) 2019-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
*/
import {random} from '../utils';
const $ = Cypress.$;
let origin;
const lineStart = 32;
const lineEnd = 42;
let url;
describe('Code highlighting tests', function() {
before(function() {
origin = this.origin[0];
url = `${this.Urls.browse_origin_content()}?origin_url=${origin.url}&path=${origin.content[0].path}`;
});
it('should highlight source code and add line numbers', function() {
cy.visit(url);
cy.get('.hljs-ln-numbers').then(lnNumbers => {
cy.get('.hljs-ln-code')
.should('have.length', lnNumbers.length);
});
});
it('should emphasize source code lines based on url fragment', function() {
cy.visit(`${url}/#L${lineStart}-L${lineEnd}`);
cy.get('.hljs-ln-line').then(lines => {
- for (let line of lines) {
+ for (const line of lines) {
const lineElt = $(line);
const lineNumber = parseInt(lineElt.data('line-number'));
if (lineNumber >= lineStart && lineNumber <= lineEnd) {
assert.notEqual(lineElt.css('background-color'), 'rgba(0, 0, 0, 0)');
} else {
assert.equal(lineElt.css('background-color'), 'rgba(0, 0, 0, 0)');
}
}
});
});
it('should emphasize a line by clicking on its number', function() {
cy.visit(url);
cy.get('.hljs-ln-numbers').then(lnNumbers => {
const lnNumber = lnNumbers[random(0, lnNumbers.length)];
const lnNumberElt = $(lnNumber);
assert.equal(lnNumberElt.css('background-color'), 'rgba(0, 0, 0, 0)');
const line = parseInt(lnNumberElt.data('line-number'));
cy.get(`.hljs-ln-numbers[data-line-number="${line}"]`)
.click()
.then(() => {
assert.notEqual(lnNumberElt.css('background-color'), 'rgba(0, 0, 0, 0)');
});
});
});
it('should emphasize a range of lines by clicking on two line numbers and holding shift', function() {
cy.visit(url);
cy.get(`.hljs-ln-numbers[data-line-number="${lineStart}"]`)
.click()
.get(`.hljs-ln-numbers[data-line-number="${lineEnd}"]`)
.click({shiftKey: true})
.get('.hljs-ln-line')
.then(lines => {
- for (let line of lines) {
+ for (const line of lines) {
const lineElt = $(line);
const lineNumber = parseInt(lineElt.data('line-number'));
if (lineNumber >= lineStart && lineNumber <= lineEnd) {
assert.notEqual(lineElt.css('background-color'), 'rgba(0, 0, 0, 0)');
} else {
assert.equal(lineElt.css('background-color'), 'rgba(0, 0, 0, 0)');
}
}
});
});
it('should remove emphasized lines when clicking anywhere in code', function() {
cy.visit(`${url}/#L${lineStart}-L${lineEnd}`);
cy.get(`.hljs-ln-code[data-line-number="1"]`)
.click()
.get('.hljs-ln-line')
.should('have.css', 'background-color', 'rgba(0, 0, 0, 0)');
});
});
diff --git a/cypress/integration/content-rendering.spec.js b/cypress/integration/content-rendering.spec.js
index fa532f02..6a693959 100644
--- a/cypress/integration/content-rendering.spec.js
+++ b/cypress/integration/content-rendering.spec.js
@@ -1,99 +1,99 @@
/**
* 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 {checkLanguageHighlighting, describeSlowTests} from '../utils';
describeSlowTests('Code highlighting tests', function() {
const extensions = require('../fixtures/source-file-extensions.json');
extensions.forEach(ext => {
it(`should highlight source files with extension ${ext}`, function() {
cy.request(this.Urls.tests_content_code_extension(ext)).then(response => {
const data = response.body;
cy.visit(`${this.Urls.browse_content(data.sha1)}?path=file.${ext}`);
checkLanguageHighlighting(data.language);
});
});
});
const filenames = require('../fixtures/source-file-names.json');
filenames.forEach(filename => {
it(`should highlight source files with filenames ${filename}`, function() {
cy.request(this.Urls.tests_content_code_filename(filename)).then(response => {
const data = response.body;
cy.visit(`${this.Urls.browse_content(data.sha1)}?path=${filename}`);
checkLanguageHighlighting(data.language);
});
});
});
});
describe('Image rendering tests', function() {
const imgExtensions = ['gif', 'jpeg', 'png', 'webp'];
imgExtensions.forEach(ext => {
it(`should render image with extension ${ext}`, function() {
cy.request(this.Urls.tests_content_other_extension(ext)).then(response => {
const data = response.body;
cy.visit(`${this.Urls.browse_content(data.sha1)}?path=file.${ext}`);
cy.get('.swh-content img')
.should('be.visible');
});
});
});
});
describe('PDF rendering test', function() {
function sum(previousValue, currentValue) {
return previousValue + currentValue;
}
it(`should render a PDF file`, function() {
cy.request(this.Urls.tests_content_other_extension('pdf')).then(response => {
const data = response.body;
cy.visit(`${this.Urls.browse_content(data.sha1)}?path=file.pdf`);
cy.get('.swh-content canvas')
.wait(2000)
.then(canvas => {
- let width = canvas[0].width;
- let height = canvas[0].height;
- let context = canvas[0].getContext('2d');
- let imgData = context.getImageData(0, 0, width, height);
+ const width = canvas[0].width;
+ const height = canvas[0].height;
+ const context = canvas[0].getContext('2d');
+ const imgData = context.getImageData(0, 0, width, height);
assert.notEqual(imgData.data.reduce(sum), 0);
});
});
});
});
describe('Jupyter notebook rendering test', function() {
it(`should render a notebook file to HTML`, function() {
cy.request(this.Urls.tests_content_other_extension('ipynb')).then(response => {
const data = response.body;
cy.visit(`${this.Urls.browse_content(data.sha1)}?path=file.ipynb`);
cy.get('.nb-notebook')
.should('be.visible')
.and('not.be.empty');
cy.get('.nb-cell.nb-markdown-cell')
.should('be.visible')
.and('not.be.empty');
cy.get('.nb-cell.nb-code-cell')
.should('be.visible')
.and('not.be.empty');
cy.get('.MathJax')
.should('be.visible')
.and('not.be.empty');
});
});
});
diff --git a/cypress/integration/deposit-admin.spec.js b/cypress/integration/deposit-admin.spec.js
index b0e78f11..cba933bc 100644
--- a/cypress/integration/deposit-admin.spec.js
+++ b/cypress/integration/deposit-admin.spec.js
@@ -1,156 +1,156 @@
/**
* Copyright (C) 2020-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
*/
// data to use as request query response
let responseDeposits;
let expectedOrigins;
describe('Test admin deposit page', function() {
beforeEach(() => {
responseDeposits = [
{
'id': 614,
'external_id': 'ch-de-1',
'reception_date': '2020-05-18T13:48:27Z',
'status': 'done',
'status_detail': null,
'swhid': 'swh:1:dir:ef04a768',
'swhid_context': 'swh:1:dir:ef04a768;origin=https://w.s.o/c-d-1;visit=swh:1:snp:b234be1e;anchor=swh:1:rev:d24a75c9;path=/'
},
{
'id': 613,
'external_id': 'ch-de-2',
'reception_date': '2020-05-18T11:20:16Z',
'status': 'done',
'status_detail': null,
'swhid': 'swh:1:dir:181417fb',
'swhid_context': 'swh:1:dir:181417fb;origin=https://w.s.o/c-d-2;visit=swh:1:snp:8c32a2ef;anchor=swh:1:rev:3d1eba04;path=/'
},
{
'id': 612,
'external_id': 'ch-de-3',
'reception_date': '2020-05-18T11:20:16Z',
'status': 'rejected',
'status_detail': 'incomplete deposit!',
'swhid': null,
'swhid_context': null
}
];
// those are computed from the
expectedOrigins = {
614: 'https://w.s.o/c-d-1',
613: 'https://w.s.o/c-d-2',
612: ''
};
});
it('Should display properly entries', function() {
cy.adminLogin();
cy.visit(this.Urls.admin_deposit());
- let testDeposits = responseDeposits;
+ const testDeposits = responseDeposits;
cy.intercept(`${this.Urls.admin_deposit_list()}**`, {
body: {
'draw': 10,
'recordsTotal': testDeposits.length,
'recordsFiltered': testDeposits.length,
'data': testDeposits
}
}).as('listDeposits');
cy.location('pathname')
.should('be.equal', this.Urls.admin_deposit());
cy.url().should('include', '/admin/deposit');
cy.get('#swh-admin-deposit-list')
.should('exist');
cy.wait('@listDeposits').then((xhr) => {
cy.log('response:', xhr.response);
cy.log(xhr.response.body);
- let deposits = xhr.response.body.data;
+ const deposits = xhr.response.body.data;
cy.log('Deposits: ', deposits);
expect(deposits.length).to.equal(testDeposits.length);
cy.get('#swh-admin-deposit-list').find('tbody > tr').as('rows');
// only 2 entries
cy.get('@rows').each((row, idx, collection) => {
- let deposit = deposits[idx];
- let responseDeposit = testDeposits[idx];
+ const deposit = deposits[idx];
+ const responseDeposit = testDeposits[idx];
assert.isNotNull(deposit);
assert.isNotNull(responseDeposit);
expect(deposit.id).to.be.equal(responseDeposit['id']);
expect(deposit.external_id).to.be.equal(responseDeposit['external_id']);
expect(deposit.status).to.be.equal(responseDeposit['status']);
expect(deposit.status_detail).to.be.equal(responseDeposit['status_detail']);
expect(deposit.swhid).to.be.equal(responseDeposit['swhid']);
expect(deposit.swhid_context).to.be.equal(responseDeposit['swhid_context']);
- let expectedOrigin = expectedOrigins[deposit.id];
+ const expectedOrigin = expectedOrigins[deposit.id];
// ensure it's in the dom
cy.contains(deposit.id).should('be.visible');
if (deposit.status !== 'rejected') {
expect(row).to.not.contain(deposit.external_id);
cy.contains(expectedOrigin).should('be.visible');
}
cy.contains(deposit.status).should('be.visible');
// those are hidden by default, so now visible
if (deposit.status_detail !== null) {
cy.contains(deposit.status_detail).should('not.exist');
}
// those are hidden by default
if (deposit.swhid !== null) {
cy.contains(deposit.swhid).should('not.exist');
cy.contains(deposit.swhid_context).should('not.exist');
}
});
// toggling all links and ensure, the previous checks are inverted
cy.get('a.toggle-col').click({'multiple': true}).then(() => {
cy.get('#swh-admin-deposit-list').find('tbody > tr').as('rows');
cy.get('@rows').each((row, idx, collection) => {
- let deposit = deposits[idx];
- let expectedOrigin = expectedOrigins[deposit.id];
+ const deposit = deposits[idx];
+ const expectedOrigin = expectedOrigins[deposit.id];
// ensure it's in the dom
cy.contains(deposit.id).should('not.exist');
if (deposit.status !== 'rejected') {
expect(row).to.not.contain(deposit.external_id);
expect(row).to.contain(expectedOrigin);
}
expect(row).to.not.contain(deposit.status);
// those are hidden by default, so now visible
if (deposit.status_detail !== null) {
cy.contains(deposit.status_detail).should('be.visible');
}
// those are hidden by default, so now they should be visible
if (deposit.swhid !== null) {
cy.contains(deposit.swhid).should('be.visible');
cy.contains(deposit.swhid_context).should('be.visible');
// check SWHID link text formatting
cy.contains(deposit.swhid_context).then(elt => {
expect(elt[0].innerHTML).to.equal(deposit.swhid_context.replace(/;/g, '; '));
});
}
});
});
cy.get('#swh-admin-deposit-list-error')
.should('not.contain',
'An error occurred while retrieving the list of deposits');
});
});
});
diff --git a/cypress/integration/directory.spec.js b/cypress/integration/directory.spec.js
index c5be3bc2..4f1ba217 100644
--- a/cypress/integration/directory.spec.js
+++ b/cypress/integration/directory.spec.js
@@ -1,83 +1,83 @@
/**
* Copyright (C) 2019-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
*/
const $ = Cypress.$;
let origin;
let url;
-let dirs = [];
-let files = [];
+const dirs = [];
+const files = [];
describe('Directory Tests', function() {
before(function() {
origin = this.origin[0];
url = `${this.Urls.browse_origin_directory()}?origin_url=${origin.url}`;
- for (let entry of origin.dirContent) {
+ for (const entry of origin.dirContent) {
if (entry.type === 'file') {
files.push(entry);
} else {
dirs.push(entry);
}
}
});
beforeEach(function() {
cy.visit(url);
});
it('should display all files and directories', function() {
cy.get('.swh-directory')
.should('have.length', dirs.length)
.and('be.visible');
cy.get('.swh-content')
.should('have.length', files.length)
.and('be.visible');
});
it('should display sizes for files', function() {
cy.get('.swh-content')
.parent('tr')
.then((rows) => {
- for (let row of rows) {
- let text = $(row).children('td').eq(2).text();
+ for (const row of rows) {
+ const text = $(row).children('td').eq(2).text();
expect(text.trim()).to.not.be.empty;
}
});
});
it('should display readme when it is present', function() {
cy.get('#readme-panel > .card-body')
.should('be.visible')
.and('have.class', 'swh-showdown')
.and('not.be.empty')
.and('not.contain', 'Readme bytes are not available');
});
it('should open subdirectory when clicked', function() {
cy.get('.swh-directory')
.first()
.children('a')
.click();
cy.url()
.should('include', `${url}&path=${dirs[0]['name']}`);
cy.get('.swh-directory-table')
.should('be.visible');
});
it('should have metadata available from javascript', function() {
cy.window().then(win => {
const metadata = win.swh.webapp.getBrowsedSwhObjectMetadata();
expect(metadata).to.not.be.empty;
expect(metadata).to.have.any.keys('directory');
});
});
});
diff --git a/cypress/integration/errors.spec.js b/cypress/integration/errors.spec.js
index ce16fa1b..4708eba7 100644
--- a/cypress/integration/errors.spec.js
+++ b/cypress/integration/errors.spec.js
@@ -1,147 +1,147 @@
/**
* Copyright (C) 2019-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
*/
let origin;
const invalidChecksum = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
const invalidPageUrl = '/invalidPath';
function urlShouldShowError(url, error) {
cy.visit(url, {
failOnStatusCode: false
});
cy.get('.swh-http-error')
.should('be.visible');
cy.get('.swh-http-error-code')
.should('contain', error.code);
cy.get('.swh-http-error-desc')
.should('contain', error.msg);
}
describe('Test Errors', function() {
before(function() {
origin = this.origin[0];
});
it('should show navigation buttons on error page', function() {
cy.visit(invalidPageUrl, {
failOnStatusCode: false
});
cy.get('a[onclick="window.history.back();"]')
.should('be.visible');
cy.get('a[href="/"')
.should('be.visible');
});
context('For unarchived repositories', function() {
it('should display NotFoundExc for unarchived repo', function() {
const url = `${this.Urls.browse_origin_directory()}?origin_url=${this.unarchivedRepo.url}`;
urlShouldShowError(url, {
code: '404',
msg: 'NotFoundExc: Origin with url ' + this.unarchivedRepo.url + ' not found!'
});
});
it('should display NotFoundExc for unarchived content', function() {
const url = this.Urls.browse_content(`sha1_git:${this.unarchivedRepo.content[0].sha1git}`);
urlShouldShowError(url, {
code: '404',
msg: 'NotFoundExc: Content with sha1_git checksum equals to ' + this.unarchivedRepo.content[0].sha1git + ' not found!'
});
});
it('should display NotFoundExc for unarchived directory sha1git', function() {
const url = this.Urls.browse_directory(this.unarchivedRepo.rootDirectory);
urlShouldShowError(url, {
code: '404',
msg: 'NotFoundExc: Directory with sha1_git ' + this.unarchivedRepo.rootDirectory + ' not found'
});
});
it('should display NotFoundExc for unarchived revision sha1git', function() {
const url = this.Urls.browse_revision(this.unarchivedRepo.revision);
urlShouldShowError(url, {
code: '404',
msg: 'NotFoundExc: Revision with sha1_git ' + this.unarchivedRepo.revision + ' not found.'
});
});
it('should display NotFoundExc for unarchived snapshot sha1git', function() {
const url = this.Urls.browse_snapshot(this.unarchivedRepo.snapshot);
urlShouldShowError(url, {
code: '404',
msg: 'Snapshot with id ' + this.unarchivedRepo.snapshot + ' not found!'
});
});
});
context('For archived repositories', function() {
before(function() {
const url = `${this.Urls.browse_origin_directory()}?origin_url=${origin.url}`;
cy.visit(url);
});
it('should display NotFoundExc for invalid directory from archived repo', function() {
const subDir = `${this.Urls.browse_origin_directory()}?origin_url=${origin.url}&path=${origin.invalidSubDir}`;
urlShouldShowError(subDir, {
code: '404',
msg: 'NotFoundExc: Directory entry with path ' +
origin.invalidSubDir + ' from root directory ' +
origin.rootDirectory + ' not found'
});
});
it(`should display NotFoundExc for incorrect origin_url
with correct content hash`, function() {
const url = this.Urls.browse_content(`sha1_git:${origin.content[0].sha1git}`) +
`?origin_url=${this.unarchivedRepo.url}`;
urlShouldShowError(url, {
code: '404',
msg: 'The Software Heritage archive has a content ' +
'with the hash you provided but the origin ' +
'mentioned in your request appears broken: ' +
this.unarchivedRepo.url + '. ' +
'Please check the URL and try again.\n\n' +
'Nevertheless, you can still browse the content ' +
'without origin information: ' +
'/browse/content/sha1_git:' +
origin.content[0].sha1git + '/'
});
});
});
context('For invalid data', function() {
it(`should display 400 for invalid checksum for
directory, snapshot, revision, content`, function() {
const types = ['directory', 'snapshot', 'revision', 'content'];
- for (let type of types) {
+ for (const type of types) {
const url = this.Urls[`browse_${type}`](invalidChecksum);
urlShouldShowError(url, {
code: '400',
msg: 'BadInputExc: Invalid checksum query string ' +
invalidChecksum
});
}
});
it('should show 404 error for invalid path', function() {
urlShouldShowError(invalidPageUrl, {
code: '404',
msg: 'The resource ' + invalidPageUrl +
' could not be found on the server.'
});
});
});
});
diff --git a/cypress/integration/home.spec.js b/cypress/integration/home.spec.js
index 0491e710..1f8c6efd 100644
--- a/cypress/integration/home.spec.js
+++ b/cypress/integration/home.spec.js
@@ -1,108 +1,108 @@
/**
* 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 $ = Cypress.$;
const url = '/';
describe('Home Page Tests', function() {
it('should have focus on search form after page load', function() {
cy.visit(url);
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 display positive stats for each category', function() {
cy.intercept(this.Urls.stat_counters())
.as('getStatCounters');
cy.visit(url)
.wait('@getStatCounters')
.wait(500)
.get('.swh-counter:visible')
.then((counters) => {
- for (let counter of counters) {
- let innerText = $(counter).text();
+ for (const counter of counters) {
+ const innerText = $(counter).text();
const value = parseInt(innerText.replace(/,/g, ''));
assert.isAbove(value, 0);
}
});
});
it('should display null counters and hide history graphs when storage is empty', function() {
cy.intercept(this.Urls.stat_counters(), {
body: {
'stat_counters': {},
'stat_counters_history': {}
}
}).as('getStatCounters');
cy.visit(url)
.wait('@getStatCounters')
.wait(500)
.get('.swh-counter:visible')
.then((counters) => {
- for (let counter of counters) {
+ for (const counter of counters) {
const value = parseInt($(counter).text());
assert.equal(value, 0);
}
});
cy.get('.swh-counter-history')
.should('not.be.visible');
});
it('should hide counters when data is missing', function() {
cy.intercept(this.Urls.stat_counters(), {
body: {
'stat_counters': {
'content': 150,
'directory': 45,
'revision': 78
},
'stat_counters_history': {}
}
}).as('getStatCounters');
cy.visit(url)
.wait('@getStatCounters')
.wait(500);
cy.get('#swh-content-count, #swh-directory-count, #swh-revision-count')
.should('be.visible');
cy.get('#swh-release-count, #swh-person-count, #swh-origin-count')
.should('not.be.visible');
cy.get('.swh-counter-history')
.should('not.be.visible');
});
it('should redirect to search page when submitting search form', function() {
const searchText = 'git';
cy.get('#swh-origins-url-patterns')
.type(searchText)
.get('.swh-search-icon')
.click();
cy.location('pathname')
.should('equal', this.Urls.browse_search());
cy.location('search')
.should('equal', `?q=${searchText}&with_visit=true&with_content=true`);
});
});
diff --git a/cypress/integration/layout.spec.js b/cypress/integration/layout.spec.js
index dd17d854..5944b1d5 100644
--- a/cypress/integration/layout.spec.js
+++ b/cypress/integration/layout.spec.js
@@ -1,231 +1,231 @@
/**
* 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 url = '/browse/help/';
const statusUrl = 'https://status.softwareheritage.org';
describe('Test top-bar', function() {
beforeEach(function() {
cy.clearLocalStorage();
cy.visit(url);
});
it('should should contain all navigation links', function() {
cy.get('.swh-top-bar a')
.should('have.length.of.at.least', 4)
.and('be.visible')
.and('have.attr', 'href');
});
it('should show donate button on lg screen', function() {
cy.get('.swh-donate-link')
.should('be.visible');
});
it('should hide donate button on sm screen', function() {
cy.viewport(600, 800);
cy.get('.swh-donate-link')
.should('not.be.visible');
});
it('should hide full width switch on small screens', function() {
cy.viewport(360, 740);
cy.get('#swh-full-width-switch-container')
.should('not.be.visible');
cy.viewport(600, 800);
cy.get('#swh-full-width-switch-container')
.should('not.be.visible');
cy.viewport(800, 600);
cy.get('#swh-full-width-switch-container')
.should('not.be.visible');
});
it('should show full width switch on large screens', function() {
cy.viewport(1024, 768);
cy.get('#swh-full-width-switch-container')
.should('be.visible');
cy.viewport(1920, 1080);
cy.get('#swh-full-width-switch-container')
.should('be.visible');
});
it('should change container width when toggling Full width switch', function() {
cy.get('#swh-web-content')
.should('have.class', 'container')
.should('not.have.class', 'container-fluid');
cy.should(() => {
expect(JSON.parse(localStorage.getItem('swh-web-full-width'))).to.be.null;
});
cy.get('#swh-full-width-switch')
.click({force: true});
cy.get('#swh-web-content')
.should('not.have.class', 'container')
.should('have.class', 'container-fluid');
cy.should(() => {
expect(JSON.parse(localStorage.getItem('swh-web-full-width'))).to.be.true;
});
cy.get('#swh-full-width-switch')
.click({force: true});
cy.get('#swh-web-content')
.should('have.class', 'container')
.should('not.have.class', 'container-fluid');
cy.should(() => {
expect(JSON.parse(localStorage.getItem('swh-web-full-width'))).to.be.false;
});
});
it('should restore container width when loading page again', function() {
cy.visit(url)
.get('#swh-web-content')
.should('have.class', 'container')
.should('not.have.class', 'container-fluid');
cy.get('#swh-full-width-switch')
.click({force: true});
cy.visit(url)
.get('#swh-web-content')
.should('not.have.class', 'container')
.should('have.class', 'container-fluid');
cy.get('#swh-full-width-switch')
.click({force: true});
cy.visit(url)
.get('#swh-web-content')
.should('have.class', 'container')
.should('not.have.class', 'container-fluid');
});
function genStatusResponse(status, statusCode) {
return {
'result': {
'status': [
{
'id': '5f7c4c567f50b304c1e7bd5f',
'name': 'Save Code Now',
'updated': '2020-11-30T13:51:21.151Z',
'status': 'Operational',
'status_code': 100
},
{
'id': '5f7c4c6f8338bc04b7f476fe',
'name': 'Source Code Crawlers',
'updated': '2020-11-30T13:51:21.151Z',
'status': status,
'status_code': statusCode
}
]
}
};
}
it('should display swh status widget when data are available', function() {
const statusTestData = [
{
status: 'Operational',
statusCode: 100,
color: 'green'
},
{
status: 'Scheduled Maintenance',
statusCode: 200,
color: 'blue'
},
{
status: 'Degraded Performance',
statusCode: 300,
color: 'yellow'
},
{
status: 'Partial Service Disruption',
statusCode: 400,
color: 'yellow'
},
{
status: 'Service Disruption',
statusCode: 500,
color: 'red'
},
{
status: 'Security Event',
statusCode: 600,
color: 'red'
}
];
const responses = [];
- for (let std of statusTestData) {
+ for (const std of statusTestData) {
responses.push(genStatusResponse(std.status, std.statusCode));
}
let i = 0;
- for (let std of statusTestData) {
+ for (const std of statusTestData) {
cy.visit(url);
// trick to override the response of an intercepted request
// https://github.com/cypress-io/cypress/issues/9302
cy.intercept(`${statusUrl}/**`, req => req.reply(responses.shift()))
.as(`getSwhStatusData${i}`);
cy.wait(`@getSwhStatusData${i}`);
cy.get('.swh-current-status-indicator').should('have.class', std.color);
cy.get('#swh-current-status-description').should('have.text', std.status);
++i;
}
});
it('should not display swh status widget when data are not available', function() {
cy.intercept(`${statusUrl}/**`, {
body: {}
}).as('getSwhStatusData');
cy.visit(url);
cy.wait('@getSwhStatusData');
cy.get('.swh-current-status').should('not.exist');
});
});
describe('Test navbar', function() {
it('should redirect to search page when submitting search form in navbar', function() {
const keyword = 'python';
cy.get('#swh-origins-search-top-input')
.type(keyword);
cy.get('.swh-search-navbar')
.submit();
cy.url()
.should('include', `${this.Urls.browse_search()}?q=${keyword}`);
});
});
describe('Test footer', function() {
beforeEach(function() {
cy.visit(url);
});
it('should be visible', function() {
cy.get('footer')
.should('be.visible');
});
it('should have correct copyright years', function() {
const currentYear = new Date().getFullYear();
const copyrightText = '(C) 2015–' + currentYear.toString();
cy.get('footer')
.should('contain', copyrightText);
});
it('should contain link to Web API', function() {
cy.get('footer')
.get(`a[href="${this.Urls.api_1_homepage()}"]`)
.should('contain', 'Web API');
});
});
diff --git a/cypress/integration/origin-save.spec.js b/cypress/integration/origin-save.spec.js
index becb6ed8..b2300530 100644
--- a/cypress/integration/origin-save.spec.js
+++ b/cypress/integration/origin-save.spec.js
@@ -1,711 +1,711 @@
/**
* 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
*/
let url;
let origin;
const $ = Cypress.$;
const saveCodeMsg = {
'success': 'The "save code now" request has been accepted and will be processed as soon as possible.',
'warning': 'The "save code now" request has been put in pending state and may be accepted for processing after manual review.',
'rejected': 'The "save code now" request has been rejected because the provided origin url is blacklisted.',
'rateLimit': 'The rate limit for "save code now" requests has been reached. Please try again later.',
'not-found': 'The provided url does not exist',
'unknownError': 'An unexpected error happened when submitting the "save code now request',
'csrfError': 'CSRF Failed: Referrer checking failed - no Referrer.'
};
const anonymousVisitTypes = ['git', 'hg', 'svn'];
const allVisitTypes = ['archives', 'git', 'hg', 'svn'];
function makeOriginSaveRequest(originType, originUrl) {
cy.get('#swh-input-origin-url')
.type(originUrl)
.get('#swh-input-visit-type')
.select(originType)
.get('#swh-save-origin-form')
.submit();
}
function checkAlertVisible(alertType, msg) {
cy.get('#swh-origin-save-request-status')
.should('be.visible')
.find(`.alert-${alertType}`)
.should('be.visible')
.and('contain', msg);
}
// Stub requests to save an origin
function stubSaveRequest({
requestUrl,
visitType = 'git',
saveRequestStatus,
originUrl,
saveTaskStatus,
responseStatus = 200,
// For error code with the error message in the 'reason' key response
errorMessage = '',
saveRequestDate = new Date(),
visitDate = new Date(),
visitStatus = null
} = {}) {
let response;
if (responseStatus !== 200 && errorMessage) {
response = {
'reason': errorMessage
};
} else {
response = genOriginSaveResponse({visitType: visitType,
saveRequestStatus: saveRequestStatus,
originUrl: originUrl,
saveRequestDate: saveRequestDate,
saveTaskStatus: saveTaskStatus,
visitDate: visitDate,
visitStatus: visitStatus
});
}
cy.intercept('POST', requestUrl, {body: response, statusCode: responseStatus})
.as('saveRequest');
}
// Mocks API response : /save/(:visit_type)/(:origin_url)
// visit_type : {'git', 'hg', 'svn', ...}
function genOriginSaveResponse({
visitType = 'git',
saveRequestStatus,
originUrl,
saveRequestDate = new Date(),
saveTaskStatus,
visitDate = new Date(),
visitStatus
} = {}) {
return {
'visit_type': visitType,
'save_request_status': saveRequestStatus,
'origin_url': originUrl,
'id': 1,
'save_request_date': saveRequestDate ? saveRequestDate.toISOString() : null,
'save_task_status': saveTaskStatus,
'visit_date': visitDate ? visitDate.toISOString() : null,
'visit_status': visitStatus
};
};
describe('Origin Save Tests', function() {
before(function() {
url = this.Urls.origin_save();
origin = this.origin[0];
this.originSaveUrl = this.Urls.api_1_save_origin(origin.type, origin.url);
});
beforeEach(function() {
cy.fixture('origin-save').as('originSaveJSON');
cy.fixture('save-task-info').as('saveTaskInfoJSON');
cy.visit(url);
});
it('should format appropriately values depending on their type', function() {
- let inputValues = [ // null values stay null
+ const inputValues = [ // null values stay null
{type: 'json', value: null, expectedValue: null},
{type: 'date', value: null, expectedValue: null},
{type: 'raw', value: null, expectedValue: null},
{type: 'duration', value: null, expectedValue: null},
// non null values formatted depending on their type
{type: 'json', value: '{}', expectedValue: '"{}"'},
{type: 'date', value: '04/04/2021 01:00:00', expectedValue: '4/4/2021, 1:00:00 AM'},
{type: 'raw', value: 'value-for-identity', expectedValue: 'value-for-identity'},
{type: 'duration', value: '10', expectedValue: '10 seconds'},
{type: 'duration', value: 100, expectedValue: '100 seconds'}
];
cy.window().then(win => {
inputValues.forEach(function(input, index, array) {
- let actualValue = win.swh.save.formatValuePerType(input.type, input.value);
+ const actualValue = win.swh.save.formatValuePerType(input.type, input.value);
assert.equal(actualValue, input.expectedValue);
});
});
});
it('should display accepted message when accepted', function() {
stubSaveRequest({requestUrl: this.originSaveUrl,
saveRequestStatus: 'accepted',
originUrl: origin.url,
saveTaskStatus: 'not yet scheduled'});
makeOriginSaveRequest(origin.type, origin.url);
cy.wait('@saveRequest').then(() => {
checkAlertVisible('success', saveCodeMsg['success']);
});
});
it('should validate gitlab subproject url', function() {
const gitlabSubProjectUrl = 'https://gitlab.com/user/project/sub/';
const originSaveUrl = this.Urls.api_1_save_origin('git', gitlabSubProjectUrl);
stubSaveRequest({requestUrl: originSaveUrl,
saveRequestStatus: 'accepted',
originurl: gitlabSubProjectUrl,
saveTaskStatus: 'not yet scheduled'});
makeOriginSaveRequest('git', gitlabSubProjectUrl);
cy.wait('@saveRequest').then(() => {
checkAlertVisible('success', saveCodeMsg['success']);
});
});
it('should validate project url with _ in username', function() {
const gitlabSubProjectUrl = 'https://gitlab.com/user_name/project.git';
const originSaveUrl = this.Urls.api_1_save_origin('git', gitlabSubProjectUrl);
stubSaveRequest({requestUrl: originSaveUrl,
saveRequestStatus: 'accepted',
originurl: gitlabSubProjectUrl,
saveTaskStatus: 'not yet scheduled'});
makeOriginSaveRequest('git', gitlabSubProjectUrl);
cy.wait('@saveRequest').then(() => {
checkAlertVisible('success', saveCodeMsg['success']);
});
});
it('should display warning message when pending', function() {
stubSaveRequest({requestUrl: this.originSaveUrl,
saveRequestStatus: 'pending',
originUrl: origin.url,
saveTaskStatus: 'not created'});
makeOriginSaveRequest(origin.type, origin.url);
cy.wait('@saveRequest').then(() => {
checkAlertVisible('warning', saveCodeMsg['warning']);
});
});
it('should show error when the origin does not exist (status: 400)', function() {
stubSaveRequest({requestUrl: this.originSaveUrl,
originUrl: origin.url,
responseStatus: 400,
errorMessage: saveCodeMsg['not-found']});
makeOriginSaveRequest(origin.type, origin.url);
cy.wait('@saveRequest').then(() => {
checkAlertVisible('danger', saveCodeMsg['not-found']);
});
});
it('should show error when csrf validation failed (status: 403)', function() {
stubSaveRequest({requestUrl: this.originSaveUrl,
saveRequestStatus: 'rejected',
originUrl: origin.url,
saveTaskStatus: 'not created',
responseStatus: 403,
errorMessage: saveCodeMsg['csrfError']});
makeOriginSaveRequest(origin.type, origin.url);
cy.wait('@saveRequest').then(() => {
checkAlertVisible('danger', saveCodeMsg['csrfError']);
});
});
it('should show error when origin is rejected (status: 403)', function() {
stubSaveRequest({requestUrl: this.originSaveUrl,
saveRequestStatus: 'rejected',
originUrl: origin.url,
saveTaskStatus: 'not created',
responseStatus: 403,
errorMessage: saveCodeMsg['rejected']});
makeOriginSaveRequest(origin.type, origin.url);
cy.wait('@saveRequest').then(() => {
checkAlertVisible('danger', saveCodeMsg['rejected']);
});
});
it('should show error when rate limited (status: 429)', function() {
stubSaveRequest({requestUrl: this.originSaveUrl,
saveRequestStatus: 'Request was throttled. Expected available in 60 seconds.',
originUrl: origin.url,
saveTaskStatus: 'not created',
responseStatus: 429});
makeOriginSaveRequest(origin.type, origin.url);
cy.wait('@saveRequest').then(() => {
checkAlertVisible('danger', saveCodeMsg['rateLimit']);
});
});
it('should show error when unknown error occurs (status other than 200, 403, 429)', function() {
stubSaveRequest({requestUrl: this.originSaveUrl,
saveRequestStatus: 'Error',
originUrl: origin.url,
saveTaskStatus: 'not created',
responseStatus: 406});
makeOriginSaveRequest(origin.type, origin.url);
cy.wait('@saveRequest').then(() => {
checkAlertVisible('danger', saveCodeMsg['unknownError']);
});
});
it('should display origin save info in the requests table', function() {
cy.intercept('/save/requests/list/**', {fixture: 'origin-save'});
cy.get('#swh-origin-save-requests-list-tab').click();
cy.get('tbody tr').then(rows => {
let i = 0;
- for (let row of rows) {
+ for (const row of rows) {
const cells = row.cells;
const requestDateStr = new Date(this.originSaveJSON.data[i].save_request_date).toLocaleString();
const saveStatus = this.originSaveJSON.data[i].save_task_status;
assert.equal($(cells[0]).text(), requestDateStr);
assert.equal($(cells[1]).text(), this.originSaveJSON.data[i].visit_type);
let html = '';
if (saveStatus === 'succeeded') {
let browseOriginUrl = `${this.Urls.browse_origin()}?origin_url=${encodeURIComponent(this.originSaveJSON.data[i].origin_url)}`;
browseOriginUrl += `×tamp=${encodeURIComponent(this.originSaveJSON.data[i].visit_date)}`;
html += `${this.originSaveJSON.data[i].origin_url}`;
} else {
html += this.originSaveJSON.data[i].origin_url;
}
html += ` `;
html += '';
assert.equal($(cells[2]).html(), html);
assert.equal($(cells[3]).text(), this.originSaveJSON.data[i].save_request_status);
assert.equal($(cells[4]).text(), saveStatus);
++i;
}
});
});
it('should not add timestamp to the browse origin URL is no visit date has been found', function() {
const originUrl = 'https://git.example.org/example.git';
const saveRequestData = genOriginSaveResponse({
saveRequestStatus: 'accepted',
originUrl: originUrl,
saveTaskStatus: 'succeeded',
visitDate: null,
visitStatus: 'full'
});
const saveRequestsListData = {
'recordsTotal': 1,
'draw': 2,
'recordsFiltered': 1,
'data': [saveRequestData]
};
cy.intercept('/save/requests/list/**', {body: saveRequestsListData})
.as('saveRequestsList');
cy.get('#swh-origin-save-requests-list-tab').click();
cy.wait('@saveRequestsList');
cy.get('tbody tr').then(rows => {
const firstRowCells = rows[0].cells;
const browseOriginUrl = `${this.Urls.browse_origin()}?origin_url=${encodeURIComponent(originUrl)}`;
const browseOriginLink = `${originUrl}`;
expect($(firstRowCells[2]).html()).to.have.string(browseOriginLink);
});
});
it('should display/close task info popover when clicking on the info button', function() {
cy.intercept('/save/requests/list/**', {fixture: 'origin-save'});
cy.intercept('/save/task/info/**', {fixture: 'save-task-info'});
cy.get('#swh-origin-save-requests-list-tab').click();
cy.get('.swh-save-request-info')
.eq(0)
.click();
cy.get('.swh-save-request-info-popover')
.should('be.visible');
cy.get('.swh-save-request-info')
.eq(0)
.click();
cy.get('.swh-save-request-info-popover')
.should('not.exist');
});
it('should hide task info popover when clicking on the close button', function() {
cy.intercept('/save/requests/list/**', {fixture: 'origin-save'});
cy.intercept('/save/task/info/**', {fixture: 'save-task-info'});
cy.get('#swh-origin-save-requests-list-tab').click();
cy.get('.swh-save-request-info')
.eq(0)
.click();
cy.get('.swh-save-request-info-popover')
.should('be.visible');
cy.get('.swh-save-request-info-close')
.click();
cy.get('.swh-save-request-info-popover')
.should('not.exist');
});
it('should fill save request form when clicking on "Save again" button', function() {
cy.intercept('/save/requests/list/**', {fixture: 'origin-save'});
cy.get('#swh-origin-save-requests-list-tab').click();
cy.get('.swh-save-origin-again')
.eq(0)
.click();
cy.get('tbody tr').eq(0).then(row => {
const cells = row[0].cells;
cy.get('#swh-input-visit-type')
.should('have.value', $(cells[1]).text());
cy.get('#swh-input-origin-url')
.should('have.value', $(cells[2]).text().slice(0, -1));
});
});
it('should select correct visit type if possible when clicking on "Save again" button', function() {
const originUrl = 'https://gitlab.inria.fr/solverstack/maphys/maphys/';
const badVisitType = 'hg';
const goodVisitType = 'git';
cy.intercept('/save/requests/list/**', {fixture: 'origin-save'});
stubSaveRequest({requestUrl: this.Urls.api_1_save_origin(badVisitType, originUrl),
visitType: badVisitType,
saveRequestStatus: 'accepted',
originUrl: originUrl,
saveTaskStatus: 'failed',
visitStatus: 'failed',
responseStatus: 200,
errorMessage: saveCodeMsg['accepted']});
makeOriginSaveRequest(badVisitType, originUrl);
cy.get('#swh-origin-save-requests-list-tab').click();
cy.wait('@saveRequest').then(() => {
cy.get('.swh-save-origin-again')
.eq(0)
.click();
cy.get('tbody tr').eq(0).then(row => {
const cells = row[0].cells;
cy.get('#swh-input-visit-type')
.should('have.value', goodVisitType);
cy.get('#swh-input-origin-url')
.should('have.value', $(cells[2]).text().slice(0, -1));
});
});
});
it('should create save request for authenticated user', function() {
cy.userLogin();
cy.visit(url);
const originUrl = 'https://git.example.org/account/repo';
stubSaveRequest({requestUrl: this.Urls.api_1_save_origin('git', originUrl),
saveRequestStatus: 'accepted',
originUrl: origin.url,
saveTaskStatus: 'not yet scheduled'});
makeOriginSaveRequest('git', originUrl);
cy.wait('@saveRequest').then(() => {
checkAlertVisible('success', saveCodeMsg['success']);
});
});
it('should not show user requests filter checkbox for anonymous users', function() {
cy.get('#swh-origin-save-requests-list-tab').click();
cy.get('#swh-save-requests-user-filter').should('not.exist');
});
it('should show user requests filter checkbox for authenticated users', function() {
cy.userLogin();
cy.visit(url);
cy.get('#swh-origin-save-requests-list-tab').click();
cy.get('#swh-save-requests-user-filter').should('exist');
});
it('should show only user requests when filter is activated', function() {
cy.intercept('POST', '/api/1/origin/save/**')
.as('saveRequest');
const originAnonymousUser = 'https://some.git.server/project/';
const originAuthUser = 'https://other.git.server/project/';
// anonymous user creates a save request
makeOriginSaveRequest('git', originAnonymousUser);
cy.wait('@saveRequest');
// authenticated user creates another save request
cy.userLogin();
cy.visit(url);
makeOriginSaveRequest('git', originAuthUser);
cy.wait('@saveRequest');
// user requests filter checkbox should be in the DOM
cy.get('#swh-origin-save-requests-list-tab').click();
cy.get('#swh-save-requests-user-filter').should('exist');
// check unfiltered user requests
cy.get('tbody tr').then(rows => {
expect(rows.length).to.eq(2);
expect($(rows[0].cells[2]).text()).to.contain(originAuthUser);
expect($(rows[1].cells[2]).text()).to.contain(originAnonymousUser);
});
// activate filter and check filtered user requests
cy.get('#swh-save-requests-user-filter')
.click({force: true});
cy.get('tbody tr').then(rows => {
expect(rows.length).to.eq(1);
expect($(rows[0].cells[2]).text()).to.contain(originAuthUser);
});
// deactivate filter and check unfiltered user requests
cy.get('#swh-save-requests-user-filter')
.click({force: true});
cy.get('tbody tr').then(rows => {
expect(rows.length).to.eq(2);
});
});
it('should list unprivileged visit types when not connected', function() {
cy.visit(url);
cy.get('#swh-input-visit-type').children('option').then(options => {
const actual = [...options].map(o => o.value);
expect(actual).to.deep.eq(anonymousVisitTypes);
});
});
it('should list unprivileged visit types when connected as unprivileged user', function() {
cy.userLogin();
cy.visit(url);
cy.get('#swh-input-visit-type').children('option').then(options => {
const actual = [...options].map(o => o.value);
expect(actual).to.deep.eq(anonymousVisitTypes);
});
});
it('should list privileged visit types when connected as ambassador', function() {
cy.ambassadorLogin();
cy.visit(url);
cy.get('#swh-input-visit-type').children('option').then(options => {
const actual = [...options].map(o => o.value);
expect(actual).to.deep.eq(allVisitTypes);
});
});
it('should display extra inputs when dealing with \'archives\' visit type', function() {
cy.ambassadorLogin();
cy.visit(url);
- for (let visitType of anonymousVisitTypes) {
+ for (const visitType of anonymousVisitTypes) {
cy.get('#swh-input-visit-type').select(visitType);
cy.get('.swh-save-origin-archives-form').should('not.be.visible');
}
// this should display more inputs with the 'archives' type
cy.get('#swh-input-visit-type').select('archives');
cy.get('.swh-save-origin-archives-form').should('be.visible');
});
it('should be allowed to submit \'archives\' save request when connected as ambassador', function() {
- let originUrl = 'https://ftp.gnu.org/pub/pub/gnu/3dldf';
- let artifactUrl = 'https://ftp.gnu.org/pub/pub/gnu/3dldf/3DLDF-1.1.4.tar.gz';
- let artifactVersion = '1.1.4';
+ const originUrl = 'https://ftp.gnu.org/pub/pub/gnu/3dldf';
+ const artifactUrl = 'https://ftp.gnu.org/pub/pub/gnu/3dldf/3DLDF-1.1.4.tar.gz';
+ const artifactVersion = '1.1.4';
stubSaveRequest({
requestUrl: this.Urls.api_1_save_origin('archives', originUrl),
saveRequestStatus: 'accepted',
originUrl: originUrl,
saveTaskStatus: 'not yet scheduled'
});
cy.ambassadorLogin();
cy.visit(url);
// input new 'archives' information and submit
cy.get('#swh-input-origin-url')
.type(originUrl)
.get('#swh-input-visit-type')
.select('archives')
.get('#swh-input-artifact-url-0')
.type(artifactUrl)
.get('#swh-input-artifact-version-0')
.clear()
.type(artifactVersion)
.get('#swh-save-origin-form')
.submit();
cy.wait('@saveRequest').then(() => {
checkAlertVisible('success', saveCodeMsg['success']);
});
});
it('should submit multiple artifacts for the archives visit type', function() {
- let originUrl = 'https://ftp.gnu.org/pub/pub/gnu/3dldf';
- let artifactUrl = 'https://ftp.gnu.org/pub/pub/gnu/3dldf/3DLDF-1.1.4.tar.gz';
- let artifactVersion = '1.1.4';
- let artifact2Url = 'https://ftp.gnu.org/pub/pub/gnu/3dldf/3DLDF-1.1.5.tar.gz';
- let artifact2Version = '1.1.5';
+ const originUrl = 'https://ftp.gnu.org/pub/pub/gnu/3dldf';
+ const artifactUrl = 'https://ftp.gnu.org/pub/pub/gnu/3dldf/3DLDF-1.1.4.tar.gz';
+ const artifactVersion = '1.1.4';
+ const artifact2Url = 'https://ftp.gnu.org/pub/pub/gnu/3dldf/3DLDF-1.1.5.tar.gz';
+ const artifact2Version = '1.1.5';
cy.ambassadorLogin();
cy.visit(url);
cy.get('#swh-input-origin-url')
.type(originUrl)
.get('#swh-input-visit-type')
.select('archives');
// fill first artifact info
cy.get('#swh-input-artifact-url-0')
.type(artifactUrl)
.get('#swh-input-artifact-version-0')
.clear()
.type(artifactVersion);
// add new artifact form row
cy.get('#swh-add-archive-artifact')
.click();
// check new row is displayed
cy.get('#swh-input-artifact-url-1')
.should('exist');
// request removal of newly added row
cy.get('#swh-remove-archive-artifact-1')
.click();
// check row has been removed
cy.get('#swh-input-artifact-url-1')
.should('not.exist');
// add new artifact form row
cy.get('#swh-add-archive-artifact')
.click();
// fill second artifact info
cy.get('#swh-input-artifact-url-1')
.type(artifact2Url)
.get('#swh-input-artifact-version-1')
.clear()
.type(artifact2Version);
// setup request interceptor to check POST data and stub response
cy.intercept('POST', this.Urls.api_1_save_origin('archives', originUrl), (req) => {
expect(req.body).to.deep.equal({
archives_data: [
{artifact_url: artifactUrl, artifact_version: artifactVersion},
{artifact_url: artifact2Url, artifact_version: artifact2Version}
]
});
req.reply(genOriginSaveResponse({
visitType: 'archives',
saveRequestStatus: 'accepted',
originUrl: originUrl,
saveRequestDate: new Date(),
saveTaskStatus: 'not yet scheduled',
visitDate: null,
visitStatus: null
}));
}).as('saveRequest');
// submit form
cy.get('#swh-save-origin-form')
.submit();
// submission should be successful
cy.wait('@saveRequest').then(() => {
checkAlertVisible('success', saveCodeMsg['success']);
});
});
it('should autofill artifact version when pasting artifact url', function() {
- let originUrl = 'https://ftp.gnu.org/pub/pub/gnu/3dldf';
- let artifactUrl = 'https://ftp.gnu.org/pub/pub/gnu/3dldf/3DLDF-1.1.4.tar.gz';
- let artifactVersion = '3DLDF-1.1.4';
- let artifact2Url = 'https://example.org/artifact/test/1.3.0.zip';
- let artifact2Version = '1.3.0';
+ const originUrl = 'https://ftp.gnu.org/pub/pub/gnu/3dldf';
+ const artifactUrl = 'https://ftp.gnu.org/pub/pub/gnu/3dldf/3DLDF-1.1.4.tar.gz';
+ const artifactVersion = '3DLDF-1.1.4';
+ const artifact2Url = 'https://example.org/artifact/test/1.3.0.zip';
+ const artifact2Version = '1.3.0';
cy.ambassadorLogin();
cy.visit(url);
cy.get('#swh-input-origin-url')
.type(originUrl)
.get('#swh-input-visit-type')
.select('archives');
// fill first artifact info
cy.get('#swh-input-artifact-url-0')
.type(artifactUrl);
// check autofilled version
cy.get('#swh-input-artifact-version-0')
.should('have.value', artifactVersion);
// add new artifact form row
cy.get('#swh-add-archive-artifact')
.click();
// fill second artifact info
cy.get('#swh-input-artifact-url-1')
.type(artifact2Url);
// check autofilled version
cy.get('#swh-input-artifact-version-1')
.should('have.value', artifact2Version);
});
it('should use canonical URL for github repository to save', function() {
const ownerRepo = 'BIC-MNI/mni_autoreg';
const canonicalOriginUrl = 'https://github.com/BIC-MNI/mni_autoreg';
// stub call to github Web API fetching canonical repo URL
cy.intercept(`https://api.github.com/repos/${ownerRepo.toLowerCase()}`, (req) => {
req.reply({html_url: canonicalOriginUrl});
}).as('ghWebApiRequest');
// stub save request creation with canonical URL of github repo
cy.intercept('POST', this.Urls.api_1_save_origin('git', canonicalOriginUrl), (req) => {
req.reply(genOriginSaveResponse({
visitType: 'git',
saveRequestStatus: 'accepted',
originUrl: canonicalOriginUrl,
saveRequestDate: new Date(),
saveTaskStatus: 'not yet scheduled',
visitDate: null,
visitStatus: null
}));
}).as('saveRequest');
- for (let originUrl of ['https://github.com/BiC-MnI/MnI_AuToReG',
- 'https://github.com/BiC-MnI/MnI_AuToReG.git',
- 'https://github.com/BiC-MnI/MnI_AuToReG/']) {
+ for (const originUrl of ['https://github.com/BiC-MnI/MnI_AuToReG',
+ 'https://github.com/BiC-MnI/MnI_AuToReG.git',
+ 'https://github.com/BiC-MnI/MnI_AuToReG/']) {
// enter non canonical URL of github repo
cy.get('#swh-input-origin-url')
.clear()
.type(originUrl);
// submit form
cy.get('#swh-save-origin-form')
.submit();
// submission should be successful
cy.wait('@ghWebApiRequest')
.wait('@saveRequest').then(() => {
checkAlertVisible('success', saveCodeMsg['success']);
});
}
});
});
diff --git a/cypress/integration/origin-search.spec.js b/cypress/integration/origin-search.spec.js
index 4ecf0b12..6a7f9288 100644
--- a/cypress/integration/origin-search.spec.js
+++ b/cypress/integration/origin-search.spec.js
@@ -1,569 +1,569 @@
/**
* 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 (let visitType of ['git', 'tar']) {
+ 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 (let elt of 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);
});
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());
});
});
});
diff --git a/cypress/integration/persistent-identifiers.spec.js b/cypress/integration/persistent-identifiers.spec.js
index a4d57cfd..5211d963 100644
--- a/cypress/integration/persistent-identifiers.spec.js
+++ b/cypress/integration/persistent-identifiers.spec.js
@@ -1,228 +1,228 @@
/**
* Copyright (C) 2019-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
*/
let origin, originBadgeUrl, originBrowseUrl;
let url, urlPrefix;
let cntSWHID, cntSWHIDWithContext;
let dirSWHID, dirSWHIDWithContext;
let relSWHID, relSWHIDWithContext;
let revSWHID, revSWHIDWithContext;
let snpSWHID, snpSWHIDWithContext;
let testsData;
const firstSelLine = 6;
const lastSelLine = 12;
describe('Persistent Identifiers Tests', function() {
before(function() {
origin = this.origin[1];
url = `${this.Urls.browse_origin_content()}?origin_url=${origin.url}&path=${origin.content[0].path}`;
url = `${url}&release=${origin.release.name}#L${firstSelLine}-L${lastSelLine}`;
originBadgeUrl = this.Urls.swh_badge('origin', origin.url);
originBrowseUrl = `${this.Urls.browse_origin()}?origin_url=${origin.url}`;
cy.visit(url).window().then(win => {
urlPrefix = `${win.location.protocol}//${win.location.hostname}`;
if (win.location.port) {
urlPrefix += `:${win.location.port}`;
}
const swhids = win.swh.webapp.getSwhIdsContext();
cntSWHID = swhids.content.swhid;
cntSWHIDWithContext = swhids.content.swhid_with_context;
cntSWHIDWithContext += `;lines=${firstSelLine}-${lastSelLine}`;
dirSWHID = swhids.directory.swhid;
dirSWHIDWithContext = swhids.directory.swhid_with_context;
revSWHID = swhids.revision.swhid;
revSWHIDWithContext = swhids.revision.swhid_with_context;
relSWHID = swhids.release.swhid;
relSWHIDWithContext = swhids.release.swhid_with_context;
snpSWHID = swhids.snapshot.swhid;
snpSWHIDWithContext = swhids.snapshot.swhid_with_context;
testsData = [
{
'objectType': 'content',
'objectSWHIDs': [cntSWHIDWithContext, cntSWHID],
'badgeUrl': this.Urls.swh_badge('content', swhids.content.object_id),
'badgeSWHIDUrl': this.Urls.swh_badge_swhid(cntSWHID),
'browseUrl': this.Urls.browse_swhid(cntSWHIDWithContext)
},
{
'objectType': 'directory',
'objectSWHIDs': [dirSWHIDWithContext, dirSWHID],
'badgeUrl': this.Urls.swh_badge('directory', swhids.directory.object_id),
'badgeSWHIDUrl': this.Urls.swh_badge_swhid(dirSWHID),
'browseUrl': this.Urls.browse_swhid(dirSWHIDWithContext)
},
{
'objectType': 'release',
'objectSWHIDs': [relSWHIDWithContext, relSWHID],
'badgeUrl': this.Urls.swh_badge('release', swhids.release.object_id),
'badgeSWHIDUrl': this.Urls.swh_badge_swhid(relSWHID),
'browseUrl': this.Urls.browse_swhid(relSWHIDWithContext)
},
{
'objectType': 'revision',
'objectSWHIDs': [revSWHIDWithContext, revSWHID],
'badgeUrl': this.Urls.swh_badge('revision', swhids.revision.object_id),
'badgeSWHIDUrl': this.Urls.swh_badge_swhid(revSWHID),
'browseUrl': this.Urls.browse_swhid(revSWHIDWithContext)
},
{
'objectType': 'snapshot',
'objectSWHIDs': [snpSWHIDWithContext, snpSWHID],
'badgeUrl': this.Urls.swh_badge('snapshot', swhids.snapshot.object_id),
'badgeSWHIDUrl': this.Urls.swh_badge_swhid(snpSWHID),
'browseUrl': this.Urls.browse_swhid(snpSWHIDWithContext)
}
];
});
});
beforeEach(function() {
cy.visit(url);
});
it('should open and close identifiers tab when clicking on handle', function() {
cy.get('#swh-identifiers')
.should('have.class', 'ui-slideouttab-ready');
cy.get('.ui-slideouttab-handle')
.click();
cy.get('#swh-identifiers')
.should('have.class', 'ui-slideouttab-open');
cy.get('.ui-slideouttab-handle')
.click();
cy.get('#swh-identifiers')
.should('not.have.class', 'ui-slideouttab-open');
});
it('should display identifiers with permalinks for browsed objects', function() {
cy.get('.ui-slideouttab-handle')
.click();
- for (let td of testsData) {
+ for (const td of testsData) {
cy.get(`a[href="#swhid-tab-${td.objectType}"]`)
.click();
cy.get(`#swhid-tab-${td.objectType}`)
.should('be.visible');
cy.get(`#swhid-tab-${td.objectType} .swhid`)
.should('have.text', td.objectSWHIDs[0].replace(/;/g, ';\n'))
.should('have.attr', 'href', this.Urls.browse_swhid(td.objectSWHIDs[0]));
}
});
it('should update other object identifiers contextual info when toggling context checkbox', function() {
cy.get('.ui-slideouttab-handle')
.click();
- for (let td of testsData) {
+ for (const td of testsData) {
cy.get(`a[href="#swhid-tab-${td.objectType}"]`)
.click();
cy.get(`#swhid-tab-${td.objectType} .swhid`)
.should('have.text', td.objectSWHIDs[0].replace(/;/g, ';\n'))
.should('have.attr', 'href', this.Urls.browse_swhid(td.objectSWHIDs[0]));
cy.get(`#swhid-tab-${td.objectType} .swhid-option`)
.click();
cy.get(`#swhid-tab-${td.objectType} .swhid`)
.contains(td.objectSWHIDs[1])
.should('have.attr', 'href', this.Urls.browse_swhid(td.objectSWHIDs[1]));
cy.get(`#swhid-tab-${td.objectType} .swhid-option`)
.click();
cy.get(`#swhid-tab-${td.objectType} .swhid`)
.should('have.text', td.objectSWHIDs[0].replace(/;/g, ';\n'))
.should('have.attr', 'href', this.Urls.browse_swhid(td.objectSWHIDs[0]));
}
});
it('should display swh badges in identifiers tab for browsed objects', function() {
cy.get('.ui-slideouttab-handle')
.click();
const originBadgeUrl = this.Urls.swh_badge('origin', origin.url);
- for (let td of testsData) {
+ for (const td of testsData) {
cy.get(`a[href="#swhid-tab-${td.objectType}"]`)
.click();
cy.get(`#swhid-tab-${td.objectType} .swh-badge-origin`)
.should('have.attr', 'src', originBadgeUrl);
cy.get(`#swhid-tab-${td.objectType} .swh-badge-${td.objectType}`)
.should('have.attr', 'src', td.badgeUrl);
}
});
it('should display badge integration info when clicking on it', function() {
cy.get('.ui-slideouttab-handle')
.click();
- for (let td of testsData) {
+ for (const td of testsData) {
cy.get(`a[href="#swhid-tab-${td.objectType}"]`)
.click();
cy.get(`#swhid-tab-${td.objectType} .swh-badge-origin`)
.click()
.wait(500);
- for (let badgeType of ['html', 'md', 'rst']) {
+ for (const badgeType of ['html', 'md', 'rst']) {
cy.get(`.modal .swh-badge-${badgeType}`)
.contains(`${urlPrefix}${originBrowseUrl}`)
.contains(`${urlPrefix}${originBadgeUrl}`);
}
cy.get('.modal.show .close')
.click()
.wait(500);
cy.get(`#swhid-tab-${td.objectType} .swh-badge-${td.objectType}`)
.click()
.wait(500);
- for (let badgeType of ['html', 'md', 'rst']) {
+ for (const badgeType of ['html', 'md', 'rst']) {
cy.get(`.modal .swh-badge-${badgeType}`)
.contains(`${urlPrefix}${td.browseUrl}`)
.contains(`${urlPrefix}${td.badgeSWHIDUrl}`);
}
cy.get('.modal.show .close')
.click()
.wait(500);
}
});
it('should be possible to retrieve SWHIDs context from JavaScript', function() {
cy.window().then(win => {
const swhIdsContext = win.swh.webapp.getSwhIdsContext();
- for (let testData of testsData) {
+ for (const testData of testsData) {
assert.isTrue(swhIdsContext.hasOwnProperty(testData.objectType));
assert.equal(swhIdsContext[testData.objectType].swhid,
testData.objectSWHIDs.slice(-1)[0]);
}
});
});
});
diff --git a/cypress/integration/revision-diff.spec.js b/cypress/integration/revision-diff.spec.js
index 12f45f0c..13852d31 100644
--- a/cypress/integration/revision-diff.spec.js
+++ b/cypress/integration/revision-diff.spec.js
@@ -1,492 +1,492 @@
/**
* Copyright (C) 2019-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
*/
const $ = Cypress.$;
const origin = 'https://github.com/memononen/libtess2';
const revision = '98c65dad5e47ad888032b6cdf556f192e0e028d0';
const diffsHighlightingData = {
'unified': {
diffId: '3d4c0797cf0e89430410e088339aac384dfa4d82',
startLines: [913, 915],
endLines: [0, 979]
},
'split-from': {
diffId: '602cec77c3d3f41d396d9e1083a0bbce0796b505',
startLines: [192, 0],
endLines: [198, 0]
},
'split-to': {
diffId: '602cec77c3d3f41d396d9e1083a0bbce0796b505',
startLines: [0, 120],
endLines: [0, 130]
},
'split-from-top-to-bottom': {
diffId: 'a00c33990655a93aa2c821c4008bbddda812a896',
startLines: [63, 0],
endLines: [0, 68]
},
'split-to-top-from-bottom': {
diffId: 'a00c33990655a93aa2c821c4008bbddda812a896',
startLines: [0, 63],
endLines: [67, 0]
}
};
let diffData;
let swh;
describe('Test Revision View', function() {
it('should add/remove #swh-revision-changes url fragment when switching tab', function() {
const url = this.Urls.browse_revision(revision) + `?origin=${origin}`;
cy.visit(url);
cy.get('a[data-toggle="tab"]')
.contains('Changes')
.click();
cy.hash().should('be.equal', '#swh-revision-changes');
cy.get('a[data-toggle="tab"]')
.contains('Files')
.click();
cy.hash().should('be.equal', '');
});
it('should display Changes tab by default when url ends with #swh-revision-changes', function() {
const url = this.Urls.browse_revision(revision) + `?origin=${origin}`;
cy.visit(url + '#swh-revision-changes');
cy.get('#swh-revision-changes-list')
.should('be.visible');
});
});
describe('Test Diffs View', function() {
beforeEach(function() {
const url = this.Urls.browse_revision(revision) + `?origin=${origin}`;
cy.visit(url);
cy.window().then(win => {
swh = win.swh;
cy.request(win.diffRevUrl)
.then(res => {
diffData = res.body;
});
});
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) {
+ const files = new Set([]);
+ for (const change of diffData.changes) {
files.add(change.from_path);
files.add(change.to_path);
}
- for (let file of files) {
+ for (const 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) {
+ for (const change of diffData.changes) {
cy.get(`#swh-revision-changes-list a[href="#diff_${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) {
+ for (const change of diffData.changes) {
cy.get(`#${change.id}-unified-diff`)
.should('be.visible');
cy.get(`#${change.id}-split-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(`#diff_${id}`)
.contains('label', 'Side-by-side')
.click();
cy.get(`#${id}-split-diff`)
.should('be.visible')
.get(`#${id}-unified-diff`)
.should('not.be.visible');
});
function checkDiffHighlighted(diffId, start, end) {
cy.get(`#${diffId} .hljs-ln-line`)
.then(lines => {
let inHighlightedRange = false;
- for (let line of lines) {
+ for (const line of lines) {
const lnNumber = $(line).data('line-number');
if (lnNumber === start || lnNumber === end) {
inHighlightedRange = true;
}
const backgroundColor = $(line).css('background-color');
const mixBlendMode = $(line).css('mix-blend-mode');
if (inHighlightedRange && parseInt(lnNumber)) {
assert.equal(mixBlendMode, 'multiply');
assert.notEqual(backgroundColor, 'rgba(0, 0, 0, 0)');
} else {
assert.equal(mixBlendMode, 'normal');
assert.equal(backgroundColor, 'rgba(0, 0, 0, 0)');
}
if (lnNumber === end) {
inHighlightedRange = false;
}
}
});
}
function unifiedDiffHighlightingTest(diffId, startLines, endLines) {
// render diff
cy.get(`#diff_${diffId}`)
.scrollIntoView()
.get(`#${diffId}-unified-diff`)
.should('be.visible')
// ensure all asynchronous treatments in the page have been performed
// before testing diff highlighting
.then(() => {
let startLinesStr = swh.revision.formatDiffLineNumbers(diffId, startLines[0], startLines[1]);
let endLinesStr = swh.revision.formatDiffLineNumbers(diffId, endLines[0], endLines[1]);
// highlight a range of lines
- let startElt = `#${diffId}-unified-diff .hljs-ln-numbers[data-line-number="${startLinesStr}"]`;
- let endElt = `#${diffId}-unified-diff .hljs-ln-numbers[data-line-number="${endLinesStr}"]`;
+ const startElt = `#${diffId}-unified-diff .hljs-ln-numbers[data-line-number="${startLinesStr}"]`;
+ const endElt = `#${diffId}-unified-diff .hljs-ln-numbers[data-line-number="${endLinesStr}"]`;
cy.get(startElt).click();
cy.get(endElt).click({shiftKey: true});
// check URL fragment has been updated
const selectedLinesFragment =
swh.revision.selectedDiffLinesToFragment(startLines, endLines, true);
cy.hash().should('be.equal', `#diff_${diffId}+${selectedLinesFragment}`);
if ($(endElt).position().top < $(startElt).position().top) {
[startLinesStr, endLinesStr] = [endLinesStr, startLinesStr];
}
// check lines range is highlighted
checkDiffHighlighted(`${diffId}-unified-diff`, startLinesStr, endLinesStr);
// check selected diff lines get highlighted when reloading page
// with highlighting info in URL fragment
cy.reload();
cy.get(`#diff_${diffId}`)
.get(`#${diffId}-unified-diff`)
.should('be.visible');
checkDiffHighlighted(`${diffId}-unified-diff`, startLinesStr, endLinesStr);
});
}
it('should highlight unified diff lines when selecting them from top to bottom', function() {
const diffHighlightingData = diffsHighlightingData['unified'];
const diffId = diffHighlightingData.diffId;
- let startLines = diffHighlightingData.startLines;
- let endLines = diffHighlightingData.endLines;
+ const startLines = diffHighlightingData.startLines;
+ const endLines = diffHighlightingData.endLines;
unifiedDiffHighlightingTest(diffId, startLines, endLines);
});
it('should highlight unified diff lines when selecting them from bottom to top', function() {
const diffHighlightingData = diffsHighlightingData['unified'];
const diffId = diffHighlightingData.diffId;
- let startLines = diffHighlightingData.startLines;
- let endLines = diffHighlightingData.endLines;
+ const startLines = diffHighlightingData.startLines;
+ const endLines = diffHighlightingData.endLines;
unifiedDiffHighlightingTest(diffId, endLines, startLines);
});
function singleSpitDiffHighlightingTest(diffId, startLines, endLines, to) {
let singleDiffId = `${diffId}-from`;
if (to) {
singleDiffId = `${diffId}-to`;
}
let startLine = startLines[0] || startLines[1];
let endLine = endLines[0] || endLines[1];
// render diff
cy.get(`#diff_${diffId}`)
.scrollIntoView()
.get(`#${diffId}-unified-diff`)
.should('be.visible');
cy.get(`#diff_${diffId}`)
.contains('label', 'Side-by-side')
.click()
// ensure all asynchronous treatments in the page have been performed
// before testing diff highlighting
.then(() => {
// highlight a range of lines
- let startElt = `#${singleDiffId} .hljs-ln-numbers[data-line-number="${startLine}"]`;
- let endElt = `#${singleDiffId} .hljs-ln-numbers[data-line-number="${endLine}"]`;
+ const startElt = `#${singleDiffId} .hljs-ln-numbers[data-line-number="${startLine}"]`;
+ const endElt = `#${singleDiffId} .hljs-ln-numbers[data-line-number="${endLine}"]`;
cy.get(startElt).click();
cy.get(endElt).click({shiftKey: true});
const selectedLinesFragment =
swh.revision.selectedDiffLinesToFragment(startLines, endLines, false);
// check URL fragment has been updated
cy.hash().should('be.equal', `#diff_${diffId}+${selectedLinesFragment}`);
if ($(endElt).position().top < $(startElt).position().top) {
[startLine, endLine] = [endLine, startLine];
}
// check lines range is highlighted
checkDiffHighlighted(`${singleDiffId}`, startLine, endLine);
// check selected diff lines get highlighted when reloading page
// with highlighting info in URL fragment
cy.reload();
cy.get(`#diff_${diffId}`)
.get(`#${diffId}-split-diff`)
.get(`#${singleDiffId}`)
.should('be.visible');
checkDiffHighlighted(`${singleDiffId}`, startLine, endLine);
});
}
it('should highlight split diff from lines when selecting them from top to bottom', function() {
const diffHighlightingData = diffsHighlightingData['split-from'];
const diffId = diffHighlightingData.diffId;
- let startLines = diffHighlightingData.startLines;
- let endLines = diffHighlightingData.endLines;
+ const startLines = diffHighlightingData.startLines;
+ const endLines = diffHighlightingData.endLines;
singleSpitDiffHighlightingTest(diffId, startLines, endLines, false);
});
it('should highlight split diff from lines when selecting them from bottom to top', function() {
const diffHighlightingData = diffsHighlightingData['split-from'];
const diffId = diffHighlightingData.diffId;
- let startLines = diffHighlightingData.startLines;
- let endLines = diffHighlightingData.endLines;
+ const startLines = diffHighlightingData.startLines;
+ const endLines = diffHighlightingData.endLines;
singleSpitDiffHighlightingTest(diffId, endLines, startLines, false);
});
it('should highlight split diff to lines when selecting them from top to bottom', function() {
const diffHighlightingData = diffsHighlightingData['split-to'];
const diffId = diffHighlightingData.diffId;
- let startLines = diffHighlightingData.startLines;
- let endLines = diffHighlightingData.endLines;
+ const startLines = diffHighlightingData.startLines;
+ const endLines = diffHighlightingData.endLines;
singleSpitDiffHighlightingTest(diffId, startLines, endLines, true);
});
it('should highlight split diff to lines when selecting them from bottom to top', function() {
const diffHighlightingData = diffsHighlightingData['split-to'];
const diffId = diffHighlightingData.diffId;
- let startLines = diffHighlightingData.startLines;
- let endLines = diffHighlightingData.endLines;
+ const startLines = diffHighlightingData.startLines;
+ const endLines = diffHighlightingData.endLines;
singleSpitDiffHighlightingTest(diffId, endLines, startLines, true);
});
function checkSplitDiffHighlighted(diffId, startLines, endLines) {
let left, right;
if (startLines[0] && endLines[1]) {
left = startLines[0];
right = endLines[1];
} else {
left = endLines[0];
right = startLines[1];
}
cy.get(`#${diffId}-from .hljs-ln-line`)
.then(fromLines => {
cy.get(`#${diffId}-to .hljs-ln-line`)
.then(toLines => {
const leftLine = $(`#${diffId}-from .hljs-ln-line[data-line-number="${left}"]`);
const rightLine = $(`#${diffId}-to .hljs-ln-line[data-line-number="${right}"]`);
const leftLineAbove = $(leftLine).position().top < $(rightLine).position().top;
let inHighlightedRange = false;
for (let i = 0; i < Math.max(fromLines.length, toLines.length); ++i) {
const fromLn = fromLines[i];
const toLn = toLines[i];
const fromLnNumber = $(fromLn).data('line-number');
const toLnNumber = $(toLn).data('line-number');
if ((leftLineAbove && fromLnNumber === left) ||
(!leftLineAbove && toLnNumber === right) ||
(leftLineAbove && toLnNumber === right) ||
(!leftLineAbove && fromLnNumber === left)) {
inHighlightedRange = true;
}
if (fromLn) {
const fromBackgroundColor = $(fromLn).css('background-color');
const fromMixBlendMode = $(fromLn).css('mix-blend-mode');
if (inHighlightedRange && fromLnNumber) {
assert.equal(fromMixBlendMode, 'multiply');
assert.notEqual(fromBackgroundColor, 'rgba(0, 0, 0, 0)');
} else {
assert.equal(fromMixBlendMode, 'normal');
assert.equal(fromBackgroundColor, 'rgba(0, 0, 0, 0)');
}
}
if (toLn) {
const toBackgroundColor = $(toLn).css('background-color');
const toMixBlendMode = $(toLn).css('mix-blend-mode');
if (inHighlightedRange && toLnNumber) {
assert.equal(toMixBlendMode, 'multiply');
assert.notEqual(toBackgroundColor, 'rgba(0, 0, 0, 0)');
} else {
assert.equal(toMixBlendMode, 'normal');
assert.equal(toBackgroundColor, 'rgba(0, 0, 0, 0)');
}
}
if ((leftLineAbove && toLnNumber === right) ||
(!leftLineAbove && fromLnNumber === left)) {
inHighlightedRange = false;
}
}
});
});
}
function splitDiffHighlightingTest(diffId, startLines, endLines) {
// render diff
cy.get(`#diff_${diffId}`)
.scrollIntoView()
.find(`#${diffId}-unified-diff`)
.should('be.visible');
cy.get(`#diff_${diffId}`)
.contains('label', 'Side-by-side')
.click()
// ensure all asynchronous treatments in the page have been performed
// before testing diff highlighting
.then(() => {
// select lines range in diff
let startElt;
if (startLines[0]) {
startElt = `#${diffId}-from .hljs-ln-numbers[data-line-number="${startLines[0]}"]`;
} else {
startElt = `#${diffId}-to .hljs-ln-numbers[data-line-number="${startLines[1]}"]`;
}
let endElt;
if (endLines[0]) {
endElt = `#${diffId}-from .hljs-ln-numbers[data-line-number="${endLines[0]}"]`;
} else {
endElt = `#${diffId}-to .hljs-ln-numbers[data-line-number="${endLines[1]}"]`;
}
cy.get(startElt).click();
cy.get(endElt).click({shiftKey: true});
const selectedLinesFragment =
swh.revision.selectedDiffLinesToFragment(startLines, endLines, false);
// check URL fragment has been updated
cy.hash().should('be.equal', `#diff_${diffId}+${selectedLinesFragment}`);
// check lines range is highlighted
checkSplitDiffHighlighted(diffId, startLines, endLines);
// check selected diff lines get highlighted when reloading page
// with highlighting info in URL fragment
cy.reload();
cy.get(`#diff_${diffId}`)
.get(`#${diffId}-split-diff`)
.get(`#${diffId}-to`)
.should('be.visible');
checkSplitDiffHighlighted(diffId, startLines, endLines);
});
}
it('should highlight split diff from and to lines when selecting them from top-left to bottom-right', function() {
const diffHighlightingData = diffsHighlightingData['split-from-top-to-bottom'];
const diffId = diffHighlightingData.diffId;
- let startLines = diffHighlightingData.startLines;
- let endLines = diffHighlightingData.endLines;
+ const startLines = diffHighlightingData.startLines;
+ const endLines = diffHighlightingData.endLines;
splitDiffHighlightingTest(diffId, startLines, endLines);
});
it('should highlight split diff from and to lines when selecting them from bottom-right to top-left', function() {
const diffHighlightingData = diffsHighlightingData['split-from-top-to-bottom'];
const diffId = diffHighlightingData.diffId;
- let startLines = diffHighlightingData.startLines;
- let endLines = diffHighlightingData.endLines;
+ const startLines = diffHighlightingData.startLines;
+ const endLines = diffHighlightingData.endLines;
splitDiffHighlightingTest(diffId, endLines, startLines);
});
it('should highlight split diff from and to lines when selecting them from top-right to bottom-left', function() {
const diffHighlightingData = diffsHighlightingData['split-to-top-from-bottom'];
const diffId = diffHighlightingData.diffId;
- let startLines = diffHighlightingData.startLines;
- let endLines = diffHighlightingData.endLines;
+ const startLines = diffHighlightingData.startLines;
+ const endLines = diffHighlightingData.endLines;
splitDiffHighlightingTest(diffId, startLines, endLines);
});
it('should highlight split diff from and to lines when selecting them from bottom-left to top-right', function() {
const diffHighlightingData = diffsHighlightingData['split-to-top-from-bottom'];
const diffId = diffHighlightingData.diffId;
- let startLines = diffHighlightingData.startLines;
- let endLines = diffHighlightingData.endLines;
+ const startLines = diffHighlightingData.startLines;
+ const endLines = diffHighlightingData.endLines;
splitDiffHighlightingTest(diffId, endLines, startLines);
});
it('should highlight diff lines properly when a content is browsed in the Files tab', function() {
const url = this.Urls.browse_revision(revision) + `?origin=${origin}&path=README.md`;
cy.visit(url);
cy.get('a[data-toggle="tab"]')
.contains('Changes')
.click();
const diffHighlightingData = diffsHighlightingData['unified'];
const diffId = diffHighlightingData.diffId;
- let startLines = diffHighlightingData.startLines;
- let endLines = diffHighlightingData.endLines;
+ const startLines = diffHighlightingData.startLines;
+ const endLines = diffHighlightingData.endLines;
unifiedDiffHighlightingTest(diffId, startLines, endLines);
});
});
diff --git a/cypress/integration/vault.spec.js b/cypress/integration/vault.spec.js
index 2cfa97df..d2d6be7c 100644
--- a/cypress/integration/vault.spec.js
+++ b/cypress/integration/vault.spec.js
@@ -1,504 +1,504 @@
/**
* 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
*/
-let vaultItems = [];
+const vaultItems = [];
const progressbarColors = {
'new': 'rgba(128, 128, 128, 0.5)',
'pending': 'rgba(0, 0, 255, 0.5)',
'done': 'rgb(92, 184, 92)'
};
function checkVaultCookingTask(objectType) {
cy.contains('button', 'Download')
.click();
cy.contains('.dropdown-item', objectType)
.click();
cy.wait('@checkVaultCookingTask');
}
function updateVaultItemList(vaultUrl, vaultItems) {
cy.visit(vaultUrl)
.then(() => {
// Add uncooked task to localStorage
// which updates it in vault items list
window.localStorage.setItem('swh-vault-cooking-tasks', JSON.stringify(vaultItems));
});
}
// 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() {
const dirInfo = this.origin[0].directory[0];
this.directory = dirInfo.id;
this.directoryUrl = this.Urls.browse_origin_directory() +
`?origin_url=${this.origin[0].url}&path=${dirInfo.path}`;
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);
const release = this.origin[1].release;
this.releaseUrl = this.Urls.browse_release(release.id) + `?origin_url=${this.origin[1].url}`;
this.vaultReleaseDirectoryUrl = this.Urls.api_1_vault_cook_directory(release.directory);
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.genVaultRevCookingResponse = (status, message = null) => {
return genVaultCookingResponse('revision', this.revision, status,
message, this.vaultFetchRevisionUrl);
};
});
it('should report an error when vault service is experiencing issues', function() {
// Browse a directory
cy.visit(this.directoryUrl);
// Stub responses when requesting the vault API to simulate
// an internal server error
cy.intercept(this.vaultDirectoryUrl, {
body: {'exception': 'APIError'},
statusCode: 500
}).as('checkVaultCookingTask');
cy.contains('button', 'Download')
.click();
// Check error alert is displayed
cy.get('.alert-danger')
.should('be.visible')
.should('contain', 'Archive cooking service is currently experiencing issues.');
});
it('should report an error when a cooking task creation failed', function() {
// Browse a directory
cy.visit(this.directoryUrl);
// Stub responses when requesting the vault API to simulate
// a task can not be created
cy.intercept('GET', this.vaultDirectoryUrl, {
body: {'exception': 'NotFoundExc'}
}).as('checkVaultCookingTask');
cy.intercept('POST', this.vaultDirectoryUrl, {
body: {'exception': 'ValueError'},
statusCode: 500
}).as('createVaultCookingTask');
cy.contains('button', 'Download')
.click();
// Create a vault cooking task through the GUI
cy.get('.modal-dialog')
.contains('button:visible', 'Ok')
.click();
cy.wait('@createVaultCookingTask');
// Check error alert is displayed
cy.get('.alert-danger')
.should('be.visible')
.should('contain', 'Archive cooking request submission failed.');
});
it('should create a directory cooking task and report the success', function() {
// Browse a directory
cy.visit(this.directoryUrl);
// Stub response to the vault API to simulate archive download
cy.intercept('GET', this.vaultFetchDirectoryUrl, {
fixture: `${this.directory}.tar.gz`,
headers: {
'Content-disposition': `attachment; filename=${this.directory}.tar.gz`,
'Content-Type': 'application/gzip'
}
}).as('fetchCookedArchive');
// Stub responses when checking vault task status
const checkVaulResponses = [
{'exception': 'NotFoundExc'},
this.genVaultDirCookingResponse('new'),
this.genVaultDirCookingResponse('pending', 'Processing...'),
this.genVaultDirCookingResponse('done')
];
// trick to override the response of an intercepted request
// https://github.com/cypress-io/cypress/issues/9302
cy.intercept('GET', this.vaultDirectoryUrl, req => req.reply(checkVaulResponses.shift()))
.as('checkVaultCookingTask');
// Stub responses when requesting the vault API to simulate
// a task has been created
cy.intercept('POST', this.vaultDirectoryUrl, {
body: this.genVaultDirCookingResponse('new')
}).as('createVaultCookingTask');
cy.contains('button', 'Download')
.click();
cy.window().then(win => {
const swhIdsContext = win.swh.webapp.getSwhIdsContext();
const browseDirectoryUrl = swhIdsContext.directory.swhid_with_context_url;
// Create a vault cooking task through the GUI
cy.get('.modal-dialog')
.contains('button:visible', 'Ok')
.click();
cy.wait('@createVaultCookingTask');
// Check success alert is displayed
cy.get('.alert-success')
.should('be.visible')
.should('contain', 'Archive cooking request successfully submitted.');
// Go to Downloads page
cy.visit(this.Urls.browse_vault());
cy.wait('@checkVaultCookingTask').then(() => {
testStatus(this.directory, progressbarColors['new'], 'new', 'new');
});
cy.wait('@checkVaultCookingTask').then(() => {
testStatus(this.directory, progressbarColors['pending'], 'Processing...', 'pending');
});
cy.wait('@checkVaultCookingTask').then(() => {
testStatus(this.directory, progressbarColors['done'], 'done', 'done');
});
cy.get(`#vault-task-${this.directory} .vault-origin a`)
.should('contain', this.origin[0].url)
.should('have.attr', 'href', `${this.Urls.browse_origin()}?origin_url=${this.origin[0].url}`);
cy.get(`#vault-task-${this.directory} .vault-object-info a`)
.should('have.text', this.directory)
.should('have.attr', 'href', browseDirectoryUrl);
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() {
cy.adminLogin();
// Browse a revision
cy.visit(this.revisionUrl);
// Stub response to the vault API indicating to simulate archive download
cy.intercept({url: this.vaultFetchRevisionUrl}, {
fixture: `${this.revision}.gitfast.gz`,
headers: {
'Content-disposition': `attachment; filename=${this.revision}.gitfast.gz`,
'Content-Type': 'application/gzip'
}
}).as('fetchCookedArchive');
// Stub responses when checking vault task status
const checkVaultResponses = [
{'exception': 'NotFoundExc'},
this.genVaultRevCookingResponse('new'),
this.genVaultRevCookingResponse('pending', 'Processing...'),
this.genVaultRevCookingResponse('done')
];
// trick to override the response of an intercepted request
// https://github.com/cypress-io/cypress/issues/9302
cy.intercept('GET', this.vaultRevisionUrl, req => req.reply(checkVaultResponses.shift()))
.as('checkVaultCookingTask');
// Stub responses when requesting the vault API to simulate
// a task has been created
cy.intercept('POST', this.vaultRevisionUrl, {
body: this.genVaultRevCookingResponse('new')
}).as('createVaultCookingTask');
// Create a vault cooking task through the GUI
checkVaultCookingTask('as git');
cy.window().then(win => {
const swhIdsContext = win.swh.webapp.getSwhIdsContext();
const browseRevisionUrl = swhIdsContext.revision.swhid_url;
// Create a vault cooking task through the GUI
cy.get('.modal-dialog')
.contains('button:visible', 'Ok')
.click();
cy.wait('@createVaultCookingTask');
// Check success alert is displayed
cy.get('.alert-success')
.should('be.visible')
.should('contain', 'Archive cooking request successfully submitted.');
// Go to Downloads page
cy.visit(this.Urls.browse_vault());
cy.wait('@checkVaultCookingTask').then(() => {
testStatus(this.revision, progressbarColors['new'], 'new', 'new');
});
cy.wait('@checkVaultCookingTask').then(() => {
testStatus(this.revision, progressbarColors['pending'], 'Processing...', 'pending');
});
cy.wait('@checkVaultCookingTask').then(() => {
testStatus(this.revision, progressbarColors['done'], 'done', 'done');
});
cy.get(`#vault-task-${this.revision} .vault-origin`)
.should('have.text', 'unknown');
cy.get(`#vault-task-${this.revision} .vault-object-info a`)
.should('have.text', this.revision)
.should('have.attr', 'href', browseRevisionUrl);
cy.get(`#vault-task-${this.revision} .vault-dl-link button`)
.click();
cy.wait('@fetchCookedArchive').then((xhr) => {
assert.isNotNull(xhr.response.body);
});
});
});
it('should create a directory cooking task from the release view', function() {
// Browse a directory
cy.visit(this.releaseUrl);
// Stub responses when checking vault task status
const checkVaultResponses = [
{'exception': 'NotFoundExc'},
this.genVaultDirCookingResponse('new')
];
// trick to override the response of an intercepted request
// https://github.com/cypress-io/cypress/issues/9302
cy.intercept('GET', this.vaultReleaseDirectoryUrl, req => req.reply(checkVaultResponses.shift()))
.as('checkVaultCookingTask');
// Stub responses when requesting the vault API to simulate
// a task has been created
cy.intercept('POST', this.vaultReleaseDirectoryUrl, {
body: this.genVaultDirCookingResponse('new')
}).as('createVaultCookingTask');
cy.contains('button', 'Download')
.click();
// Create a vault cooking task through the GUI
cy.get('.modal-dialog')
.contains('button:visible', 'Ok')
.click();
cy.wait('@createVaultCookingTask');
// Check success alert is displayed
cy.get('.alert-success')
.should('be.visible')
.should('contain', 'Archive cooking request successfully submitted.');
});
it('should offer to recook an archive if no more available to download', function() {
updateVaultItemList(this.Urls.browse_vault(), vaultItems);
// Send 404 when fetching vault item
cy.intercept({url: this.vaultFetchRevisionUrl}, {
statusCode: 404,
body: {
'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.intercept('POST', this.vaultRevisionUrl, {
body: this.genVaultRevCookingResponse('new')
}).as('createVaultCookingTask');
cy.intercept(this.vaultRevisionUrl, {
body: this.genVaultRevCookingResponse('new')
}).as('checkVaultCookingTask');
cy.get('#vault-recook-object-modal > .modal-dialog')
.should('be.visible')
.contains('button:visible', 'Ok')
.click();
cy.wait('@checkVaultCookingTask')
.then(() => {
testStatus(this.revision, progressbarColors['new'], 'new', 'new');
});
});
});
it('should remove selected vault items', function() {
updateVaultItemList(this.Urls.browse_vault(), vaultItems);
cy.get(`#vault-task-${this.revision}`)
.find('input[type="checkbox"]')
.click({force: true});
cy.contains('button', 'Remove selected tasks')
.click();
cy.get(`#vault-task-${this.revision}`)
.should('not.exist');
});
it('should offer to immediately download a directory tarball if already cooked', function() {
// Browse a directory
cy.visit(this.directoryUrl);
// Stub response to the vault API to simulate archive download
cy.intercept({url: this.vaultFetchDirectoryUrl}, {
fixture: `${this.directory}.tar.gz`,
headers: {
'Content-disposition': `attachment; filename=${this.directory}.tar.gz`,
'Content-Type': 'application/gzip'
}
}).as('fetchCookedArchive');
// Stub responses when requesting the vault API to simulate
// the directory tarball has already been cooked
cy.intercept(this.vaultDirectoryUrl, {
body: this.genVaultDirCookingResponse('done')
}).as('checkVaultCookingTask');
// Create a vault cooking task through the GUI
cy.contains('button', 'Download')
.click();
// Start archive download through the GUI
cy.get('.modal-dialog')
.contains('button:visible', 'Ok')
.click();
cy.wait('@fetchCookedArchive');
});
it('should offer to immediately download a revision gitfast archive if already cooked', function() {
cy.adminLogin();
// Browse a directory
cy.visit(this.revisionUrl);
// Stub response to the vault API to simulate archive download
cy.intercept({url: this.vaultFetchRevisionUrl}, {
fixture: `${this.revision}.gitfast.gz`,
headers: {
'Content-disposition': `attachment; filename=${this.revision}.gitfast.gz`,
'Content-Type': 'application/gzip'
}
}).as('fetchCookedArchive');
// Stub responses when requesting the vault API to simulate
// the directory tarball has already been cooked
cy.intercept(this.vaultRevisionUrl, {
body: this.genVaultRevCookingResponse('done')
}).as('checkVaultCookingTask');
checkVaultCookingTask('as git');
// Start archive download through the GUI
cy.get('.modal-dialog')
.contains('button:visible', 'Ok')
.click();
cy.wait('@fetchCookedArchive');
});
it('should offer to recook an object if previous vault task failed', function() {
cy.visit(this.directoryUrl);
// Stub responses when requesting the vault API to simulate
// the last cooking of the directory tarball has failed
cy.intercept(this.vaultDirectoryUrl, {
body: this.genVaultDirCookingResponse('failed')
}).as('checkVaultCookingTask');
cy.contains('button', 'Download')
.click();
// Check that recooking the directory is offered to user
cy.get('.modal-dialog')
.contains('button:visible', 'Ok')
.should('be.visible');
});
});
diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js
index 71ce678a..f21d0d0f 100644
--- a/cypress/plugins/index.js
+++ b/cypress/plugins/index.js
@@ -1,27 +1,27 @@
/**
* 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 axios = require('axios');
const fs = require('fs');
module.exports = (on, config) => {
require('@cypress/code-coverage/task')(on, config);
// produce JSON files prior launching browser in order to dynamically generate tests
on('before:browser:launch', function(browser, launchOptions) {
return new Promise((resolve) => {
- let p1 = axios.get(`${config.baseUrl}/tests/data/content/code/extensions/`);
- let p2 = axios.get(`${config.baseUrl}/tests/data/content/code/filenames/`);
+ const p1 = axios.get(`${config.baseUrl}/tests/data/content/code/extensions/`);
+ const p2 = axios.get(`${config.baseUrl}/tests/data/content/code/filenames/`);
Promise.all([p1, p2])
.then(function(responses) {
fs.writeFileSync('cypress/fixtures/source-file-extensions.json', JSON.stringify(responses[0].data));
fs.writeFileSync('cypress/fixtures/source-file-names.json', JSON.stringify(responses[1].data));
resolve();
});
});
});
return config;
};
diff --git a/cypress/support/index.js b/cypress/support/index.js
index f5974994..58b45488 100644
--- a/cypress/support/index.js
+++ b/cypress/support/index.js
@@ -1,158 +1,158 @@
/**
* Copyright (C) 2019-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
*/
import 'cypress-hmr-restarter';
import '@cypress/code-coverage/support';
import {httpGetJson} from '../utils';
Cypress.Screenshot.defaults({
screenshotOnRunFailure: false
});
Cypress.Commands.add('xhrShouldBeCalled', (alias, timesCalled) => {
const testRoutes = cy.state('routes');
const aliasRoute = Cypress._.find(testRoutes, {alias});
expect(Object.keys(aliasRoute.requests || {})).to.have.length(timesCalled);
});
function loginUser(username, password) {
const url = '/admin/login/';
return cy.request({
url: url,
method: 'GET'
}).then(() => {
cy.getCookie('sessionid').should('not.exist');
cy.getCookie('csrftoken').its('value').then((token) => {
cy.request({
url: url,
method: 'POST',
form: true,
followRedirect: false,
body: {
username: username,
password: password,
csrfmiddlewaretoken: token
}
}).then(() => {
cy.getCookie('sessionid').should('exist');
return cy.getCookie('csrftoken').its('value');
});
});
});
}
Cypress.Commands.add('adminLogin', () => {
return loginUser('admin', 'admin');
});
Cypress.Commands.add('userLogin', () => {
return loginUser('user', 'user');
});
Cypress.Commands.add('ambassadorLogin', () => {
return loginUser('ambassador', 'ambassador');
});
before(function() {
this.unarchivedRepo = {
url: 'https://github.com/SoftwareHeritage/swh-web',
type: 'git',
revision: '7bf1b2f489f16253527807baead7957ca9e8adde',
snapshot: 'd9829223095de4bb529790de8ba4e4813e38672d',
rootDirectory: '7d887d96c0047a77e2e8c4ee9bb1528463677663',
content: [{
sha1git: 'b203ec39300e5b7e97b6e20986183cbd0b797859'
}]
};
this.origin = [{
url: 'https://github.com/memononen/libtess2',
type: 'git',
content: [{
path: 'Source/tess.h'
}, {
path: 'premake4.lua'
}],
directory: [{
path: 'Source',
id: 'cd19126d815470b28919d64b2a8e6a3e37f900dd'
}],
revisions: [],
invalidSubDir: 'Source1'
}, {
url: 'https://github.com/wcoder/highlightjs-line-numbers.js',
type: 'git',
content: [{
path: 'src/highlightjs-line-numbers.js'
}],
directory: [],
revisions: ['1c480a4573d2a003fc2630c21c2b25829de49972'],
release: {
name: 'v2.6.0',
id: '6877028d6e5412780517d0bfa81f07f6c51abb41',
directory: '5b61d50ef35ca9a4618a3572bde947b8cccf71ad'
}
}];
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);
let revision = lastOriginSnapshot.branches.HEAD.target;
if (lastOriginSnapshot.branches.HEAD.target_type === 'alias') {
revision = lastOriginSnapshot.branches[revision].target;
}
const revisionApiUrl = this.Urls.api_1_revision(revision);
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) {
+ for (const 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.revisions.push(metadata.revision);
origin.snapshot = metadata.snapshot;
- for (let content of origin.content) {
+ for (const content of origin.content) {
const contentPathApiUrl = this.Urls.api_1_directory(origin.rootDirectory, content.path);
const contentMetaData = await httpGetJson(contentPathApiUrl);
content.name = contentMetaData.name.split('/').slice(-1)[0];
content.sha1git = contentMetaData.target;
content.directory = contentMetaData.dir_id;
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;
});
}
}
});
});