Fix dates in stats export links (csv/json) (#15086)
This commit is contained in:
Родитель
ef9225ff7f
Коммит
1d37fe4122
|
@ -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:
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
4
tox.ini
4
tox.ini
|
@ -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
|
||||
|
|
Загрузка…
Ссылка в новой задаче