/**
* WikiUnit
* On-wiki unit testing for gadgets and user scripts, based on QUnit
*
* Tests are defined on a /testcases.js subpage of a javascript page (i.e.
* gadget or userscript). The /testcases.js script contains `QUnit.test`s,
* perhaps grouped into `QUnit.module`s. See the QUnit cookbook[1] and
* documentation[2] for more details.
* [1] https://qunitjs.com/cookbook/
* [2] https://api.qunitjs.com/
*
* To run tests, edit the script and click "Show preview" or "Show changes", or
* visit the /testcases.js subpage and click the "Run tests" tab. If changes
* have been made, tests will run against the changed (not yet saved) code.
*
* Testcases execute in the same scope as the script they test, and can access
* any functions or variables declared in the scripts outer scope. Alternatively
* properties can be added to the `window` object, which can be accessed by
* both the testcases and the script being tested – but be careful to use unique
* names that are unlikely to conflict with outher scripts.
*
* Gadgets which need to load ResourceLoader dependencies should specify those
* depenedencies in a comment in the top of the script, like the one on line 59
* of this script.
*
* For an example, see [[User:Evad37/extra/sandbox.js/testcases.js]]
*
* == Licenses ==
* This script is avialable under the following licenses (you may select the
* license of your choice):
* - CC BY-SA 3.0 License <https://en.wikipedia.org/wiki/WP:CC_BY-SA>
* - GFDL <https://en.wikipedia.org/wiki/WP:FDL>
* - MIT license (see below)
*
* MIT license
* Copyright (c) 2019 Evad37
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/* jshint esversion: 5, laxbreak: true, undef: true, maxerr:999 */
/* globals window, mw, $, OO, QUnit, importStylesheet */
// Example comment to specify ResourceLoader dependencies (usually only needed for gadgets):
/* wikiunit dependencies=mediawiki.api,mediawiki.util,oojs-ui-core,oojs-ui-windows */
// <nowiki>
$.when(
mw.loader.using([
"mediawiki.api", "mediawiki.util", "oojs-ui-core", "oojs-ui-windows",
]),
$.ready
).then(function() {
var config = {
version: "1.0.0",
mw: mw.config.get([
"wgAction",
"wgPageContentModel",
"wgPageName",
"wgRelevantUserName",
"wgUserName",
"wgServer",
"wgUserLanguage",
"wgDBname",
"skin"
]),
};
config.testPageName = config.mw.wgPageName + "/testcases.js";
config.api = new mw.Api( {
ajax: {
headers: {
"Api-User-Agent": "WikiUnit/" + config.version +
" ( https://en.wikipedia.org/wiki/User:Evad37/WikiUnit )"
}
}
} );
// Storing messages here, in English, pending a sane way of doing i18n
var msg = {
"tab-run-text": "Run tests",
"tab-run-tooltip": "Run unit tests",
"btn-previewAndTests": "Show preview & tests",
"btn-changesAndTests": "Show changes & tests",
"confirm-title": "Run unit tests?",
"confirm-message":
"The code on this page, as well as `$1`, will be executed to run "+
"unit tests. If you are unsure whether the code is safe, you can "+
"ask at the appropriate village pump.", // $1 is the page where unit tests are defined
"action-cancel-label": "Back to safety",
"action-accept-label": "Continue",
"heading-unittesting": "Unit testing – $1" // $1 is the page where unit tests are defined
};
function addRunTestsTab() {
var portlet;
var nextNode;
var runTestsUrl = mw.util.getUrl(config.mw.wgPageName.replace(/\/testcases\.js$/, ""), { action: 'submit' });
switch(config.mw.skin) {
case "monobook":
case "modern":
portlet = "p-cactions";
nextNode = "#ca-edit";
break;
case "cologneblue":
portlet = "p-pageoptions";
break;
case "minerva":
$("<a>").text( msg["tab-run-text"] ).attr({
"href": runTestsUrl,
"title": msg["tab-run-tooltip"]
}).appendTo( $(".minerva__tab-container").length
? ".minerva__tab-container"
: "#language-selector"
);
return;
default: // Vector, Timeless
portlet = "p-namespaces";
break;
}
mw.util.addPortletLink(
portlet,
runTestsUrl,
msg["tab-run-text"],
"wikiunit-runtests",
msg["tab-run-tooltip"],
"_",
nextNode
);
}
// If on a /testcases.js subpage, add a Run tests tab
if (/\/testcases\.js$/.test(config.mw.wgPageName) && config.mw.wgPageContentModel === "javascript") {
addRunTestsTab();
}
// Check if previewing a javascript page
if (config.mw.wgPageContentModel !== "javascript") {
return;
}
// Check if /testcases.js exists and is a javascript page
return config.api.get({
action: "query",
format: "json",
prop: "info|revisions",
rvprop: "content",
rvslots: "main",
titles: config.testPageName,
formatversion: "2"
}).then(function(response) {
var page = response.query.pages[0];
if (!page || page.missing || page.contentmodel !== "javascript" ) {
return;
}
addRunTestsTab();
$("#wpPreview").attr("value", msg["btn-previewAndTests"]);
$("#wpDiff").attr("value", msg["btn-changesAndTests"]);
if (config.mw.wgAction !== "submit") {
return;
}
// Warn only if not in MediaWiki namespace and not in your own namespace
var skipWarning = (page.ns === 8 || config.mw.wgRelevantUserName === config.mw.wgUserName);
return $.when(skipWarning || OO.ui.confirm(
msg["confirm-message"].replace(/\$1/g, config.testPageName),
{
title: msg["confirm-title"],
size: "medium",
actions: [
{
action: "accept",
label: msg["action-accept-label"],
flags: "progressive"
},
{
action: "cancel",
label: msg["action-cancel-label"],
flags: "safe"
},
]
})
).then(function(confirmed) {
if (!confirmed) {
return;
}
// Load QUnit. TODO: should be a hidden gadget in MediaWiki namespace
importStylesheet("User:Evad37/qunit-2.8.0.css");
mw.util.$content.prepend(
$('<div>').attr('id', 'qunit'),
$('<div>').attr('id', 'qunit-fixture')
);
window.QUnit = { config: { autostart: false } };
return mw.loader.getScript(
'https://en.wikipedia.org/w/index.php?title=' +
'User:Evad37/qunit-2.8.0.js' +
'&action=raw&ctype=text/javascript'
).then(function() {
QUnit.on( "runEnd", function() {
$('#qunit-header a').text(
msg["heading-unittesting"].replace(/\$1/g, config.testPageName)
).attr({
'href': mw.util.getUrl(config.testPageName),
target:'_blank'
});
});
// Evaluate textbox content and testcases content as javascript,
// so they can execute in the same scope. First get any
// depenedencies that are specified in a wikiunit comment.
var wikiunitComment = /^\/\*\s*wikiunit\s+(.+)\s*\*\/$/m.exec(
$("#wpTextbox1").textSelection("getContents")
);
var dependencies = wikiunitComment && wikiunitComment[1] &&
/dependencies\s*=\s*(\S+)/.exec(wikiunitComment[1]);
var dependenciesList = dependencies && dependencies[1];
// Will need to wait for depenedecies, if there are any
var wrapTop = dependenciesList
? 'mw.loader.using(["' + dependenciesList.replace(/,/g, '","') + '"]).then(function() { \n'
: "$.Deferred().resolve().then(function() { \n";
var wrapBottom = "});";
var textboxContent = $("#wpTextbox1").textSelection("getContents") + "\n";
var testcasesContent = page.revisions[0].slots.main.content + "\n";
// Eval wrapped contents
var evaluatedPromise = eval( // jshint ignore:line
wrapTop + textboxContent + testcasesContent + wrapBottom
);
if (!evaluatedPromise) {
throw new Error("[WikiUnit] Failed to evaluate scripts");
}
evaluatedPromise.then(QUnit.start);
});
});
});
}).catch(console.error);