User:Evad37/WikiUnit.js

/** * 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  * - GFDL  * - 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 */ // $.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": $("").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(				$(' ').attr('id', 'qunit'),				$(' ').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);