fix tests to work with webpack

run tests through webpack first, then mocha
reorganise webpack config files
This commit is contained in:
Leo McArdle 2021-12-16 14:53:52 +00:00 коммит произвёл Leo McArdle
Родитель 8e66941f01
Коммит 97a7514315
39 изменённых файлов: 1232 добавлений и 1662 удалений

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

@ -39,7 +39,7 @@ jobs:
command: ./bin/dc_ci.sh run test ./bin/run-unit-tests.sh command: ./bin/dc_ci.sh run test ./bin/run-unit-tests.sh
- run: - run:
name: Run js tests name: Run js tests
command: ./bin/dc_ci.sh run test ./bin/run-mocha-tests.sh command: ./bin/dc_ci.sh run test npm run webpack:test
- when: - when:
condition: condition:
or: or:

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

@ -28,7 +28,7 @@ repos:
hooks: hooks:
- id: eslint - id: eslint
args: [--no-eslintrc, --config=webpack/eslintrc.js] args: [--no-eslintrc, --config=webpack/eslintrc.js]
exclude: "webpack/.*" exclude: "webpack/.*|webpack\\..*\\.js"
additional_dependencies: additional_dependencies:
- eslint@8.1.0 - eslint@8.1.0
- eslint-import-resolver-webpack@0.13.2 - eslint-import-resolver-webpack@0.13.2

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

@ -81,7 +81,7 @@ test: .docker-build
${DC} run web ./bin/run-unit-tests.sh ${DC} run web ./bin/run-unit-tests.sh
test-js: .docker-build test-js: .docker-build
${DC} run web ./bin/run-mocha-tests.sh ${DC} run web npm run webpack:test
docs: .docker-build docs: .docker-build
${DC} run web $(MAKE) -C docs/ clean ${DC} run web $(MAKE) -C docs/ clean

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

@ -1,5 +0,0 @@
#!/bin/bash
set -ex
./node_modules/.bin/mocha --require ./webpack/mocha-require --recursive kitsune/*/static/*/js/tests/* $@

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

@ -144,7 +144,7 @@ Running JavaScript Tests
To run tests, make sure you have have the NPM dependencies installed, and To run tests, make sure you have have the NPM dependencies installed, and
then run:: then run::
$ bin/run-mocha-tests.sh $ npm run webpack:test
Writing JavaScript Tests Writing JavaScript Tests
------------------------ ------------------------
@ -164,7 +164,6 @@ Here are a few tips for writing tests:
* You can use `sinon` to mock out parts of libraries or functions under * You can use `sinon` to mock out parts of libraries or functions under
test. This is useful for testing AJAX. test. This is useful for testing AJAX.
* The tests run in a Node.js environment. A browser environment can be * The tests run in a Node.js environment. A browser environment can be
simulated using ``jsdom``. Specifically, ``mocha-jsdom`` is useful to simulated using ``jsdom``.
set up and tear down the simulated environment.
.. _Mocha: https://mochajs.org/ .. _Mocha: https://mochajs.org/

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

@ -5,7 +5,7 @@ export default env;
(function($) { (function($) {
env.addGlobal('_', gettext); env.addGlobal('_', window.gettext);
env.addGlobal('ngettext', window.ngettext); env.addGlobal('ngettext', window.ngettext);
env.addFilter('f', function(fmt, obj, named) { env.addFilter('f', function(fmt, obj, named) {
@ -16,7 +16,7 @@ export default env;
obj[keys[i]] = escape(obj[keys[i]]); obj[keys[i]] = escape(obj[keys[i]]);
} }
return interpolate(fmt, obj, named); return window.interpolate(fmt, obj, named);
}); });
env.addFilter('urlparams', function(url, params) { env.addFilter('urlparams', function(url, params) {

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

@ -17,313 +17,309 @@ import AAQSystemInfo from "sumo/js/aaq";
// TODO: Figure out how to break out the functionality here into // TODO: Figure out how to break out the functionality here into
// testable parts. // testable parts.
(function($) { function init() {
var $body = $('body');
function init() { // if there's an error on page load, focus the field.
var $body = $('body'); $('.has-error input, .has-error textarea').first().focus();
// if there's an error on page load, focus the field. if ($body.is('.new-question')) {
$('.has-error input, .has-error textarea').first().focus(); initQuestion();
if ($body.is('.new-question')) { if (window.location.search.indexOf('step=aaq-register') > -1) {
initQuestion(); trackEvent('Ask A Question Flow', 'step 1 page');
} else if (window.location.search.indexOf('step=aaq-question') > -1) {
if (window.location.search.indexOf('step=aaq-register') > -1) { trackEvent('Ask A Question Flow', 'step 2 page');
trackEvent('Ask A Question Flow', 'step 1 page');
} else if (window.location.search.indexOf('step=aaq-question') > -1) {
trackEvent('Ask A Question Flow', 'step 2 page');
}
} }
if ($body.is('.edit-question')) {
initQuestion("editing");
}
if ($body.is('.questions')) {
initTagFilterToggle();
$('#flag-filter input[type="checkbox"]').on('click', function() {
window.location = $(this).data('url');
});
if (window.location.pathname.indexOf('questions/new/confirm') > -1) {
trackEvent('Ask A Question Flow', 'step 3 confirm page');
}
}
if ($body.is('.answers')) {
// Put last search query into search box
$('#support-search input[name=q]')
.val(unquote($.cookie('last_search')));
function takeQuestion() {
if ($(this).val().length > 0) {
var $form = $(this).closest('form');
var url = $form.data('take-question-url');
var csrftoken = $('input[name=csrfmiddlewaretoken]').val();
$.ajax({
url: url,
method: 'POST',
beforeSend: function(xhr, settings) {
xhr.setRequestHeader('X-CSRFToken', csrftoken);
}
});
}
}
$('#id_content').on('keyup', _throttle(takeQuestion, 60000));
$(document).on('click', '#details-edit', function(ev) {
ev.preventDefault();
$('#question-details').addClass('editing');
});
initHaveThisProblemTooAjax();
initHelpfulVote();
initCrashIdLinking();
initEditDetails();
addReferrerAndQueryToVoteForm();
initReplyToAnswer();
new AjaxPreview($('#preview'));
}
Marky.createSimpleToolbar('.editor-tools', '#reply-content, #id_content', {cannedResponses: !$body.is('.new-question')});
// product selector page reloading
$('#product-selector select').on('change', function() {
var val = $(this).val();
var queryParams = getQueryParamsAsDict(document.location.toString());
if (val === '') {
delete queryParams.product;
} else {
queryParams.product = val;
}
document.location = document.location.pathname + '?' + $.param(queryParams);
});
// sort questions page reloading
$('[data-sort-questions]').on('change', function() {
document.location = $(this).val()
});
} }
/* if ($body.is('.edit-question')) {
* Initialize the new/edit question page/form initQuestion("editing");
*/ }
function initQuestion(action) {
var $questionForm = $('#question-form'); if ($body.is('.questions')) {
var aaq = new AAQSystemInfo($questionForm); initTagFilterToggle();
if (action === "editing") {
$("#troubleshooting-field").show(); $('#flag-filter input[type="checkbox"]').on('click', function() {
window.location = $(this).data('url');
});
if (window.location.pathname.indexOf('questions/new/confirm') > -1) {
trackEvent('Ask A Question Flow', 'step 3 confirm page');
}
}
if ($body.is('.answers')) {
// Put last search query into search box
$('#support-search input[name=q]')
.val(unquote($.cookie('last_search')));
function takeQuestion() {
if ($(this).val().length > 0) {
var $form = $(this).closest('form');
var url = $form.data('take-question-url');
var csrftoken = $('input[name=csrfmiddlewaretoken]').val();
$.ajax({
url: url,
method: 'POST',
beforeSend: function(xhr, settings) {
xhr.setRequestHeader('X-CSRFToken', csrftoken);
}
});
}
}
$('#id_content').on('keyup', _throttle(takeQuestion, 60000));
$(document).on('click', '#details-edit', function(ev) {
ev.preventDefault();
$('#question-details').addClass('editing');
});
initHaveThisProblemTooAjax();
initHelpfulVote();
initCrashIdLinking();
initEditDetails();
addReferrerAndQueryToVoteForm();
initReplyToAnswer();
new AjaxPreview($('#preview'));
}
Marky.createSimpleToolbar('.editor-tools', '#reply-content, #id_content', {cannedResponses: !$body.is('.new-question')});
// product selector page reloading
$('#product-selector select').on('change', function() {
var val = $(this).val();
var queryParams = getQueryParamsAsDict(document.location.toString());
if (val === '') {
delete queryParams.product;
} else { } else {
hideDetails($questionForm, aaq); queryParams.product = val;
} }
document.location = document.location.pathname + '?' + $.param(queryParams);
});
// sort questions page reloading
$('[data-sort-questions]').on('change', function() {
document.location = $(this).val()
});
}
/*
* Initialize the new/edit question page/form
*/
function initQuestion(action) {
var $questionForm = $('#question-form');
var aaq = new AAQSystemInfo($questionForm);
if (action === "editing") {
$("#troubleshooting-field").show();
} else {
hideDetails($questionForm, aaq);
} }
}
function isLoggedIn() { function isLoggedIn() {
return $('#greeting span.user').length > 0; return $('#greeting span.user').length > 0;
} }
// Handle changes to the details for a question // Handle changes to the details for a question
function initEditDetails() { function initEditDetails() {
$('#details-product').on('change', function() { $('#details-product').on('change', function() {
var $selected; var $selected;
$(this).children().each(function() { $(this).children().each(function() {
if (this.selected) { if (this.selected) {
$selected = $(this); $selected = $(this);
}
});
$('#details-topic').children().remove();
$('#details-submit').prop('disabled', true);
$.ajax($selected.data('url'), {
'dataType': 'json',
'success': function(data) {
for (var i = 0; i < data.topics.length; i++) {
var topic = data.topics[i];
var $opt = $('<option />');
$opt.attr('value', topic.id);
$opt.text(topic.title);
$('#details-topic').append($opt);
} }
}); $('#details-submit').prop('disabled', false);
}
});
});
}
$('#details-topic').children().remove(); // Hide the browser/system details for users on FF with js enabled
$('#details-submit').prop('disabled', true); // and are submitting a question for FF on desktop.
function hideDetails($form, aaq) {
$form.find('ul').addClass('hide-details');
$form.find('a.show, a.hide').click(function(ev) {
ev.preventDefault();
$(this).closest('li')
.toggleClass('show')
.toggleClass('hide')
.closest('ul')
.toggleClass('show-details');
});
}
$.ajax($selected.data('url'), { /*
'dataType': 'json', * Ajaxify any "I have this problem too" forms (may be multiple per page)
'success': function(data) { */
for (var i = 0; i < data.topics.length; i++) { function initHaveThisProblemTooAjax() {
var topic = data.topics[i]; var $container = $('#question div.me-too, .question-tools div.me-too');
var $opt = $('<option />');
$opt.attr('value', topic.id); // ajaxify each form individually so the resulting kbox attaches to
$opt.text(topic.title); // the correct DOM element
$container.each(function() {
initAjaxForm($(this), 'form', '#vote-thanks');
});
$('#details-topic').append($opt); $container.find('input').click(function() {
$(this).attr('disabled', 'disabled');
});
// closing or cancelling the kbox on any of the forms should remove
// all of them
$container.delegate('.kbox-close, .kbox-cancel', 'click', function(ev) {
ev.preventDefault();
$container.unbind().remove();
});
}
function addReferrerAndQueryToVoteForm() {
// Add the source/referrer and query terms to the helpful vote form
var urlParams = getQueryParamsAsDict(),
referrer = getReferrer(urlParams),
query = getSearchQuery(urlParams, referrer);
$('form.helpful, .me-too form')
.append($('<input type="hidden" name="referrer"/>')
.attr('value', referrer))
.append($('<input type="hidden" name="query"/>')
.attr('value', query));
}
/*
* Ajaxify the Helpful/Not Helpful form
*/
function initHelpfulVote() {
$('.sumo-l-two-col--sidebar, #document-list, .answer-tools').each(function() {
new AjaxVote($(this).find('form.helpful'), { // eslint-disable-line
replaceFormWithMessage: true,
removeForm: true
});
});
}
// Helper
function initAjaxForm($container, formSelector, boxSelector, onKboxClose) {
$container.delegate(formSelector, 'submit', function(ev) {
ev.preventDefault();
var $form = $(this);
var url = $form.attr('action');
var data = $form.serialize();
$.ajax({
url: url,
type: 'POST',
data: data,
dataType: 'json',
success: function(response) {
if (response.html) {
if ($(boxSelector).length === 0) {
// We don't have a modal set up yet.
var kbox = new KBox(response.html, {
container: $container,
preClose: onKboxClose
});
kbox.open();
} else {
$(boxSelector).html($(response.html).children());
} }
$('#details-submit').prop('disabled', false); } else if (response.message) {
var html = '<div class="msg"></div>';
$(boxSelector)
.html(html)
.find('.msg').text(response.message);
} }
});
});
}
// Hide the browser/system details for users on FF with js enabled if (!response.ignored) {
// and are submitting a question for FF on desktop. // Trigger a document event for others to listen for.
function hideDetails($form, aaq) { $(document).trigger('vote', $.extend(data, {url: url}));
$form.find('ul').addClass('hide-details');
$form.find('a.show, a.hide').click(function(ev) {
ev.preventDefault();
$(this).closest('li')
.toggleClass('show')
.toggleClass('hide')
.closest('ul')
.toggleClass('show-details');
});
}
/*
* Ajaxify any "I have this problem too" forms (may be multiple per page)
*/
function initHaveThisProblemTooAjax() {
var $container = $('#question div.me-too, .question-tools div.me-too');
// ajaxify each form individually so the resulting kbox attaches to
// the correct DOM element
$container.each(function() {
initAjaxForm($(this), 'form', '#vote-thanks');
});
$container.find('input').click(function() {
$(this).attr('disabled', 'disabled');
});
// closing or cancelling the kbox on any of the forms should remove
// all of them
$container.delegate('.kbox-close, .kbox-cancel', 'click', function(ev) {
ev.preventDefault();
$container.unbind().remove();
});
}
function addReferrerAndQueryToVoteForm() {
// Add the source/referrer and query terms to the helpful vote form
var urlParams = getQueryParamsAsDict(),
referrer = getReferrer(urlParams),
query = getSearchQuery(urlParams, referrer);
$('form.helpful, .me-too form')
.append($('<input type="hidden" name="referrer"/>')
.attr('value', referrer))
.append($('<input type="hidden" name="query"/>')
.attr('value', query));
}
/*
* Ajaxify the Helpful/Not Helpful form
*/
function initHelpfulVote() {
$('.sumo-l-two-col--sidebar, #document-list, .answer-tools').each(function() {
new AjaxVote($(this).find('form.helpful'), { // eslint-disable-line
replaceFormWithMessage: true,
removeForm: true
});
});
}
// Helper
function initAjaxForm($container, formSelector, boxSelector, onKboxClose) {
$container.delegate(formSelector, 'submit', function(ev) {
ev.preventDefault();
var $form = $(this);
var url = $form.attr('action');
var data = $form.serialize();
$.ajax({
url: url,
type: 'POST',
data: data,
dataType: 'json',
success: function(response) {
if (response.html) {
if ($(boxSelector).length === 0) {
// We don't have a modal set up yet.
var kbox = new KBox(response.html, {
container: $container,
preClose: onKboxClose
});
kbox.open();
} else {
$(boxSelector).html($(response.html).children());
}
} else if (response.message) {
var html = '<div class="msg"></div>';
$(boxSelector)
.html(html)
.find('.msg').text(response.message);
}
if (!response.ignored) {
// Trigger a document event for others to listen for.
$(document).trigger('vote', $.extend(data, {url: url}));
}
},
error: function() {
var message = gettext('There was an error.');
alert(message);
} }
}); },
error: function() {
return false; var message = gettext('There was an error.');
alert(message);
}
}); });
return false;
});
}
function initTagFilterToggle() {
$('#toggle-tag-filter').click(function(e) {
e.preventDefault();
$('#tag-filter').slideToggle('fast'); // CSS3: Y U NO TRANSITION TO `height: auto;`?
$(this).toggleClass('off');
});
}
/*
* Links all crash IDs found in the passed HTML container elements
*/
export function linkCrashIds(container) {
if (!container) {
return;
} }
var crashIDRegex = new RegExp('(bp-[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})', 'g');
var crashStatsBase = 'https://crash-stats.mozilla.com/report/index/';
var helpingWithCrashesArticle = '/kb/helping-crashes';
var crashReportContainer =
"<span class='crash-report'>" +
"<a href='" + crashStatsBase + "$1' target='_blank'>$1</a>" +
"<a href='" + helpingWithCrashesArticle + "' target='_blank'>" +
"<img src='" + questionmarkIcon + "'></img></a></span>";
function initTagFilterToggle() { container.html(container.html().replace(crashIDRegex, crashReportContainer));
$('#toggle-tag-filter').click(function(e) { }
e.preventDefault();
$('#tag-filter').slideToggle('fast'); // CSS3: Y U NO TRANSITION TO `height: auto;`?
$(this).toggleClass('off');
});
}
/* /*
* Links all crash IDs found in the passed HTML container elements * Initialize the automatic linking of crash IDs
*/ */
function linkCrashIds(container) { function initCrashIdLinking() {
if (!container) { var postContents = $('.question .main-content, .answer .main-content, #more-system-details');
return; postContents.each(function() {
} linkCrashIds($(this));
var crashIDRegex = new RegExp('(bp-[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})', 'g'); });
var crashStatsBase = 'https://crash-stats.mozilla.com/report/index/'; }
var helpingWithCrashesArticle = '/kb/helping-crashes';
var crashReportContainer =
"<span class='crash-report'>" +
"<a href='" + crashStatsBase + "$1' target='_blank'>$1</a>" +
"<a href='" + helpingWithCrashesArticle + "' target='_blank'>" +
"<img src='" + questionmarkIcon + "'></img></a></span>";
container.html(container.html().replace(crashIDRegex, crashReportContainer)); function initReplyToAnswer() {
} $('a.quoted-reply').click(function() {
var contentId = $(this).data('content-id'),
$content = $('#' + contentId),
text = $content.find('.content-raw').text(),
user = $content.find('.display-name').text(),
reply_text = `''<p>${user} [[#${contentId}|${gettext('said')}]]</p>''\n<blockquote>${text}\n</blockquote>\n\n`,
$textarea = $('#id_content'),
oldtext = $textarea.val();
/* $textarea.val(oldtext + reply_text);
* Initialize the automatic linking of crash IDs
*/
function initCrashIdLinking() {
var postContents = $('.question .main-content, .answer .main-content, #more-system-details');
postContents.each(function() {
linkCrashIds($(this));
});
}
function initReplyToAnswer() { setTimeout(function() {
$('a.quoted-reply').click(function() { $textarea.focus();
var contentId = $(this).data('content-id'), }, 10);
$content = $('#' + contentId),
text = $content.find('.content-raw').text(),
user = $content.find('.display-name').text(),
reply_text = `''<p>${user} [[#${contentId}|${gettext('said')}]]</p>''\n<blockquote>${text}\n</blockquote>\n\n`,
$textarea = $('#id_content'),
oldtext = $textarea.val();
$textarea.val(oldtext + reply_text); return true;
});
}
setTimeout(function() { $(document).ready(init);
$textarea.focus();
}, 10);
return true;
});
}
$(document).ready(init);
})(jQuery);

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

@ -345,9 +345,9 @@ ShowFor.prototype.initShowFuncs = function() {
ShowFor.prototype.showAndHide = function() { ShowFor.prototype.showAndHide = function() {
this.$container.find('.for').each(function(i, elem) { this.$container.find('.for').each(function(i, elem) {
var $elem = $(elem); var $elem = $(elem);
var showFunc = $elem.data('show-func'); var showFuncVal = $elem.data('show-func')();
if (showFunc) { if (showFuncVal !== undefined) {
$elem.toggle(showFunc()); $elem.toggle(showFuncVal);
} else { } else {
$elem.show(); $elem.show();
} }

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

@ -6,93 +6,90 @@ import _keys from "underscore/modules/keys";
* A tag filtering form. * A tag filtering form.
*/ */
(function($) { function init($container) {
var $form = $container ? $container.find('form') : $('#tag-filter form'),
$tags = $form.find('input[type="text"]'), $btn = $form.find('input[type="submit"], button'),
$hidden = $('<input type="hidden"/>'),
vocab = $tags.data('vocabulary'),
lowerVocab = {};
function init($container) { if (!$form.length) {
var $form = $container ? $container.find('form') : $('#tag-filter form'), return;
$tags = $form.find('input[type="text"]'), $btn = $form.find('input[type="submit"], button'), }
$hidden = $('<input type="hidden"/>'),
vocab = $tags.data('vocabulary'),
lowerVocab = {};
if (!$form.length) { // Create a lower case vocab for case insensitive match.
return; _each(_keys(vocab), function(name) {
lowerVocab[name.toLowerCase()] = vocab[name];
});
// Add a hidden field for comma-separated slugs.
$hidden.attr('name', $tags.attr('name'))
.appendTo($form);
$tags.removeAttr('name');
// Disable button while text input is empty.
$btn.attr('disabled', 'disabled');
$tags.keyup(function() {
if ($tags.val()) {
$btn.removeAttr('disabled');
} else {
$btn.attr('disabled', 'disabled');
} }
});
// Create a lower case vocab for case insensitive match. // Set up autocomplete
_each(_keys(vocab), function(name) { // Skip if the autocomplete plugin isn't available (unit tests).
lowerVocab[name.toLowerCase()] = vocab[name]; if ($tags.autocomplete) {
}); $tags.autocomplete({
source: _keys(vocab),
// Add a hidden field for comma-separated slugs. delay: 0,
$hidden.attr('name', $tags.attr('name')) minLength: 1
.appendTo($form);
$tags.removeAttr('name');
// Disable button while text input is empty.
$btn.attr('disabled', 'disabled');
$tags.keyup(function() {
if ($tags.val()) {
$btn.removeAttr('disabled');
} else {
$btn.attr('disabled', 'disabled');
}
});
// Set up autocomplete
// Skip if the autocomplete plugin isn't available (unit tests).
if ($tags.autocomplete) {
$tags.autocomplete({
source: _keys(vocab),
delay: 0,
minLength: 1
});
}
// When form is submitted, get the slugs to send over in request.
$form.submit(function() {
var tagNames = $tags.val(),
slugNames = [],
currentSlugs = $form.find('input.current-tagged').val(),
slugs,
invalid = false;
// For each tag name, find the slug.
_each(tagNames.split(','), function(tag) {
var trimmed = $.trim(tag),
slug = lowerVocab[trimmed.toLowerCase()];
if (slug) {
slugNames.push(slug);
} else if (trimmed) {
invalid = true;
alert(interpolate(gettext('Invalid tag entered: %s'), [tag]));
}
});
// Invalid or no tags? No requests!
if (invalid || slugNames.length === 0) {
$form.trigger('ajaxComplete');
if (!invalid) {
alert(gettext('No tags entered.'));
}
return false;
}
slugs = slugNames.join(',');
// Prepend any existing filters applied.
if (currentSlugs) {
slugs = currentSlugs + ',' + slugs;
}
$hidden.val(slugs);
}); });
} }
const TagsFilter = { // When form is submitted, get the slugs to send over in request.
init: init $form.submit(function() {
}; var tagNames = $tags.val(),
slugNames = [],
currentSlugs = $form.find('input.current-tagged').val(),
slugs,
invalid = false;
$(document).ready(function() { // For each tag name, find the slug.
TagsFilter.init(); _each(tagNames.split(','), function(tag) {
var trimmed = $.trim(tag),
slug = lowerVocab[trimmed.toLowerCase()];
if (slug) {
slugNames.push(slug);
} else if (trimmed) {
invalid = true;
alert(interpolate(gettext('Invalid tag entered: %s'), [tag]));
}
});
// Invalid or no tags? No requests!
if (invalid || slugNames.length === 0) {
$form.trigger('ajaxComplete');
if (!invalid) {
alert(gettext('No tags entered.'));
}
return false;
}
slugs = slugNames.join(',');
// Prepend any existing filters applied.
if (currentSlugs) {
slugs = currentSlugs + ',' + slugs;
}
$hidden.val(slugs);
}); });
}
})(jQuery); const TagsFilter = {
init: init
};
export default TagsFilter;
$(document).ready(function() {
TagsFilter.init();
});

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

@ -1,28 +1,13 @@
import React from 'react'; import React from 'react';
import {default as mochaJsdom, rerequire} from 'mocha-jsdom';
import {expect} from 'chai'; import {expect} from 'chai';
import sinon from 'sinon'; import sinon from 'sinon';
import mochaGettext from './fixtures/mochaGettext.js';
import mochaK from './fixtures/mochaK.js';
import mochaJquery from './fixtures/mochaJquery.js';
import AjaxPreview from "sumo/js/ajaxpreview"; import AjaxPreview from "sumo/js/ajaxpreview";
describe('ajax preview', () => { describe('ajax preview', () => {
mochaJsdom({useEach: true, url: 'http://localhost'});
mochaJquery();
mochaK();
mochaGettext();
/* globals window, $, k */
var fakeServer;
describe('events', () => { describe('events', () => {
beforeEach(() => { beforeEach(() => {
rerequire('../ajaxpreview.js');
rerequire('../libs/jquery.lazyload.js');
sinon.stub($, 'ajax').yieldsTo('success', '<p>The content to preview.</p>'); sinon.stub($, 'ajax').yieldsTo('success', '<p>The content to preview.</p>');

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

@ -1,25 +1,12 @@
import React from 'react'; import React from 'react';
import {default as mochaJsdom, rerequire} from 'mocha-jsdom';
import {expect} from 'chai'; import {expect} from 'chai';
import sinon from 'sinon'; import sinon from 'sinon';
import mochaK from './fixtures/mochaK.js';
import mochaJquery from './fixtures/mochaJquery.js';
import AjaxVote from "sumo/js/ajaxvote"; import AjaxVote from "sumo/js/ajaxvote";
describe('ajaxvote', () => { describe('ajaxvote', () => {
mochaJsdom({useEach: true, url: 'http://localhost'});
mochaJquery();
mochaK();
/* globals window, document, $, k */
describe('helpful vote', () => { describe('helpful vote', () => {
let fakeServer;
beforeEach(() => { beforeEach(() => {
rerequire('../ajaxvote.js');
sinon.stub($, 'ajax').yieldsTo('success', {message: 'Thanks for the vote!'}); sinon.stub($, 'ajax').yieldsTo('success', {message: 'Thanks for the vote!'});
let sandbox = ( let sandbox = (
@ -34,6 +21,7 @@ describe('ajaxvote', () => {
afterEach(() => { afterEach(() => {
$.ajax.restore(); $.ajax.restore();
React.unmountComponentAtNode(document.body); React.unmountComponentAtNode(document.body);
$(document).off('vote');
}); });
it('should fire an event on a helpful vote', done => { it('should fire an event on a helpful vote', done => {

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

@ -1,21 +1,8 @@
import {expect} from 'chai'; import {expect} from 'chai';
import {default as mochaJsdom, rerequire} from 'mocha-jsdom';
import mochaUnderscore from './fixtures/mochaUnderscore.js'; import BrowserDetect from "sumo/js/browserdetect";
describe('BrowserDetect', () => { describe('BrowserDetect', () => {
mochaJsdom({useEach: true, url: 'http://localhost'});
mochaUnderscore();
/* globals window */
let BrowserDetect;
beforeEach(() => {
rerequire('../browserdetect.js');
BrowserDetect = window.BrowserDetect;
});
describe('Fennec versions', () => { describe('Fennec versions', () => {
it('should detect Fennec 7', () => { it('should detect Fennec 7', () => {
let ua = 'Mozilla/5.0 (Android; Linux armv7l; rv:7.0.1) Gecko/ Firefox/7.0.1 Fennec/7.0.1'; let ua = 'Mozilla/5.0 (Android; Linux armv7l; rv:7.0.1) Gecko/ Firefox/7.0.1 Fennec/7.0.1';

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

@ -1,51 +1,37 @@
import {default as mochaJsdom, rerequire} from 'mocha-jsdom';
import {default as chai, expect} from 'chai'; import {default as chai, expect} from 'chai';
import chaiLint from 'chai-lint'; import chaiLint from 'chai-lint';
import sinon from 'sinon'; import sinon from 'sinon';
import sinonChai from 'sinon-chai'; import sinonChai from 'sinon-chai';
import mochaK from './fixtures/mochaK.js'; import {
import mochaJquery from './fixtures/mochaJquery.js'; getQueryParamsAsDict,
import mochaUnderscore from './fixtures/mochaUnderscore.js'; getReferrer,
getSearchQuery,
unquote,
safeString,
safeInterpolate,
} from "sumo/js/main";
chai.use(chaiLint); chai.use(chaiLint);
chai.use(sinonChai); chai.use(sinonChai);
describe('k', () => { describe('k', () => {
mochaJsdom({
useEach: true,
url: 'http://localhost',
document: {
referrer: 'http://google.com/?q=cookies',
referer: 'http://google.com/?q=cookies',
},
});
mochaJquery();
mochaK();
mochaUnderscore();
/* globals document:false, $:false, k:false */
beforeEach(() => {
rerequire('../libs/jquery.placeholder.js');
rerequire('../main.js');
});
describe('getQueryParamsAsDict', () => { describe('getQueryParamsAsDict', () => {
it('should return an empty object for no params', () => { it('should return an empty object for no params', () => {
let url = 'http://example.com'; let url = 'http://example.com';
let params = k.getQueryParamsAsDict(url); let params = getQueryParamsAsDict(url);
expect(params).to.deep.equal({}); expect(params).to.deep.equal({});
}); });
it('should parse a query string with one parameter', () => { it('should parse a query string with one parameter', () => {
let url = 'http://example.com/?test=woot'; let url = 'http://example.com/?test=woot';
let params = k.getQueryParamsAsDict(url); let params = getQueryParamsAsDict(url);
expect(params).to.deep.equal({test: 'woot'}); expect(params).to.deep.equal({test: 'woot'});
}); });
it('should parse a query string with two paramaters', () => { it('should parse a query string with two paramaters', () => {
let url = 'http://example.com/?x=foo&y=bar'; let url = 'http://example.com/?x=foo&y=bar';
let params = k.getQueryParamsAsDict(url); let params = getQueryParamsAsDict(url);
expect(params).to.deep.equal({x: 'foo', y: 'bar'}); expect(params).to.deep.equal({x: 'foo', y: 'bar'});
}); });
@ -54,7 +40,7 @@ describe('k', () => {
'ved=0CDEQFjAA&url=http%3A%2F%2Fsupport.mozilla.com%2F&' + 'ved=0CDEQFjAA&url=http%3A%2F%2Fsupport.mozilla.com%2F&' +
'rct=j&q=firefox%20help&ei=OsBSTpbZBIGtgQfgzv3yBg&' + 'rct=j&q=firefox%20help&ei=OsBSTpbZBIGtgQfgzv3yBg&' +
'usg=AFQjCNFIV7wgd9Pnr0m3Ofc7r1zVTNK8dw'); 'usg=AFQjCNFIV7wgd9Pnr0m3Ofc7r1zVTNK8dw');
let params = k.getQueryParamsAsDict(url); let params = getQueryParamsAsDict(url);
expect(params).to.deep.equal({ expect(params).to.deep.equal({
sa: 't', sa: 't',
source: 'web', source: 'web',
@ -70,50 +56,23 @@ describe('k', () => {
}); });
}); });
describe('queryParamStringFromDict', () => {
it('should serialize an empty dict into a ?', () => {
let actual = k.queryParamStringFromDict({});
expect(actual).to.equal('?');
});
it('it should serialize an object with a single key', () => {
let actual = k.queryParamStringFromDict({foo: 1});
expect(actual).to.equal('?foo=1');
});
it('should serialize an object with two keys', () => {
let actual = k.queryParamStringFromDict({foo: 1, bar: 2});
expect(actual).to.equal('?foo=1&bar=2');
});
it('should not include null or undefined in the output', () => {
let actual = k.queryParamStringFromDict({foo: undefined, bar: 2, baz: null});
expect(actual).to.equal('?bar=2');
});
it('should serialize an object with three keys', () => {
let actual = k.queryParamStringFromDict({foo: 1, bar: 2, baz: 3});
expect(actual).to.deep.equal('?foo=1&bar=2&baz=3');
});
});
describe('getReferrer', () => { describe('getReferrer', () => {
it('should recognize search referrers', () => { it('should recognize search referrers', () => {
let params = {as: 's', s: 'cookies'}; let params = {as: 's', s: 'cookies'};
let actual = k.getReferrer(params); let actual = getReferrer(params);
expect(actual).to.equal('search'); expect(actual).to.equal('search');
}); });
it('should recognize inproduct referrers', () => { it('should recognize inproduct referrers', () => {
let params = {as: 'u'}; let params = {as: 'u'};
let actual = k.getReferrer(params); let actual = getReferrer(params);
expect(actual).to.equal('inproduct'); expect(actual).to.equal('inproduct');
}); });
it('should fall back to `document.referrer`', () => { it('should fall back to `document.referrer`', () => {
let referrer = 'http://google.com/?q=cookies'; let referrer = 'http://google.com/?q=cookies';
expect(document.referrer).to.equal(referrer); expect(document.referrer).to.equal(referrer);
expect(k.getReferrer({})).to.equal(referrer); expect(getReferrer({})).to.equal(referrer);
}); });
}); });
@ -121,54 +80,54 @@ describe('k', () => {
it('should return the s query string for local search referrers', () => { it('should return the s query string for local search referrers', () => {
let params = {as: 's', s: 'cookies'}; let params = {as: 's', s: 'cookies'};
let referrer = 'search'; let referrer = 'search';
expect(k.getSearchQuery(params, referrer)).to.equal('cookies'); expect(getSearchQuery(params, referrer)).to.equal('cookies');
}); });
it('should return an empty string fro inproduct referrers', () => { it('should return an empty string fro inproduct referrers', () => {
let params = {as: 'u', s: 'wrong'}; let params = {as: 'u', s: 'wrong'};
let referrer = 'inproduct'; let referrer = 'inproduct';
expect(k.getSearchQuery(params, referrer)).to.equal(''); expect(getSearchQuery(params, referrer)).to.equal('');
}); });
it('should detect external search parameters from google', () => { it('should detect external search parameters from google', () => {
let referrer = 'http://google.com/?q=cookies'; let referrer = 'http://google.com/?q=cookies';
expect(k.getSearchQuery({}, referrer)).to.equal('cookies'); expect(getSearchQuery({}, referrer)).to.equal('cookies');
}); });
}); });
describe('unquote', () => { describe('unquote', () => {
it('should return undefined for undefined input', () => { it('should return undefined for undefined input', () => {
expect(k.unquote(undefined)).to.beUndefined(); expect(unquote(undefined)).to.beUndefined();
}); });
it('should unquote simply quoted strings', () => { it('should unquote simply quoted strings', () => {
expect(k.unquote('"delete cookies"')).to.equal('delete cookies'); expect(unquote('"delete cookies"')).to.equal('delete cookies');
}); });
it('should handle escaped quotes', () => { it('should handle escaped quotes', () => {
expect(k.unquote('"\\"delete\\" cookies"')).to.equal('"delete" cookies'); expect(unquote('"\\"delete\\" cookies"')).to.equal('"delete" cookies');
}); });
it('should handle escaped quotes with no other quotes', () => { it('should handle escaped quotes with no other quotes', () => {
expect(k.unquote('\\"delete\\" cookies')).to.equal('"delete" cookies'); expect(unquote('\\"delete\\" cookies')).to.equal('"delete" cookies');
}); });
it('should pass strings without quotes through unmodified', () => { it('should pass strings without quotes through unmodified', () => {
let s = 'cookies'; let s = 'cookies';
expect(k.unquote(s)).to.equal(s); expect(unquote(s)).to.equal(s);
}); });
}); });
describe('safeString', () => { describe('safeString', () => {
it('should escape html', function() { it('should escape html', function() {
let unsafeString = '<a href="foo&\'">'; let unsafeString = '<a href="foo&\'">';
let safeString = '&lt;a href=&quot;foo&amp;&#39;&quot;&gt;'; let expectedString = '&lt;a href=&quot;foo&amp;&#39;&quot;&gt;';
expect(k.safeString(unsafeString)).to.equal(safeString); expect(safeString(unsafeString)).to.equal(expectedString);
}); });
}); });
describe('safeInterpolate', () => { describe('safeInterpolate', () => {
/* k.safeInterpolate works by delegating to `interpolate`, a Django /* safeInterpolate works by delegating to `interpolate`, a Django
* gettext function. These tests mock out interpolate and make sure * gettext function. These tests mock out interpolate and make sure
* it was called appropriately. * it was called appropriately.
*/ */
@ -183,7 +142,7 @@ describe('k', () => {
let unsafe = ['<a>', '<script>']; let unsafe = ['<a>', '<script>'];
let safe = ['&lt;a&gt;', '&lt;script&gt;']; let safe = ['&lt;a&gt;', '&lt;script&gt;'];
k.safeInterpolate(html, unsafe, false); safeInterpolate(html, unsafe, false);
expect(interpolateSpy).to.have.callCount(1); expect(interpolateSpy).to.have.callCount(1);
expect(interpolateSpy).to.have.been.calledWithExactly(html, safe, false); expect(interpolateSpy).to.have.been.calledWithExactly(html, safe, false);
@ -199,7 +158,7 @@ describe('k', () => {
display: '&lt;script&gt;alert(&#39;xss&#39;);&lt;/script&gt;', display: '&lt;script&gt;alert(&#39;xss&#39;);&lt;/script&gt;',
name: 'Jo&amp;mdash;hn', name: 'Jo&amp;mdash;hn',
}; };
k.safeInterpolate(html, unsafe, true); safeInterpolate(html, unsafe, true);
expect(interpolateSpy).to.have.callCount(1); expect(interpolateSpy).to.have.callCount(1);
expect(interpolateSpy).to.have.been.calledWithExactly(html, safe, true); expect(interpolateSpy).to.have.been.calledWithExactly(html, safe, true);

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

@ -1,26 +1,10 @@
import {default as mochaJsdom, rerequire} from 'mocha-jsdom';
import {expect} from 'chai'; import {expect} from 'chai';
import React from 'react'; import React from 'react';
import mochaK from './fixtures/mochaK.js'; import { linkCrashIds } from "sumo/js/questions";
import mochaJquery from './fixtures/mochaJquery.js';
import mochaGettext from './fixtures/mochaGettext.js';
import mochaMarky from './fixtures/mochaMarky.js';
describe('k', () => { describe('k', () => {
mochaJsdom({useEach: true, url: 'http://localhost'});
mochaJquery();
mochaK();
mochaGettext();
mochaMarky();
/* globals window, document, $, k */
describe('linkCrashIds', () => { describe('linkCrashIds', () => {
beforeEach(() => {
rerequire('../questions.js');
});
afterEach(() => { afterEach(() => {
React.unmountComponentAtNode(document.body); React.unmountComponentAtNode(document.body);
}); });
@ -39,7 +23,7 @@ describe('k', () => {
); );
React.render(sandbox, document.body); React.render(sandbox, document.body);
k.linkCrashIds($('body')); linkCrashIds($('body'));
expect($('.crash-report').length).to.equal(1); expect($('.crash-report').length).to.equal(1);
}); });
@ -61,7 +45,7 @@ describe('k', () => {
); );
React.render(sandbox, document.body); React.render(sandbox, document.body);
k.linkCrashIds($('body')); linkCrashIds($('body'));
expect($('.crash-report').length).to.equal(5); expect($('.crash-report').length).to.equal(5);
}); });
@ -73,7 +57,7 @@ describe('k', () => {
</section> </section>
); );
React.render(sandbox, document.body); React.render(sandbox, document.body);
k.linkCrashIds($('body')); linkCrashIds($('body'));
expect($('.crash-report').length).to.equal(0); expect($('.crash-report').length).to.equal(0);
}); });
@ -87,7 +71,7 @@ describe('k', () => {
); );
React.render(sandbox, document.body); React.render(sandbox, document.body);
k.linkCrashIds($('body')); linkCrashIds($('body'));
expect($('.crash-report').length).to.equal(0); expect($('.crash-report').length).to.equal(0);
}); });
}); });

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

@ -1,8 +0,0 @@
import mochaFixtureHelper from './mochaFixtureHelper.js';
export default mochaFixtureHelper(({browser='firefox', version=25.0, OS='winxp'}={}) => {
let BrowserDetect = {browser, version, OS};
return {
BrowserDetect: BrowserDetect,
};
});

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

@ -1,31 +0,0 @@
/**
* Install globals into the jsdom namespace.
* @param {function} mapFunc This function will be called to get the list of
* things to install into the namespace. Should return an object of keys
* to values to install.
*/
export default function(mapFunc) {
return function(options) {
let map;
global.beforeEach(() => {
map = mapFunc(options);
for (let key in map) {
let val = map[key];
global[key] = val;
if (global.window) {
global.window[key] = val;
}
}
});
global.afterEach(() => {
for (let key in map) {
delete global[key];
if (global.window) {
delete global.window[key];
}
}
});
};
}

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

@ -1,11 +0,0 @@
import mochaFixtureHelper from './mochaFixtureHelper.js';
function fakeGettext(msgid) {
return msgid;
}
export default mochaFixtureHelper(() => {
return {
gettext: fakeGettext,
};
});

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

@ -1,8 +0,0 @@
import mochaFixtureHelper from './mochaFixtureHelper.js';
export default mochaFixtureHelper(() => {
return {
_gaq: [],
trackEvent: function() {}
};
});

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

@ -1,10 +0,0 @@
import mochaFixtureHelper from './mochaFixtureHelper.js';
import jQuery from 'jquery';
export default mochaFixtureHelper(() => {
let jq = jQuery(global.window);
return {
$: jq,
jQuery: jq,
};
});

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

@ -1,8 +0,0 @@
import mochaFixtureHelper from './mochaFixtureHelper.js';
export default mochaFixtureHelper(() => {
let k = global.k || (global.window ? global.window.k : null) || {};
return {
k: k,
};
});

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

@ -1,9 +0,0 @@
import {rerequire} from 'mocha-jsdom';
import mochaFixtureHelper from './mochaFixtureHelper.js';
export default mochaFixtureHelper(() => {
rerequire('../../markup.js');
return {
Marky: global.window.Marky,
};
});

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

@ -1,37 +0,0 @@
import path from 'path';
import {rerequire} from 'mocha-jsdom';
import {FileSystemLoader} from 'nunjucks';
/**
* Load and set up nunjucks and the Sumo nunjucks environment for Mocha tests.
*/
export default function() {
global.beforeEach(() => {
let nunjucks = rerequire('nunjucks');
global.nunjucks = nunjucks;
if (global.window) {
global.window.nunjucks = nunjucks;
}
const originalConfigure = nunjucks.configure;
nunjucks.configure = opts => {
opts.watch = false;
return originalConfigure(opts);
};
rerequire('../../nunjucks.js');
global.window.k.nunjucksEnv.loaders = [
new FileSystemLoader('kitsune/sumo/static/sumo/tpl', true, true)
];
global.window.k.nunjucksEnv.initCache();
});
global.afterEach(() => {
delete global.nunjucks;
delete global.nunjucksEnv;
if (global.window) {
delete global.window.nunjucks;
}
});
}

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

@ -1,8 +0,0 @@
import mochaFixtureHelper from './mochaFixtureHelper.js';
import _ from 'underscore';
export default mochaFixtureHelper(() => {
return {
_: _,
};
});

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

@ -1,48 +1,23 @@
import {default as mochaJsdom, rerequire} from 'mocha-jsdom';
import {default as chai, expect} from 'chai'; import {default as chai, expect} from 'chai';
import React from 'react'; import React from 'react';
import chaiLint from 'chai-lint'; import chaiLint from 'chai-lint';
import sinon from 'sinon'; import sinon from 'sinon';
import mochaK from './fixtures/mochaK.js'; import "sumo/js/templates/search-results";
import mochaJquery from './fixtures/mochaJquery.js'; import "sumo/js/instant_search";
import mochaGoogleAnalytics from './fixtures/mochaGoogleAnalytics.js'; import CachedXHR from "sumo/js/cached_xhr";
import mochaNunjucks from './fixtures/mochaNunjucks.js';
import mochaGettext from './fixtures/mochaGettext.js';
chai.use(chaiLint); chai.use(chaiLint);
describe('instant search', () => { describe('instant search', () => {
mochaJsdom({useEach: true, url: 'http://localhost'});
mochaJquery();
mochaK();
mochaGoogleAnalytics();
mochaGettext();
mochaNunjucks();
/* globals window, document, $ */
describe('', () => { describe('', () => {
let $sandbox;
let clock; let clock;
let cxhrMock;
beforeEach(() => { beforeEach(() => {
clock = sinon.useFakeTimers(); clock = sinon.useFakeTimers();
window.matchMedia = () => { cxhrMock = sinon.fake();
return { sinon.replace(CachedXHR.prototype, "request", cxhrMock);
matches: false,
addListener: () => {}
}
}
global.matchMedia = window.matchMedia;
window.Mzp = {};
window._localStorage = { getItem: () => undefined };
rerequire('../i18n.js');
global.interpolate = global.window.interpolate;
rerequire('../search_utils.js');
rerequire('../instant_search.js');
let content = ( let content = (
<div> <div>
<div id="main-content"/> <div id="main-content"/>
@ -58,6 +33,7 @@ describe('instant search', () => {
afterEach(() => { afterEach(() => {
React.unmountComponentAtNode(document.body); React.unmountComponentAtNode(document.body);
clock.restore(); clock.restore();
sinon.restore();
}); });
it('shows and hides the main content correctly', () => { it('shows and hides the main content correctly', () => {
@ -65,27 +41,24 @@ describe('instant search', () => {
expect($('#main-content').css('display')).to.not.equal('none'); expect($('#main-content').css('display')).to.not.equal('none');
$searchInput.val('test'); $searchInput.val('test');
$searchInput.keyup(); $searchInput.trigger('keyup');
expect($('#main-content').css('display')).to.equal('none'); expect($('#main-content').css('display')).to.equal('none');
$searchInput.val(''); $searchInput.val('');
$searchInput.keyup(); $searchInput.trigger('keyup');
expect($('#main-content').css('display')).to.not.equal('none'); expect($('#main-content').css('display')).to.not.equal('none');
}); });
it('shows the search query at the top of the page', () => { it('shows the search query at the top of the page', () => {
const query = 'search query'; const query = 'search query';
const requestExpectation = cxhrMock.expects('request')
.once()
.withArgs(sinon.match.string, sinon.match(opts => opts.data.q === query));
const $searchInput = $('#search-q'); const $searchInput = $('#search-q');
$searchInput.val(query); $searchInput.val(query);
$searchInput.keyup(); $searchInput.trigger('keyup');
clock.tick(200); clock.tick(200);
// call the callback to actually render things // call the callback to actually render things
requestExpectation.firstCall.args[1].success({ cxhrMock.firstCall.args[1].success({
num_results: 0, num_results: 0,
q: query, q: query,
}); });
@ -96,15 +69,14 @@ describe('instant search', () => {
it('escapes the search query at the top of the page', () => { it('escapes the search query at the top of the page', () => {
const query = '<'; const query = '<';
const requestExpectation = cxhrMock.expects('request');
const $searchInput = $('#search-q'); const $searchInput = $('#search-q');
$searchInput.val(query); $searchInput.val(query);
$searchInput.keyup(); $searchInput.trigger('keyup');
clock.tick(200); clock.tick(200);
// call the callback to actually render things // call the callback to actually render things
requestExpectation.firstCall.args[1].success({ cxhrMock.firstCall.args[1].success({
num_results: 0, num_results: 0,
q: query, q: query,
}); });

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

@ -1,31 +1,16 @@
import {default as mochaJsdom, rerequire} from 'mocha-jsdom';
import {default as chai, expect} from 'chai'; import {default as chai, expect} from 'chai';
import React from 'react'; import React from 'react';
import chaiLint from 'chai-lint'; import chaiLint from 'chai-lint';
import mochaK from './fixtures/mochaK.js';
import mochaJquery from './fixtures/mochaJquery.js';
import mochaGettext from './fixtures/mochaGettext.js';
import mochaMarky from './fixtures/mochaMarky.js';
import KBox from "sumo/js/kbox.js"; import KBox from "sumo/js/kbox.js";
chai.use(chaiLint); chai.use(chaiLint);
describe('kbox', () => { describe('kbox', () => {
mochaJsdom({useEach: true, url: 'http://localhost'});
mochaJquery();
mochaK();
mochaGettext();
mochaMarky();
/* globals window, document, $ */
describe('declarative', () => { describe('declarative', () => {
let $kbox, kbox; let $kbox, kbox;
beforeEach(() => { beforeEach(() => {
rerequire('../kbox.js');
let sandbox = ( let sandbox = (
<div id="sandbox"> <div id="sandbox">
<div className="kbox" <div className="kbox"

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

@ -1,21 +1,10 @@
import {default as mochaJsdom, rerequire} from 'mocha-jsdom';
import {default as chai, expect} from 'chai'; import {default as chai, expect} from 'chai';
import React from 'react'; import React from 'react';
import chaiLint from 'chai-lint'; import chaiLint from 'chai-lint';
import mochaJquery from './fixtures/mochaJquery.js';
chai.use(chaiLint); chai.use(chaiLint);
describe('lazyload', () => { describe('lazyload', () => {
mochaJsdom({useEach: true, url: 'http://localhost'});
mochaJquery();
/* globals document, $ */
beforeEach(() => {
rerequire('../libs/jquery.lazyload.js');
});
it('should load original image', () => { it('should load original image', () => {
let img = <img className="lazy" data-original-src="http://example.com/test.jpg"/>; let img = <img className="lazy" data-original-src="http://example.com/test.jpg"/>;
React.render(img, document.body); React.render(img, document.body);

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

@ -1,11 +1,10 @@
import React from 'react'; import React from 'react';
import {default as mochaJsdom, rerequire} from 'mocha-jsdom';
import {default as chai, expect} from 'chai'; import {default as chai, expect} from 'chai';
import chaiLint from 'chai-lint'; import chaiLint from 'chai-lint';
import sinon from 'sinon'; import sinon from 'sinon';
import mochaJquery from './fixtures/mochaJquery.js'; import BrowserDetect from 'sumo/js/browserdetect';
import mochaBrowserDetect from './fixtures/mochaBrowserDetect.js'; import ShowFor from "sumo/js/showfor";
chai.use(chaiLint); chai.use(chaiLint);
@ -15,7 +14,7 @@ chai.use(chaiLint);
* be bound to the passed $sandbox. * be bound to the passed $sandbox.
*/ */
function showForNoInit($sandbox) { function showForNoInit($sandbox) {
let sf = Object.create(window.ShowFor.prototype); let sf = Object.create(ShowFor.prototype);
sf.$container = $sandbox; sf.$container = $sandbox;
sf.state = {}; sf.state = {};
return sf; return sf;
@ -29,14 +28,9 @@ function unorderedEquals(arr1, arr2) {
describe('ShowFor', () => { describe('ShowFor', () => {
mochaJsdom({useEach: true, url: 'http://localhost'});
mochaJquery();
/* globals window, document, $ */
let showFor; let showFor;
beforeEach(() => { beforeEach(() => {
rerequire('../showfor.js');
// Wow. That's a lot of data. Can we make this smaller? // Wow. That's a lot of data. Can we make this smaller?
let sandbox = ( let sandbox = (
<div> <div>
@ -124,6 +118,14 @@ describe('ShowFor', () => {
React.render(sandbox, document.body); React.render(sandbox, document.body);
showFor = showForNoInit($('body')); showFor = showForNoInit($('body'));
BrowserDetect.browser = "firefox";
BrowserDetect.version = 25.0;
BrowserDetect.OS = "winxp";
});
afterEach(() => {
BrowserDetect.init();
}); });
describe('loadData', () => { describe('loadData', () => {
@ -145,13 +147,10 @@ describe('ShowFor', () => {
describe('updateUI', () => { describe('updateUI', () => {
describe('Firefox 26 on Windows XP', () => { describe('Firefox 26 on Windows XP', () => {
mochaBrowserDetect({
browser: 'fx',
version: 26.0,
OS: 'winxp',
});
beforeEach(() => { beforeEach(() => {
BrowserDetect.browser = "fx"
BrowserDetect.version = 26.0
BrowserDetect.OS = "winxp"
showFor.loadData(); showFor.loadData();
showFor.updateUI(); showFor.updateUI();
}); });
@ -163,13 +162,10 @@ describe('ShowFor', () => {
}); });
describe('Firefox for Android 23', () => { describe('Firefox for Android 23', () => {
mochaBrowserDetect({
browser: 'm',
version: 23.0,
OS: 'android',
});
beforeEach(() => { beforeEach(() => {
BrowserDetect.browser = "m"
BrowserDetect.version = 23.0
BrowserDetect.OS = "android"
showFor.loadData(); showFor.loadData();
showFor.updateUI(); showFor.updateUI();
}); });
@ -182,7 +178,6 @@ describe('ShowFor', () => {
}); });
describe('updateState', () => { describe('updateState', () => {
mochaBrowserDetect();
beforeEach(() => { beforeEach(() => {
showFor.loadData(); showFor.loadData();
@ -242,7 +237,6 @@ describe('ShowFor', () => {
}); });
describe('initShowFuncs', () => { describe('initShowFuncs', () => {
mochaBrowserDetect();
beforeEach(() => { beforeEach(() => {
sinon.stub(showFor, 'matchesCriteria'); sinon.stub(showFor, 'matchesCriteria');
@ -274,7 +268,6 @@ describe('ShowFor', () => {
}); });
describe('showAndHide', () => { describe('showAndHide', () => {
mochaBrowserDetect();
beforeEach(() => { beforeEach(() => {
showFor.loadData(); showFor.loadData();
@ -302,7 +295,6 @@ describe('ShowFor', () => {
}); });
describe('matchesCriteria', () => { describe('matchesCriteria', () => {
mochaBrowserDetect();
beforeEach(() => { beforeEach(() => {
showFor.loadData(); showFor.loadData();

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

@ -1,26 +1,11 @@
import React from 'react'; import React from 'react';
import {default as mochaJsdom, rerequire} from 'mocha-jsdom';
import {expect} from 'chai'; import {expect} from 'chai';
import sinon from 'sinon';
import mochaGettext from './fixtures/mochaGettext.js'; import TagsFilter from "sumo/js/tags.filter";
import mochaK from './fixtures/mochaK.js';
import mochaJquery from './fixtures/mochaJquery.js';
import mochaUnderscore from './fixtures/mochaUnderscore.js';
describe('k', () => { describe('k', () => {
let form;
mochaJsdom({useEach: true, url: 'http://localhost'});
mochaJquery();
mochaK();
mochaUnderscore();
/* globals window, $, k */
describe('TagsFilter', () => { describe('TagsFilter', () => {
beforeEach(() => { beforeEach(() => {
rerequire('../tags.filter.js');
let sandbox = ( let sandbox = (
<div> <div>
<section className="tag-filter"> <section className="tag-filter">
@ -41,7 +26,7 @@ describe('k', () => {
); );
React.render(sandbox, window.document.body); React.render(sandbox, window.document.body);
k.TagsFilter.init($('body')); TagsFilter.init($('body'));
// Don't let forms submit // Don't let forms submit
$('form').submit((e) => e.preventDefault()); $('form').submit((e) => e.preventDefault());
}); });

1383
package-lock.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

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

@ -32,9 +32,10 @@
"browser-sync:docs": "browser-sync start --no-open --serveStatic \"styleguide/build\" --files \"styleguide/build/**/*\" --port 4000 --reload-delay=300", "browser-sync:docs": "browser-sync start --no-open --serveStatic \"styleguide/build\" --files \"styleguide/build/**/*\" --port 4000 --reload-delay=300",
"start": "concurrently --raw --kill-others \"npm run webpack:watch\" \"npm run browser-sync\"", "start": "concurrently --raw --kill-others \"npm run webpack:watch\" \"npm run browser-sync\"",
"lint:webpack": "npx eslint --no-eslintrc -c webpack/eslintrc.js kitsune", "lint:webpack": "npx eslint --no-eslintrc -c webpack/eslintrc.js kitsune",
"webpack:build": "npx webpack build --mode development", "webpack:build": "npx webpack build --config webpack.dev.js",
"webpack:build:prod": "npx webpack build --mode production", "webpack:build:prod": "npx webpack build --config webpack.prod.js",
"webpack:watch": "npx webpack watch --mode development" "webpack:watch": "npx webpack watch --config webpack.dev.js",
"webpack:test": "npx webpack build --config webpack.test.js && npx mocha --require ./webpack/mocha-require dist/tests.js"
}, },
"license": "MPL-2.0", "license": "MPL-2.0",
"dependencies": { "dependencies": {
@ -46,7 +47,7 @@
"fontawesome": "^4.3.0", "fontawesome": "^4.3.0",
"jquery": "1.11.3", "jquery": "1.11.3",
"jquery-ui": "1.12.1", "jquery-ui": "1.12.1",
"nunjucks": "^1.3.4", "nunjucks": "^3.2.3",
"react": "0.13.3", "react": "0.13.3",
"underscore": "^1.13.1" "underscore": "^1.13.1"
}, },
@ -74,17 +75,17 @@
"eslint-plugin-import": "^2.25.2", "eslint-plugin-import": "^2.25.2",
"exports-loader": "^3.0.0", "exports-loader": "^3.0.0",
"expose-loader": "^3.0.0", "expose-loader": "^3.0.0",
"glob": "^7.2.0",
"html-webpack-plugin": "^5.3.2", "html-webpack-plugin": "^5.3.2",
"image-minimizer-webpack-plugin": "^2.2.0", "image-minimizer-webpack-plugin": "^2.2.0",
"imagemin-optipng": "^8.0.0", "imagemin-optipng": "^8.0.0",
"imagemin-svgo": "^9.0.0", "imagemin-svgo": "^9.0.0",
"imports-loader": "^3.0.0", "imports-loader": "^3.0.0",
"jsdom": "^19.0.0",
"kss": "^3.0.0-beta.25", "kss": "^3.0.0-beta.25",
"locutus": "^2.0.15", "locutus": "^2.0.15",
"mini-css-extract-plugin": "^1.6.0", "mini-css-extract-plugin": "^1.6.0",
"mocha": "2.3.2", "mocha": "2.3.2",
"mocha-jsdom": "^2.0.0",
"nunjucks": "^1.3.4",
"onchange": "^6.1.0", "onchange": "^6.1.0",
"path-parse": "^1.0.7", "path-parse": "^1.0.7",
"postcss": "^8.3.5", "postcss": "^8.3.5",
@ -92,8 +93,9 @@
"postcss-loader": "^6.1.1", "postcss-loader": "^6.1.1",
"sass": "^1.23.2", "sass": "^1.23.2",
"sass-loader": "^12.0.0", "sass-loader": "^12.0.0",
"sinon": "1.16.1", "sinon": "12.0.1",
"sinon-chai": "2.8.0", "sinon-chai": "2.8.0",
"source-map-support": "^0.5.21",
"style-loader": "^2.0.0", "style-loader": "^2.0.0",
"stylelint": "^11.1.1", "stylelint": "^11.1.1",
"stylelint-config-recommended-scss": "^3.3.0", "stylelint-config-recommended-scss": "^3.3.0",
@ -102,6 +104,7 @@
"svgo": "^1.3.2", "svgo": "^1.3.2",
"webpack": "^5.38.1", "webpack": "^5.38.1",
"webpack-bundle-analyzer": "^4.4.2", "webpack-bundle-analyzer": "^4.4.2",
"webpack-cli": "^4.7.2" "webpack-cli": "^4.7.2",
"webpack-merge": "^5.8.0"
} }
} }

78
webpack.common.js Normal file
Просмотреть файл

@ -0,0 +1,78 @@
const webpack = require("webpack");
const path = require("path");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
mode: "development",
resolve: {
alias: {
protocol: "@mozilla-protocol/core/protocol",
sumo: path.resolve(__dirname, "kitsune/sumo/static/sumo"),
community: path.resolve(__dirname, "kitsune/community/static/community"),
kpi: path.resolve(__dirname, "kitsune/kpi/static/kpi"),
},
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
},
},
{
test: /\.scss$/,
use: [
MiniCssExtractPlugin.loader,
"css-loader",
"postcss-loader",
"sass-loader",
],
},
{
test: /\.(svg|png|gif|woff2?)$/,
type: "asset/resource",
},
// we copy these libraries from external sources, so define their exports here,
// rather than having to modify them, making updating them more difficult:
exports(
"./kitsune/sumo/static/sumo/js/libs/dnt-helper.js",
"default Mozilla.dntEnabled"
),
exports(
"./kitsune/sumo/static/sumo/js/libs/uitour.js",
"default Mozilla.UITour"
),
],
},
plugins: [
new webpack.ProvidePlugin({
$: "jquery",
jQuery: "jquery",
"window.jQuery": "jquery",
}),
new MiniCssExtractPlugin({
filename: "[name].css",
}),
],
cache: {
type: "filesystem",
},
devtool: "cheap-module-source-map",
output: {
filename: "[name].js",
},
};
function exports(path, exports) {
// export the named variable
return {
test: require.resolve(path),
loader: "exports-loader",
options: {
type: "module",
exports,
},
};
}

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

@ -1,90 +0,0 @@
const webpack = require("webpack");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CopyPlugin = require("copy-webpack-plugin");
const ImageMinimizerPlugin = require("image-minimizer-webpack-plugin");
const AssetJsonPlugin = require("./webpack/asset-json-plugin");
const aliases = require("./webpack/aliases");
const entrypoints = require("./webpack/entrypoints");
const entrypointsHtml = require("./webpack/entrypoints-html");
const exportRules = require("./webpack/export-rules");
const assetModuleFilename = "[name].[contenthash][ext]";
module.exports = (env, argv) => {
const dev = argv.mode === "development";
const config = {
resolve: {
alias: aliases,
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
},
},
{
test: /\.scss$/,
use: [
MiniCssExtractPlugin.loader,
"css-loader",
"postcss-loader",
"sass-loader",
],
},
{
test: /\.(svg|png|gif|woff2?)$/,
type: "asset/resource",
},
...exportRules,
],
},
entry: entrypoints,
plugins: [
new webpack.ProvidePlugin({
$: "jquery",
jQuery: "jquery",
"window.jQuery": "jquery",
}),
new MiniCssExtractPlugin({
filename: dev ? "[name].css" : "[name].[contenthash].css",
}),
...entrypointsHtml,
new CopyPlugin({
patterns: [
{ from: "node_modules/@mozilla-protocol/core/protocol/img/icons/**", to: assetModuleFilename },
{ from: "kitsune/*/static/**/img/**", to: assetModuleFilename },
],
}),
new ImageMinimizerPlugin({
minimizerOptions: {
plugins: [
"optipng",
"svgo",
]
}
}),
new AssetJsonPlugin(),
],
output: {
filename: dev ? "[name].js" : "[name].[contenthash].js",
assetModuleFilename: assetModuleFilename,
},
cache: dev ? { type: "filesystem" } : false,
optimization: {
splitChunks: {
chunks: 'all',
},
},
};
if (dev) {
// eval source maps don't work with our css loaders
config.devtool = "cheap-module-source-map";
}
return config;
};

40
webpack.dev.js Normal file
Просмотреть файл

@ -0,0 +1,40 @@
const { merge } = require("webpack-merge");
const CopyPlugin = require("copy-webpack-plugin");
const ImageMinimizerPlugin = require("image-minimizer-webpack-plugin");
const AssetJsonPlugin = require("./webpack/asset-json-plugin");
const common = require("./webpack.common.js");
const entrypoints = require("./webpack/entrypoints");
const entrypointsHtml = require("./webpack/entrypoints-html");
const assetModuleFilename = "[name].[contenthash][ext]";
module.exports = merge(common, {
entry: entrypoints,
plugins: [
...entrypointsHtml,
new CopyPlugin({
patterns: [
{
from: "node_modules/@mozilla-protocol/core/protocol/img/icons/**",
to: assetModuleFilename,
},
{ from: "kitsune/*/static/**/img/**", to: assetModuleFilename },
],
}),
new ImageMinimizerPlugin({
minimizerOptions: {
plugins: ["optipng", "svgo"],
},
}),
new AssetJsonPlugin(),
],
optimization: {
splitChunks: {
chunks: "all",
},
},
output: {
assetModuleFilename: assetModuleFilename,
},
});

24
webpack.prod.js Normal file
Просмотреть файл

@ -0,0 +1,24 @@
const { mergeWithCustomize, unique } = require("webpack-merge");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const dev = require("./webpack.dev.js");
module.exports = mergeWithCustomize({
customizeArray: unique(
"plugins",
["MiniCssExtractPlugin"],
(plugin) => plugin.constructor && plugin.constructor.name
),
})(dev, {
mode: "production",
plugins: [
new MiniCssExtractPlugin({
filename: "[name].[contenthash].css",
}),
],
cache: false,
devtool: false,
output: {
filename: "[name].[contenthash].js",
},
});

11
webpack.test.js Normal file
Просмотреть файл

@ -0,0 +1,11 @@
const { merge } = require("webpack-merge");
const glob = require("glob");
const common = require("./webpack.common.js");
module.exports = merge(common, {
target: "node",
entry: {
tests: [...glob.sync("./kitsune/*/static/*/js/tests/*.js")],
},
});

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

@ -1,8 +0,0 @@
const path = require("path");
module.exports = {
protocol: "@mozilla-protocol/core/protocol",
sumo: path.resolve(__dirname, "../kitsune/sumo/static/sumo"),
community: path.resolve(__dirname, "../kitsune/community/static/community"),
kpi: path.resolve(__dirname, "../kitsune/kpi/static/kpi"),
};

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

@ -3,7 +3,11 @@ module.exports = {
"plugin:import/recommended", "plugin:import/recommended",
], ],
"settings": { "settings": {
"import/resolver": "webpack", "import/resolver": {
"webpack": {
"config": "./webpack.common.js",
},
},
}, },
"env": { "env": {
"es6": true, "es6": true,

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

@ -1,24 +0,0 @@
module.exports = [
// we copy these libraries from external sources, so define their exports here,
// rather than having to modify them, making updating them more difficult:
exports(
"../kitsune/sumo/static/sumo/js/libs/dnt-helper.js",
"default Mozilla.dntEnabled"
),
exports(
"../kitsune/sumo/static/sumo/js/libs/uitour.js",
"default Mozilla.UITour"
),
];
function exports(path, exports) {
// export the named variable
return {
test: require.resolve(path),
loader: "exports-loader",
options: {
type: "module",
exports,
},
};
}

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

@ -1,15 +1,21 @@
require("@babel/register")({ require('source-map-support').install();
plugins: [
[
"module-resolver",
{
// make babel resolve our webpack aliases in tests
alias: require("./aliases"),
},
],
],
});
// make images imports return null, we don't need them in tests const jsdom = require("jsdom");
require.extensions[".svg"] = () => null; const { JSDOM } = jsdom;
require.extensions[".png"] = () => null; const dom = new JSDOM("<html></html>", {
url: "https://example.com",
referrer: "http://google.com/?q=cookies",
});
global.window = dom.window;
global.document = dom.window.document;
global.navigator = dom.window.navigator;
global.sessionStorage = dom.window.sessionStorage;
global.history = dom.window.history;
global.Element = dom.window.Element;
global.matchMedia = () => ({
matches : false,
addListener : () =>{},
removeListener: () =>{},
});
global.jQuery = global.$ = require("jquery");
require("../kitsune/sumo/static/sumo/js/i18n");