User:Evad37/WikiUnit.js

From Wikipedia, the free encyclopedia
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
/**
 * 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);