Fix dates in stats export links (csv/json) (#15086)

This commit is contained in:
William Durand 2020-07-30 14:00:07 +02:00 коммит произвёл GitHub
Родитель ef9225ff7f
Коммит 1d37fe4122
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
8 изменённых файлов: 279 добавлений и 93 удалений

Просмотреть файл

@ -242,6 +242,13 @@ tdd: ## run the entire test suite, but stop on the first error
test_failed: ## rerun the failed tests from the previous run
pytest --lf $(ARGS) $(APP)
.PHONY: run_js_tests
run_js_tests: ## Run the JavaScript test suite (requires compiled/compressed assets).
NODE_PATH=$(NODE_MODULES) $$(npm bin $(NPM_ARGS))/jest
.PHONY: watch_js_tests
watch_js_tests: ## Run+watch the JavaScript test suite (requires compiled/compressed assets).
NODE_PATH=$(NODE_MODULES) $$(npm bin $(NPM_ARGS))/jest --watch
.PHONY: help_submake
help_submake:

6
jest.config.js Normal file
Просмотреть файл

@ -0,0 +1,6 @@
// For a detailed explanation regarding each configuration property, visit:
// https://jestjs.io/docs/en/configuration.html
module.exports = {
setupFiles: ['<rootDir>/tests/js/setup.js'],
testMatch: ['<rootDir>/tests/js/**/*.spec.js'],
};

Просмотреть файл

@ -23,5 +23,8 @@
"uglify-js": "3.10.0",
"underscore": "1.10.2"
},
"devDependencies": {}
"devDependencies": {
"jest": "^26.1.0",
"jest-date-mock": "^1.0.8"
}
}

Просмотреть файл

@ -29,7 +29,7 @@ function WorkerPool(size) {
worker.addEventListener('message', function(e) {
if (job.cb.call(job.ctx, e.data, worker)) {
worker.terminate();
delete worker;
worker = null;
workers--;
nextJob();
};

Просмотреть файл

@ -1,104 +1,138 @@
(function() {
(function (jQuery, window) {
'use strict';
$(function() {
"use strict";
// `$` is passed by jQuery itself when calling `jQuery(stats_stats)`.
var stats_stats = function ($, injectedSessionStorage) {
var internalSessionStorage =
injectedSessionStorage || window.sessionStorage;
// Modify the URL when the page state changes, if the browser supports pushState.
if (z.capabilities.replaceState) {
$(window).on('changeview', function(e, view) {
var queryParams = {},
range = view.range;
if (range) {
if (typeof range == 'string') {
queryParams.last = range.split(/\s+/)[0];
} else if (typeof range == 'object') {
// queryParams.start = z.date.date_string(new Date(range.start), '');
// queryParams.end = z.date.date_string(new Date(range.end), '');
}
}
queryParams = $.param(queryParams);
if (queryParams) {
history.replaceState(view, document.title, '?' + queryParams);
}
});
// Modify the URL when the page state changes, if the browser supports
// pushState.
if (z.capabilities.replaceState) {
$(window).on('changeview', function (e, view) {
var queryParams = {};
var range = view.range;
if (range) {
if (typeof range == 'string') {
queryParams.last = range.split(/\s+/)[0];
}
}
// Set up initial default view.
var initView = {
metric: $('.primary').attr('data-report'),
range: $('.primary').attr('data-range') || '30 days',
group: 'day'
};
queryParams = $.param(queryParams);
// Set side nav active state.
(function() {
var sel = '#side-nav li.' + initView.metric;
sel += ', #side-nav li[data-report=' + initView.metric + ']';
$(sel).addClass('active');
})();
// Restore any session view information from sessionStorage.
if (z.capabilities.localStorage && sessionStorage.getItem('stats_view')) {
var ssView = JSON.parse(sessionStorage.getItem('stats_view'));
// The stored range is either a string or an object.
if (ssView.range && typeof ssView.range === 'object') {
var objRange = ssView.range;
Object.keys(objRange).forEach(function(key) {
var val = objRange[key];
if (typeof val === 'string') {
objRange[key] = _.escape(val);
}
});
initView.range = objRange;
} else {
initView.range = _.escape(ssView.range || initView.range);
}
initView.group = _.escape(ssView.group || initView.group);
if (queryParams) {
history.replaceState(view, document.title, '?' + queryParams);
}
});
}
// Update sessionStorage with our current view state.
(function() {
if (!z.capabilities.localStorage) return;
var ssView = _.clone(initView);
$(window).on('changeview', function(e, newView) {
_.extend(ssView, newView);
sessionStorage.setItem('stats_view', JSON.stringify({
'range': ssView.range,
'group': ssView.group
}));
});
})();
// Set up initial default view.
var initView = {
metric: $('.primary').attr('data-report'),
range: $('.primary').attr('data-range') || '30 days',
group: 'day',
};
// Update the "Export as CSV" link when the view changes.
(function() {
var view = {},
baseURL = $('.primary').attr('data-base_url');
$(window).on('changeview', function(e, newView) {
_.extend(view, newView);
var metric = view.metric,
range = normalizeRange(view.range),
url = baseURL + ([metric,'day',range.start.pretty(''),range.end.pretty('')]).join('-');
$('#export_data_csv').attr('href', url + '.csv');
$('#export_data_json').attr('href', url + '.json');
});
})();
// Set side nav active state.
(function () {
var sel = '#side-nav li.' + initView.metric;
sel += ', #side-nav li[data-report=' + initView.metric + ']';
// set up notes modal.
$('#stats-note').modal('#stats-note-link', { width: 520 });
$(sel).addClass('active');
})();
// set up stats exception modal.
var $exceptionModal = $('#exception-note').modal('', { width: 250 });
$(window).on('explain-exception', function() {
$exceptionModal.render();
// Restore any session view information from internalSessionStorage.
if (
z.capabilities.localStorage &&
internalSessionStorage.getItem('stats_view')
) {
var ssView = JSON.parse(internalSessionStorage.getItem('stats_view'));
// The stored range is either a string or an object.
if (ssView.range && typeof ssView.range === 'object') {
var objRange = ssView.range;
Object.keys(objRange).forEach(function (key) {
var val = objRange[key];
if (typeof val === 'string') {
objRange[key] = _.escape(val);
}
});
initView.range = objRange;
} else {
initView.range = _.escape(ssView.range || initView.range);
}
$('.csv-table').csvTable();
initView.group = _.escape(ssView.group || initView.group);
}
// Trigger the initial data load.
$(window).trigger('changeview', initView);
// Update internalSessionStorage with our current view state.
(function () {
if (!z.capabilities.localStorage) {
return;
}
var ssView = _.clone(initView);
$(window).on('changeview', function (e, newView) {
_.extend(ssView, newView);
internalSessionStorage.setItem(
'stats_view',
JSON.stringify({
range: ssView.range,
group: ssView.group,
})
);
});
})();
// Update the "Export as CSV" link when the view changes.
(function () {
var view = {},
baseURL = $('.primary').attr('data-base_url');
$(window).on('changeview', function (e, newView) {
_.extend(view, newView);
var metric = view.metric;
var range = normalizeRange(view.range);
// See: https://github.com/mozilla/zamboni/commit/4263102
if (
typeof view.range === 'string' ||
(view.range.custom && typeof range.end === 'object')
) {
range.end = new Date(range.end.getTime() - 24 * 60 * 60 * 1000);
}
var url =
baseURL +
[metric, 'day', range.start.pretty(''), range.end.pretty('')].join(
'-'
);
$('#export_data_csv').attr('href', url + '.csv');
$('#export_data_json').attr('href', url + '.json');
});
})();
// set up notes modal.
$('#stats-note').modal('#stats-note-link', { width: 520 });
// set up stats exception modal.
var $exceptionModal = $('#exception-note').modal('', { width: 250 });
$(window).on('explain-exception', function () {
$exceptionModal.render();
});
})();
$('.csv-table').csvTable();
// Trigger the initial data load.
$(window).trigger('changeview', initView);
};
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
module.exports.stats_stats = stats_stats;
} else {
jQuery(stats_stats);
}
})(jQuery, window);

9
tests/js/setup.js Normal file
Просмотреть файл

@ -0,0 +1,9 @@
require('jest-date-mock');
// Those objects are available globally in the JS source files.
global.$ = global.jQuery = require('jquery');
global._ = require('lodash');
// This helper is also available globally. We create a naive implementation for
// testing purposes.
global.gettext = (str) => str;

Просмотреть файл

@ -0,0 +1,127 @@
const dateMock = require('jest-date-mock');
describe(__filename, () => {
const defaultBaseUrl = 'http://example.org/';
// This should be global to all stats files.
beforeEach(() => {
// Mock mandatory jQuery plugins.
$.prototype.modal = jest.fn();
$.prototype.csvTable = jest.fn();
$.prototype.datepicker = () => ({ datepicker: jest.fn() });
$.datepicker = { setDefaults: jest.fn() };
global.z = {
SessionStorage: jest.fn(),
Storage: jest.fn(),
capabilities: {},
};
global._pd = (func) => {
return function (e) {
e.preventDefault();
func.apply(this, arguments);
};
};
});
afterEach(() => {
dateMock.clear();
});
describe('stats/stats.js', () => {
const report = 'apps';
let stats_stats;
beforeEach(() => {
stats_stats = require('../../../static/js/zamboni/stats-all.js')
.stats_stats;
});
describe('export links', () => {
const createMinimalHTML = ({
baseUrl = defaultBaseUrl,
range = '',
report = 'some-report',
}) => {
return `
<div
class="primary"
data-report="${report}"
data-base_url="${baseUrl}"
data-range="${range}"
>
<a href="" id="export_data_csv">export csv</a>
<a href="" id="export_data_json">export json</a>
</div>`;
};
beforeEach(() => {
const date = new Date(2019, 10 - 1, 14);
dateMock.advanceTo(date);
});
it('constructs the export URLs for the last 7 days', () => {
const report = 'apps';
document.body.innerHTML = createMinimalHTML({
range: 'last 7 days',
report,
});
stats_stats(global.$);
expect($('#export_data_csv').attr('href')).toEqual(
`${defaultBaseUrl}${report}-day-20191007-20191013.csv`
);
expect($('#export_data_json').attr('href')).toEqual(
`${defaultBaseUrl}${report}-day-20191007-20191013.json`
);
});
it('constructs the export URLs for the last 30 days by default', () => {
const report = 'apps';
document.body.innerHTML = createMinimalHTML({ range: '', report });
stats_stats(global.$);
expect($('#export_data_csv').attr('href')).toEqual(
`${defaultBaseUrl}${report}-day-20190914-20191013.csv`
);
expect($('#export_data_json').attr('href')).toEqual(
`${defaultBaseUrl}${report}-day-20190914-20191013.json`
);
});
it('constructs the export URLs for a custom range', () => {
const report = 'countries';
document.body.innerHTML = createMinimalHTML({ report });
// Custom range is persisted in session storage.
global.z.capabilities.localStorage = true;
const statsView = {
group: 'day',
range: {
custom: true,
start: Date.UTC(2019, 11 - 1, 15),
// When loading the page, 1 day is added to the `range.end` date so
// we have to substract it when creating the export links.
end: Date.UTC(2019, 11 - 1, 25 + 1),
},
};
const fakeSessionStorage = {
getItem: () => JSON.stringify(statsView),
setItem: jest.fn(),
};
stats_stats(global.$, fakeSessionStorage);
expect($('#export_data_csv').attr('href')).toEqual(
`${defaultBaseUrl}${report}-day-20191115-20191125.csv`
);
expect($('#export_data_json').attr('href')).toEqual(
`${defaultBaseUrl}${report}-day-20191115-20191125.json`
);
});
});
});
});

Просмотреть файл

@ -65,11 +65,11 @@ commands =
--ignore src/olympia/zadmin \
{posargs}
[testenv:assets]
commands =
make update_deps
make update_deps update_assets
pytest -m "static_assets" -v src/olympia/ {posargs}
make run_js_tests
[testenv:codestyle]
recreate = True