Jump to content

User:Evad37/Covery/sandbox.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.
/* jshint esversion: 6, laxbreak: true, undef: true, eqnull: true, maxerr: 999 */
/* globals console, document, File, FileReader, fetch, window, $, mw, OO */
// <nowiki>
var setupCovery = function setupCovery() {
	var SCRIPT = {
		name: 'Covery',
		version: '1.5.0',
		ad: ' (using [[User:Evad37/Covery|Covery]])'
	};

	function isSuitable() {
		var config = mw.config.get(['wgAction', 'wgDiffOldId', 'wgNamespaceNumber', 'wgPageName']);
		if (
			config.wgAction !== 'view' ||
			config.wgNamespaceNumber !== 0 ||
			config.wgDiffOldId !== null
		) {
			return $.Deferred().reject();
		}
		return config;
	}

	var getLeadWikitext = function getLeadWikitext(api, pageName) {
		return api
			.get({
				action: 'parse',
				format: 'json',
				page: pageName,
				prop: 'wikitext',
				section: '0'
			})
			.then(function(response) {
				return response.parse.wikitext['*'];
			});
	};

	/**
	 *
	 * @param {String} wikitext parameters section from a template, including pipes before each parameter name inside braces `{{...}}`
	 */
	var getTemplateParameters = function getTemplateParameters(wikitext) {
		var params = {};
		var unnamedParamCount = 0;
		var templateParamsPattern = /\|(?!(?:[^{]+}|[^\[]+]))(?:.|\s)*?(?=(?:\||$)(?!(?:[^{]+}|[^\[]+])))/g;
		var parts = wikitext.match(templateParamsPattern);
		return parts.map(function(part, position) {
			var isEmptyParameter = part.trim() === '|'; //  i.e. first parameter of {{foo||bar}
			if (isEmptyParameter) {
				unnamedParamCount++;
				return {
					name: unnamedParamCount.toString(),
					value: '',
					wikitext: {
						name: '|',
						value: part.slice(1)
					}
				};
			}
			var equalsIndex = part.indexOf('=');
			var bracesIndex = part.indexOf('{{');

			var hasNoEqualsSign = equalsIndex === -1;
			var firstEqualsSignWithinBraces = bracesIndex !== -1 && bracesIndex < equalsIndex;
			var isUnnamedParameter = hasNoEqualsSign || firstEqualsSignWithinBraces;
			if (isUnnamedParameter) {
				unnamedParamCount++;
				return {
					name: unnamedParamCount.toString(),
					value: part.slice(1).trim(),
					wikitext: {
						name: '|',
						value: part.slice(1)
					}
				};
			} else {
				return {
					name: part.slice(1, equalsIndex).trim(),
					value: part.slice(equalsIndex + 1).trim(),
					wikitext: {
						name: part.slice(0, equalsIndex + 1),
						value: part.slice(equalsIndex + 1)
					}
				};
			}
		});
	};

	var getInfoboxTemplate = function getInfoboxTemplate(wikitext) {
		var infoboxPattern = /\{\{\s*(.*?[Ii]nfobox.*?|[Ii]OS App)\s*(\|(?:.|\n)*?(?:(?:\{\{(?:.|\n)*?(?:(?:\{\{(?:.|\n)*?\}\})(?:.|\n)*?)*?\}\})(?:.|\n)*?)*|)\}\}\n?/;
		var infoboxParts = infoboxPattern.exec(wikitext);
		if (!infoboxParts || !infoboxParts[0] || !infoboxParts[1]) {
			throw new Error('Unable to parse infobox from wikitext `' + wikitext + '`');
		}
		var name = infoboxParts[1];
		var params = infoboxParts[2] ? getTemplateParameters(infoboxParts[2]) : [];
		return {
			name: name,
			params: params,
			wikitext: infoboxParts[0]
		};
	};

	var toSentanceCase = function toSentanceCase(text) {
		return text.slice(0, 1).toUpperCase() + text.slice(1);
	};

	var checkInfobox = function checkInfobox(infobox) {
		var videoGameInfoboxTemplates = [
			'Infobox video game',
			'Infobox Arcade Game',
			'Infobox Videogame',
			'Infobox cvg',
			'Infobox Arcade game',
			'Infobox arcade',
			'GameInfobox',
			'Infobox CVG',
			'Infobox vg',
			'Infobox Video Game',
			'Infobox VG',
			'IOS App',
			'Infobox video games',
			'Vg infobox',
			'Infobox videogame',
			'Infobox console game',
			'Infobox computer game',
			'Videogame infobox',
			'Video game infobox',
			'Infobox video game series',
			'Infobox VG series',
			'Infobox video game franchise'
		];
		var infoboxName = toSentanceCase(infobox.name);
		if (!videoGameInfoboxTemplates.includes(infoboxName)) {
			throw new Error('{{Infobox video game}} not found.');
		}
		var imageParam = infobox.params.find(paramByName('image'));
		if (imageParam && imageParam.value) {
			throw new Error('Infobox already has an image!');
		}
		return infobox;
	};

	/**
	 * @param {File} file source file
	 * @param {Number} maxResolution maximum resolution in pixels
	 * @returns {Promise} Promise of (1) a File with the given max resoltion, and (2) a data url of the resized image
	 **/
	var resizeImageFile = function resizeImageFile(file, maxResolution) {
		var resizeFilePromise = $.Deferred();

		var origImg = document.createElement('img');

		var reader = new FileReader();
		reader.onload = function(e) {
			origImg.addEventListener(
				'load',
				function() {
					var canvas = document.createElement('canvas');
					var ctx = canvas.getContext('2d');
					ctx.drawImage(origImg, 0, 0);
					var resolution = origImg.width * origImg.height;
					var scaleFactor =
						resolution > maxResolution ? Math.sqrt(maxResolution / resolution) : 1;
					var width = origImg.width * scaleFactor;
					var height = origImg.height * scaleFactor;
					canvas.width = width;
					canvas.height = height;
					ctx = canvas.getContext('2d');
					ctx.drawImage(origImg, 0, 0, width, height);

					var dataurl = canvas.toDataURL(file.type);

					canvas.toBlob(function(blob) {
						resizeFilePromise.resolve(
							new File([blob], file.name, { type: file.type }),
							dataurl
						);
					}, file.type);
				},
				false
			);
			origImg.src = e.target.result;
		};
		reader.readAsDataURL(file);

		return resizeFilePromise.promise();
	};

	/**
	 *
	 * @param {String} articleTitle
	 * @param {String} developer
	 * @param {String} publisher
	 * @param {String[]} platforms
	 */
	var makeDescriptionText = function makeDescriptionText(
		articleTitle,
		developer,
		publisher,
		platforms
	) {
		var platformsParams = platforms.reduce(function(params, platform) {
			return params + '|' + platform;
		}, '');
		return (
			'==Summary==\n{{Non-free use rationale video game cover\n' +
			'| Article = ' +
			articleTitle.getPrefixedText() +
			'\n' +
			'| Use = Infobox\n' +
			'| Publisher = ' +
			publisher +
			'\n' +
			'| Developer = ' +
			developer +
			'\n}}\n' +
			'==Licensing==\n{{Non-free video game cover' +
			platformsParams +
			'}}'
		);
	};

	/**
	 * @param {Object} api
	 * @param {File} file
	 * @param {String} text wikitext for the file description page
	 * @param {Object} title mw.Title object
	 * @returns {Promise} Promise of result object, or an error code and a jqxhr object
	 */
	var uploadFile = function uploadFile(api, file, text, title) {
		var filename = title.getMain();
		return api
			.postWithToken(
				'csrf',
				{
					action: 'upload',
					format: 'json',
					filename: filename,
					comment: 'Upload cover image' + SCRIPT.ad,
					text: text,
					file: file
				},
				{ contentType: 'multipart/form-data' }
			)
			.then(function(response) {
				/* on success, will get an object like:
            { upload:
                filename: "Image_page_sandbox_1000x596.png",
                imageinfo: {
                    bitdepth: 8,
                    canonicaltitle: "File:Image page sandbox 1000x596.png",
                    ...
                },
                result: "Success"
            }
            */
				if (response && response.upload && response.upload.result === 'Success') {
					return true;
				}
				return $.Deferred().reject('API failed to upload file');
			});
	};

	var createFileTalkpage = function(api, fileTitle) {
		return api.postWithToken('csrf', {
			action: 'edit',
			format: 'json',
			title: fileTitle.getTalkPage().toString(),
			text: '{{WikiProject Video games}}',
			summary: 'WikiProject tagging (using [[User:Evad37/Covery|Covery]])',
			createonly: true
		});
	};

	/**
	 * @param {String} pageTitle
	 * @returns {Promise} {wikitext: {String} Revision wikitext, timestamp: {String} last edit timestamp}
	 */
	var getRevisionWikitext = function getRevisionWikitext(api, pageTitle) {
		return api
			.get({
				action: 'query',
				format: 'json',
				prop: 'revisions',
				titles: pageTitle,
				rvprop: 'timestamp|content',
				rvslots: 'main'
			})
			.then(function(response) {
				return $.map(response.query.pages, function(page) {
					return {
						wikitext: page.revisions[0].slots.main['*'],
						timestamp: page.revisions[0].timestamp
					};
				})[0];
			});
	};

	var paramByName = function paramByName(name) {
		return function(param) {
			return param.name === name;
		};
	};

	var makeInfoboxWikitext = function makeInfoboxWikitext(originalInfobox, newParameters) {
		var updatedParametersWikitext = originalInfobox.params.map(function(param) {
			var updatedParam = newParameters.find(paramByName(param.name));
			return (
				param.wikitext.name +
				(updatedParam ? ' ' + updatedParam.value + '\n' : param.wikitext.value)
			);
		});
		var originalParametersList = originalInfobox.params.map(function(param) {
			return param.name;
		});
		var parametersToAddWikitext = newParameters
			.filter(function(param) {
				return !originalParametersList.includes(param.name);
			})
			.map(function(param) {
				return '|' + param.name + ' = ' + param.value + '\n';
			});

		return (
			'{{' +
			originalInfobox.name +
			'\n' +
			updatedParametersWikitext.join('') +
			parametersToAddWikitext.join('') +
			'}}'
		);
	};

	var updateWikitext = function(revisionWikitext, infobox, updatedParams) {
		if (revisionWikitext.indexOf(infobox.wikitext) === -1) {
			return $.Deferred().reject('Edit conflict');
		}
		var newInfobox = makeInfoboxWikitext(infobox, updatedParams);
		return revisionWikitext.replace(infobox.wikitext, newInfobox);
	};

	var editPage = function(api, pageTitle, wikitext, timestamp) {
		return api.postWithToken('csrf', {
			action: 'edit',
			title: pageTitle,
			text: wikitext,
			summary: 'Added cover image (using [[User:Evad37/Covery|Covery]])',
			basetimestamp: timestamp,
			nocreate: true
		});
	};

	var updatePage = function updatePage(api, page, infobox, updatedParams) {
		return getRevisionWikitext(api, page)
			.then(function(revision) {
				return $.when(
					updateWikitext(revision.wikitext, infobox, updatedParams),
					revision.timestamp
				);
			})
			.then(function(updatedWikitext, timestamp) {
				return editPage(api, page, updatedWikitext, timestamp);
			});
	};

	var updateTalkpageWikitext = function updateTalkpageWikitext(revisionWikitext) {
		/* Redirects to {{WikiProject Video games}} :
        //    Template:Cvgproj',
        //    Template:WikiProject Video Games',
        //    Template:WPVG',
        //    Template:Vgproj',
        //    Template:Wpvg',
        //    Template:WP video games',
        //    Template:WP cvg',
        //    Template:WikiProject Rockstar Games',
        //    Template:WGVG',
        //    Template:WP Video games',
        //    Template:WikiProject VG',
        //    Template:WikiProject video games (redirect page)
        */
		var bannerPattern = /\{\{\s*([Ww](?:P|p|G|ikiProject) ?c?[Vv](?:ideo )?[Gg](?:ames)?|[Cc]?[Vv]gproj|[Ww]ikiProject Rockstar Games)\s*(\|(?:.|\n)*?(?:(?:\{\{(?:.|\n)*?(?:(?:\{\{(?:.|\n)*?\}\})(?:.|\n)*?)*?\}\})(?:.|\n)*?)*|)\}\}\n?/;
		var banner = bannerPattern.exec(revisionWikitext);
		var noBannerPrersent = !banner || !banner[0];
		if (noBannerPrersent) {
			return '{{WikiProject Video games}}\n' + revisionWikitext;
		}
		var noParamsInBanner = !banner[2];
		if (noParamsInBanner) {
			return false;
		}
		var params = getTemplateParameters(banner[2]);
		var coverParam = getTemplateParameters(banner[2]).find(paramByName('cover'));
		if (!coverParam) {
			return false;
		}
		var updatedBannerWikitext = banner[0].replace(
			coverParam.wikitext.name + coverParam.wikitext.value,
			''
		);
		return revisionWikitext.replace(banner[0], updatedBannerWikitext);
	};

	var updateTalkpage = function updateTalkpage(api, page) {
		var talkpageTitle = mw.Title.newFromText(page).getTalkPage();
		var talkpage = talkpageTitle && talkpageTitle.toString();
		return getRevisionWikitext(api, talkpage)
			.then(function(revision) {
				return $.when(updateTalkpageWikitext(revision.wikitext), revision.timestamp);
			})
			.then(function(updatedWikitext, timestamp) {
				if (!updatedWikitext) {
					return 'Done';
				}
				return editPage(api, talkpage, updatedWikitext, timestamp);
			});
	};

	var CoveryDialog = function CoveryDialog(config) {
		CoveryDialog.super.call(this, config);
	};
	OO.inheritClass(CoveryDialog, OO.ui.ProcessDialog);

	CoveryDialog.static.name = 'coveryDialog';
	CoveryDialog.static.title = 'Covery';
	CoveryDialog.static.size = 'large';
	CoveryDialog.static.actions = [
		{ flags: ['primary', 'progressive'], label: 'Upload', action: 'upload' },
		{ flags: 'safe', label: 'Cancel' }
	];

	// Customize the initialize() function to add content and layouts:
	CoveryDialog.prototype.initialize = function() {
		CoveryDialog.super.prototype.initialize.call(this);

		this.panel = new OO.ui.PanelLayout({ padded: true, expanded: false });

		/* Form content: */
		this.content = new OO.ui.FieldsetLayout();

		this.fileSelect = new OO.ui.SelectFileWidget({
			droppable: true,
			showDropTarget: true,
			// thumbnailSizeLimit: 0,
			$element: $("<div style='background: #eee;'>")
		});
		// this.fileSelect.$element
		// 	.find(".oo-ui-selectFileWidget-dropTarget").css({"height":"auto"});
		// this.fileSelect.$element
		// 	.find(".oo-ui-selectFileWidget-thumbnail").css({"display":"none"});
		// this.fileSelect.$element
		// 	.find(".oo-ui-selectFileInputWidget-info").css({"margin":"0"});
		
		this.urlInput = new OO.ui.TextInputWidget({
			type: 'url',
			placeholder: 'http://'
		});
		this.imagePreview = new OO.ui.LabelWidget({ label: '...' });
		this.titleInput = new OO.ui.TextInputWidget({ required: true });
		this.captionInput = new OO.ui.TextInputWidget();
		this.altTextInput = new OO.ui.TextInputWidget();
		this.developerInput = new OO.ui.TextInputWidget({ required: true });
		this.publisherInput = new OO.ui.TextInputWidget({ required: true });
		this.platformInput = new OO.ui.MenuTagMultiselectWidget({
			inputPosition: 'inline',
			allowDisplayInvalidTags: true,
			allowArbitrary: true
		});

		this.fileSelectField = new OO.ui.FieldLayout(this.fileSelect, {
			label: 'Upload a file...',
			align: 'top'
		});
		this.fileSelect.field = this.fileSelectField;
		this.urlInputField = new OO.ui.FieldLayout(this.urlInput, {
			label: '...or enter a URL',
			align: 'left'
		});
		this.urlInput.field = this.urlInputField;
		this.imagePreviewField = new OO.ui.FieldLayout(this.imagePreview, {
			label: 'Preview:',
			align: 'top'
		});
		this.titleInputField = new OO.ui.FieldLayout(this.titleInput, {
			label: 'File name',
			align: 'top'
		});
		this.titleInputField.$element
			.find(".oo-ui-fieldLayout-messages").css({"margin-top":"2em"}); // prevent errors overlapping input
		this.captionInputField = new OO.ui.FieldLayout(this.captionInput, {
			label: 'Caption',
			align: 'left'
		});
		this.altTextInputField = new OO.ui.FieldLayout(this.altTextInput, {
			label: 'Alt text',
			align: 'left'
		});
		this.developerInputField = new OO.ui.FieldLayout(this.developerInput, {
			label: 'Developer',
			align: 'left'
		});
		this.publisherInputField = new OO.ui.FieldLayout(this.publisherInput, {
			label: 'Publisher',
			align: 'left'
		});
		this.platformInputField = new OO.ui.FieldLayout(this.platformInput, {
			label: 'Platform(s)',
			align: 'left'
		});

		this.content.addItems([
			this.fileSelectField,
			// this.urlInputField, 
			this.titleInputField,
			//this.imagePreviewField,
			this.captionInputField,
			this.altTextInputField,
			this.developerInputField,
			this.publisherInputField,
			this.platformInputField
		]);

		/* Progress status content: */
		this.progressStatusContent = new OO.ui.FieldsetLayout({
			label: 'Status'
		});
		this.progressBar = new OO.ui.ProgressBarWidget({
			progress: 0
		});
		this.progressField = new OO.ui.FieldLayout(this.progressBar, {
			label: '',
			align: 'below'
		});
		this.progressStatusContent.addItems([this.progressField]);
		this.progressStatusContent.toggle(false); //hide

		this.panel.$element.append([this.content.$element, this.progressStatusContent.$element]);
		this.$body.append(this.panel.$element);

		this.fileSelect.connect(
			this,
			{ change: 'onFileSelectChange' }
		);
		this.urlInput.connect(
			this,
			{ change: 'onUrlInputChange' }
		);
		this.titleInput.connect(
			this,
			{ flag: 'onTitleInputFlag' }
		);
		this.developerInput.connect(
			this,
			{ change: 'onRequiredInputChange' }
		);
		this.publisherInput.connect(
			this,
			{ change: 'onRequiredInputChange' }
		);
		this.platformInput.connect(
			this,
			{ add: 'onPlatformInputAdd' }
		);
		(function(self) {
			self.platformInput.$element.find('input').on('blur', function() {
				self.onPlatformInputBlur.call(self);
			});
		})(this);
	};

	CoveryDialog.prototype.onFileChosen = function(filePromise, fileName, widgetUsed, otherWidget) {
		widgetUsed.pushPending();
		widgetUsed.field.setErrors([]);
		otherWidget.setDisabled(true);
		var self = this;
		$.when(filePromise)
			.then(function(file) {
				return resizeImageFile(file, 100000);
			})
			.then(
				function(resizedFile, resizedDataURL) {
					self.resizedFile = resizedFile;
					self.imagePreview.$element
						.empty()
						.show()
						.append($('<img>').attr('src', resizedDataURL));
					self.updateSize();
					widgetUsed.popPending();
					if (widgetUsed.setIndicator) {
						widgetUsed.setIndicator('required');
					}
					otherWidget.setDisabled(false);
					if (otherWidget.setIndicator) {
						otherWidget.setIndicator(null);
					}
					self.titleInput.setValue(fileName);
					self.onRequiredInputChange();
				},
				function(code) {
					var errorMessage = code
						? 'An error occured: ' + code
						: 'An unexpected error occured';
					self.resizedFile = null;
					widgetUsed.popPending();
					if (widgetUsed.setIndicator) {
						widgetUsed.setIndicator('clear');
					}
					widgetUsed.field.setErrors([errorMessage]);
					otherWidget.setDisabled(false);
					if (otherWidget.setIndicator) {
						otherWidget.setIndicator(null);
					}
					self.onRequiredInputChange();
				}
			);
	};

	CoveryDialog.prototype.onFileSelectChange = function(files) {
		var file = files && files[0];
		if (!file || !file.name) {
			return;
		}
		this.onFileChosen(file, file.name, this.fileSelect, this.urlInput);
	};

	CoveryDialog.prototype.onUrlInputChange = function(value) {
		if (!value) {
			this.urlInput.setIcon(null);
			return;
		}
		var hasImageExtension = /\.(?:gif|png|jpe?g|svg|tiff?)$/i.test(value);
		if (!hasImageExtension) {
			this.urlInput.setIcon('ellipsis');
			return;
		}
		var filePromise = fetch(value, {mode: 'no-cors'}).then(function(result) {
			return result.blob();
		});
		var fileName = value.replace(/^.*\//, '');
		this.onFileChosen(filePromise, fileName, this.urlInput, this.fileSelect);
	};

	CoveryDialog.prototype.onTitleInputFlag = function(flag) {
		if (flag.invalid === true) {
			if (this.titleInput.getValue().length) {
				this.titleInputField.setErrors(['Invalid file name']);
			}
			this.actions.setAbilities({
				upload: false
			});
		} else {
			this.onRequiredInputChange();
		}
	};

	CoveryDialog.prototype.checkMimes = function() {
		var mimeLookup = {
			'.bmp': 'image/bmp',
			'.gif': 'image/gif',
			'.jpeg': 'image/jpeg',
			'.jpg': 'image/jpeg',
			'.png': 'image/png',
			'.svg': 'image/svg+xml',
			'.tif': 'image/tiff',
			'.tiff': 'image/tiff'
		};
		var fileMime = (this.resizedFile && this.resizedFile.type) || '';
		var titleParts = this.titleInput
			.getValue()
			.toLowerCase()
			.match(/.*(\..*)$/, '$1');
		var titleExtension = titleParts && titleParts[1];
		var impliedTitleMime = mimeLookup[titleExtension] || '';
		return fileMime === impliedTitleMime;
	};

	// Only allow uploading if requirements are met
	CoveryDialog.prototype.onRequiredInputChange = function(change) {
		var self = this;
		$.when((change && change.titleIsValid) || this.titleInput.getValidity()).then(
			function() {
				// remove any old title input errors
				self.titleInputField.setErrors([]);
				// check file mime matches title mime
				var titleHasCorrectExtension = self.checkMimes();
				if (!titleHasCorrectExtension && self.resizedFile) {
					self.titleInputField.setErrors([
						'Invalid file extension (file is a ' +
							self.resizedFile.type
								.replace('image/', '')
								.replace(/\+.*$/, '')
								.toUpperCase() +
							' image)'
					]);
				}

				var requirementsMet =
					!self.fileSelect.isPending() &&
					!self.urlInput.isPending() &&
					!!self.resizedFile &&
					!!self.titleInput.getValue().length &&
					titleHasCorrectExtension &&
					!!self.developerInput.getValue().length &&
					!!self.publisherInput.getValue().length;
				self.actions.setAbilities({
					upload: requirementsMet
				});
			},
			function() {
				if (self.titleInput.getValue().length) {
					self.titleInputField.setErrors(['Invalid file name']);
				}
				self.actions.setAbilities({
					upload: false
				});
			}
		);
	};

	CoveryDialog.prototype.onPlatformInputAdd = function(item) {
		this.api
			.get({
				action: 'query',
				format: 'json',
				titles: 'Category:' + item.data + ' game covers'
			})
			.then(function(response) {
				return $.map(response.query.pages, function(page) {
					return page.missing !== '';
				})[0];
			})
			.then(function(isValid) {
				item.toggleValid(isValid);
			});
	};

	CoveryDialog.prototype.onPlatformInputBlur = function() {
		this.platformInput.doInputEnter();
	};

	// Specify the dialog height (or don't to use the automatically generated height).
	CoveryDialog.prototype.getBodyHeight = function() {
		return this.panel.$element.outerHeight(true);
	};

	// Set up the window with data passed to it at the time of opening.
	CoveryDialog.prototype.getSetupProcess = function(data) {
		data = data || {};
		return CoveryDialog.super.prototype.getSetupProcess.call(this, data).next(function() {
			this.uploaded = false;
			this.createdFileTalkpage = false;
			this.updatedArticle = false;

			this.api = data.api;
			this.infobox = data.infobox;
			this.pageName = data.pageName;
			var developerParam = data.infobox.params.find(paramByName('developer'));
			var publisherParam = data.infobox.params.find(paramByName('publisher'));
			this.developerInput.setValue((developerParam && developerParam.value) || '');
			this.publisherInput.setValue((publisherParam && publisherParam.value) || '');
			this.titleInput.setValidation(function(value) {
				var title = mw.Title.newFromFileName(value);
				if (title === null) {
					return false;
				}
				return data.api
					.get({
						action: 'query',
						format: 'json',
						prop: 'imageinfo',
						titles: title.toString(),
						iiprop: ''
					})
					.then(function(response) {
						return $.map(response.query.pages, function(page) {
							return page.missing === '' && page.imagerepository === '';
						})[0];
					});
			});
			var self = this;
			data.api
				.get({
					action: 'query',
					format: 'json',
					list: 'categorymembers',
					cmtitle: 'Category:Video game covers',
					cmprop: 'title',
					cmtype: 'subcat',
					cmlimit: 'max'
				})
				.then(function(response) {
					return response.query.categorymembers
						.map(function(category) {
							return category.title;
						})
						.map(function(categoryTitle) {
							return {
								data: categoryTitle.replace(/Category\:(.+) game covers/, '$1')
							};
						});
				})
				.then(function(platforms) {
					self.platformInput.addOptions(platforms);
				});
		}, this);
	};

	CoveryDialog.prototype.setProgressStatus = function(label, progress) {
		this.progressBar.setProgress(progress);
		this.progressField.setLabel(label);
	};

	CoveryDialog.prototype.setProgressError = function(label, progress) {
		this.getActions().forEach(null, function(actionWidget) {
			if (actionWidget.getAction() === 'upload') {
				actionWidget.setLabel('Retry');
			}
			actionWidget.setDisabled(false);
		});
		this.setProgressStatus(label, progress);
	};

	// Specify processes to handle the actions.
	CoveryDialog.prototype.getActionProcess = function(action) {
		if (action === 'upload') {
			this.getActions().forEach(null, function(actionWidget) {
				actionWidget.setDisabled(true);
			});
			this.content.toggle(false); // hide
			this.progressStatusContent.toggle(true); // show
			this.setProgressStatus('Uploading...', 1);
			var self = this;

			var fileTitle = mw.Title.newFromFileName(this.titleInput.getValue());
			return new OO.ui.Process(function() {
				var platformValues = this.platformInput.getItems().map(function(item) {
					return item.getData();
				});
				return (
					this.uploaded ||
					uploadFile(
						this.api,
						this.resizedFile,
						makeDescriptionText(
							new mw.Title(this.pageName),
							this.developerInput.getValue(),
							this.publisherInput.getValue(),
							platformValues
						),
						fileTitle
					).then(
						function() {
							return true;
						},
						function(errorCode) {
							self.setProgressError.call(self, 'Failed', 1);
							return $.Deferred().reject(
								new OO.ui.Error('Error uploading: ' + errorCode)
							);
						}
					)
				);
			}, this)
				.next(function() {
					this.uploaded = true;
					this.setProgressStatus('Uploaded file!', 25);
				}, this)
				.next(function() {
					this.setProgressStatus('Uploaded file! Creating file talk page...', 26);
					return (
						this.createdFileTalkpage ||
						createFileTalkpage(this.api, fileTitle).then(
							function() {
								return true;
							},
							function(errorCode) {
								self.setProgressError.call(
									self,
									'Uploaded file! Failed to create file talk page.',
									26
								);
								return $.Deferred().reject(
									new OO.ui.Error('Error creating file talk page: ' + errorCode)
								);
							}
						)
					);
				}, this)
				.next(function() {
					this.createdFileTalkpage = true;
					this.setProgressStatus('Uploaded file! Created file talk page!', 50);
				}, this)
				.next(function() {
					this.setProgressStatus(
						'Uploaded file! Created file talk page! Updating article...',
						51
					);
					var updatedParams = [
						{ name: 'image', value: fileTitle.getMainText() },
						{ name: 'caption', value: this.captionInput.getValue() },
						{ name: 'alt', value: this.altTextInput.getValue() },
						{ name: 'publisher', value: this.publisherInput.getValue() },
						{ name: 'developer', value: this.developerInput.getValue() }
					];
					return (
						this.updatedArticle ||
						updatePage(this.api, this.pageName, this.infobox, updatedParams).then(
							function() {
								return true;
							},
							function(errorCode) {
								self.setProgressError.call(
									self,
									'Uploaded file! Created file talk page! Failed to update article.',
									51
								);
								return $.Deferred().reject(
									new OO.ui.Error('Error editing article: ' + errorCode)
								);
							}
						)
					);
				}, this)
				.next(function() {
					this.updatedArticle = true;
					this.setProgressStatus(
						'Uploaded file! Created file talk page! Updated article!',
						75
					);
				}, this)
				.next(function() {
					this.setProgressStatus(
						'Uploaded file! Created file talk page! Updated article! Updating article talk page...',
						76
					);
					return updateTalkpage(this.api, this.pageName).then(
						function() {
							return true;
						},
						function(errorCode) {
							self.setProgressError.call(
								self,
								'Uploaded file! Created file talk page! Updated article! Failed to update article talk page.',
								76
							);
							return $.Deferred().reject(
								new OO.ui.Error('Error editing article talk page: ' + errorCode)
							);
						}
					);
				}, this)
				.next(function() {
					this.setProgressStatus('All done! Reloading article...', 100);
					return 1200;
				}, this)
				.next(function() {
					return this.close({ sucess: true });
				}, this);
		} else if (action === 'cancel') {
			return new OO.ui.Process(function() {
				return this.close();
			}, this);
		}
		// Fallback to parent handler
		return CoveryDialog.super.prototype.getActionProcess.call(this, action);
	};

	// Use the getTeardownProcess() method to perform actions whenever the dialog is closed.
	// This method provides access to data passed into the window's close() method
	// or the window manager's closeWindow() method.
	CoveryDialog.prototype.getTeardownProcess = function(data) {
		return CoveryDialog.super.prototype.getTeardownProcess.call(this, data).first(function() {
			// Perform any cleanup as needed
		}, this);
	};

	var showDialog = function showDialog(data) {
		var coveryWindowFactory = new OO.Factory();
		coveryWindowFactory.register(CoveryDialog);
		var mainWindowManager = new OO.ui.WindowManager({
			factory: coveryWindowFactory
		});
		$('body').append(mainWindowManager.$element);
		var instance = mainWindowManager.openWindow('coveryDialog', data);
		return instance.closed;
	};

	var startCovery = function startCovery(api, pageName) {
		return getLeadWikitext(api, pageName)
			.then(getInfoboxTemplate)
			.then(checkInfobox)
			.then(function(infobox) {
				return showDialog({
					api: api,
					pageName: pageName,
					infobox: infobox
				});
			})
			.then(
				function(data) {
					if (data && data.sucess) {
						window.location.reload();
					}
				},
				function(error) {
					var errorIsString = error === error.toString();
					var errorMessage = errorIsString ? 'Error: ' + error : error.toString();
					OO.ui.alert(errorMessage);
				}
			);
	};

	$.when(isSuitable(), $.ready()).then(function(config) {
		var portletLink = mw.util.addPortletLink('p-tb', '#', 'Upload cover', 'tb-covery');
		$(portletLink).click(function(e) {
			e.preventDefault();
			var api = new mw.Api({
				ajax: {
					headers: {
						'Api-User-Agent':
							SCRIPT.name +
							'/' +
							SCRIPT.version +
							' ( https://en.wikipedia.org/wiki/User:Evad37/Covery )'
					}
				}
			});
			startCovery(api, config.wgPageName);
		});
	});

	/* ========== SANDBOX USAGE ONLY (remove when updating main script) ========== */
	SCRIPT.version += '/sandbox';
	SCRIPT.ad = ' (using [[User:Evad37/Covery|Covery/sandbox]])';
	function setupForTesting() {
		/*
        window.QUnit = {
            config: {
                autoStart: false
            }
        };
        */
		$('<div>')
			.attr('id', 'qunit')
			.insertBefore('#firstHeading');
		return true; // mw.loader.using('jquery.qunit');
	}
	if (mw.config.get('wgNamespaceNumber') === 0) {
		$.when($.ready()).then(function() {
			var FakeApi = function() {
				this.realApi = new mw.Api({
					ajax: {
						headers: {
							'Api-User-Agent':
								SCRIPT.name +
								'/' +
								SCRIPT.version +
								' ( https://en.wikipedia.org/wiki/User:Evad37/Covery )'
						}
					}
				});
			};
			FakeApi.prototype.get = function(query) {
				return this.realApi.get(query);
			};
			FakeApi.prototype.postWithToken = function(token, params) {
				console.log(params);
				if (Math.random() < 0.5) {
					return $.Deferred().reject('Random error');
				}
				var response = {};
				response[params.action] = { result: 'Success' };
				return $.Deferred().resolve(response);
			};

			var portletLink = mw.util.addPortletLink('p-tb', '#', 'Test covery', 'tb-testcovery');
			$(portletLink).click(function(e) {
				e.preventDefault();
				startCovery(new FakeApi(), mw.config.get('wgPageName'));
			});
		});
	} else if (
		mw.config.get('wgPageName').includes('User:Evad37/Covery/') &&
		mw.config.get('wgAction') === 'view' &&
		mw.config.get('wgDiffOldId') === null
	) {
		$.when(setupForTesting(), $.ready()).then(function() {
			mw.loader.load('https://en.wikipedia.org/w/index.php?title=User:Evad37/qunit-2.8.0.css&action=raw&ctype=text/css', 'text/css');
			$.getScript('https://en.wikipedia.org/w/index.php?title=User:Evad37/qunit-2.8.0.js&action=raw&ctype=text/javascript', function() {
				/* globals QUnit */
				QUnit.module('Get parameters from a template');
				QUnit.test('Single unnamed param', function(assert) {
					var params1 = getTemplateParameters('|one');
					var expected1 = [
						{
							name: '1',
							value: 'one',
							wikitext: {
								name: '|',
								value: 'one'
							}
						}
					];
					var params2 = getTemplateParameters('| one ');
					var expected2 = [
						{
							name: '1',
							value: 'one',
							wikitext: {
								name: '|',
								value: ' one '
							}
						}
					];
					assert.deepEqual(params1, expected1, 'unspaced');
					assert.deepEqual(params2, expected2, 'spaced');
				});
				QUnit.test('Two unnamed params', function(assert) {
					var params1 = getTemplateParameters('|one|two');
					var expected1 = [
						{
							name: '1',
							value: 'one',
							wikitext: {
								name: '|',
								value: 'one'
							}
						},
						{
							name: '2',
							value: 'two',
							wikitext: {
								name: '|',
								value: 'two'
							}
						}
					];
					var params2 = getTemplateParameters('| one | two ');
					var expected2 = [
						{
							name: '1',
							value: 'one',
							wikitext: {
								name: '|',
								value: ' one '
							}
						},
						{
							name: '2',
							value: 'two',
							wikitext: {
								name: '|',
								value: ' two '
							}
						}
					];
					assert.deepEqual(params1, expected1, 'unspaced');
					assert.deepEqual(params2, expected2, 'spaced');
				});
				QUnit.test('Single named param', function(assert) {
					var params1 = getTemplateParameters('|a=apple');
					var expected1 = [
						{
							name: 'a',
							value: 'apple',
							wikitext: {
								name: '|a=',
								value: 'apple'
							}
						}
					];
					var params2 = getTemplateParameters('| a = apple ');
					var expected2 = [
						{
							name: 'a',
							value: 'apple',
							wikitext: {
								name: '| a =',
								value: ' apple '
							}
						}
					];
					assert.deepEqual(params1, expected1, 'unspaced');
					assert.deepEqual(params2, expected2, 'spaced');
				});
				QUnit.test('Two named params', function(assert) {
					var params1 = getTemplateParameters('|a=apple|b=bannana');
					var expected1 = [
						{
							name: 'a',
							value: 'apple',
							wikitext: {
								name: '|a=',
								value: 'apple'
							}
						},
						{
							name: 'b',
							value: 'bannana',
							wikitext: {
								name: '|b=',
								value: 'bannana'
							}
						}
					];
					var params2 = getTemplateParameters('| a = apple | b = bannana ');
					var expected2 = [
						{
							name: 'a',
							value: 'apple',
							wikitext: {
								name: '| a =',
								value: ' apple '
							}
						},
						{
							name: 'b',
							value: 'bannana',
							wikitext: {
								name: '| b =',
								value: ' bannana '
							}
						}
					];
					assert.deepEqual(params1, expected1, 'unspaced');
					assert.deepEqual(params2, expected2, 'spaced');
				});
				QUnit.test('Named and unnamed params', function(assert) {
					var params1 = getTemplateParameters('|one|a=apple|two|b=bannana');
					var expected1 = [
						{
							name: '1',
							value: 'one',
							wikitext: {
								name: '|',
								value: 'one'
							}
						},
						{
							name: 'a',
							value: 'apple',
							wikitext: {
								name: '|a=',
								value: 'apple'
							}
						},
						{
							name: '2',
							value: 'two',
							wikitext: {
								name: '|',
								value: 'two'
							}
						},
						{
							name: 'b',
							value: 'bannana',
							wikitext: {
								name: '|b=',
								value: 'bannana'
							}
						}
					];
					var params2 = getTemplateParameters('| one | a = apple | two | b = bannana ');
					var expected2 = [
						{
							name: '1',
							value: 'one',
							wikitext: {
								name: '|',
								value: ' one '
							}
						},
						{
							name: 'a',
							value: 'apple',
							wikitext: {
								name: '| a =',
								value: ' apple '
							}
						},
						{
							name: '2',
							value: 'two',
							wikitext: {
								name: '|',
								value: ' two '
							}
						},
						{
							name: 'b',
							value: 'bannana',
							wikitext: {
								name: '| b =',
								value: ' bannana '
							}
						}
					];
					assert.deepEqual(params1, expected1, 'unspaced');
					assert.deepEqual(params2, expected2, 'spaced');
				});
				QUnit.test('Empty unnamed param in middle', function(assert) {
					var params1 = getTemplateParameters('|one||three');
					var expected1 = [
						{
							name: '1',
							value: 'one',
							wikitext: {
								name: '|',
								value: 'one'
							}
						},
						{
							name: '2',
							value: '',
							wikitext: {
								name: '|',
								value: ''
							}
						},
						{
							name: '3',
							value: 'three',
							wikitext: {
								name: '|',
								value: 'three'
							}
						}
					];
					var params2 = getTemplateParameters('||two|three');
					var expected2 = [
						{
							name: '1',
							value: '',
							wikitext: {
								name: '|',
								value: ''
							}
						},
						{
							name: '2',
							value: 'two',
							wikitext: {
								name: '|',
								value: 'two'
							}
						},
						{
							name: '3',
							value: 'three',
							wikitext: {
								name: '|',
								value: 'three'
							}
						}
					];
					var params3 = getTemplateParameters('|one|two|');
					var expected3 = [
						{
							name: '1',
							value: 'one',
							wikitext: {
								name: '|',
								value: 'one'
							}
						},
						{
							name: '2',
							value: 'two',
							wikitext: {
								name: '|',
								value: 'two'
							}
						},
						{
							name: '3',
							value: '',
							wikitext: {
								name: '|',
								value: ''
							}
						}
					];
					assert.deepEqual(params1, expected1, 'middle');
					assert.deepEqual(params2, expected2, 'start');
					assert.deepEqual(params3, expected3, 'end');
				});

				QUnit.module('Get infobox from wikitext');
				QUnit.test('Simple infobox', function(assert) {
					var wikitext1 = '{{Infobox Video Game\n|publisher=PUB\n|developer=DEV\n}}';
					var infobox1 = getInfoboxTemplate(wikitext1);
					var expected = {
						name: 'Infobox Video Game',
						params: [
							{
								name: 'publisher',
								value: 'PUB',
								wikitext: {
									name: '|publisher=',
									value: 'PUB\n'
								}
							},
							{
								name: 'developer',
								value: 'DEV',
								wikitext: {
									name: '|developer=',
									value: 'DEV\n'
								}
							}
						],
						wikitext: '{{Infobox Video Game\n|publisher=PUB\n|developer=DEV\n}}'
					};
					var wikitext2 =
						'Lorem Ipsum {{Infobox Video Game\n|publisher=PUB\n|developer=DEV\n}} Dorem Sum';
					var infobox2 = getInfoboxTemplate(wikitext2);
					// expect same output

					assert.deepEqual(infobox1, expected, 'Just the infobox');
					assert.deepEqual(infobox2, expected, 'Inside other text');
				});
				QUnit.test('Empty infobox', function(assert) {
					var wikitext1 = '{{Infobox Video Game}}';
					var infobox1 = getInfoboxTemplate(wikitext1);
					var expected = {
						name: 'Infobox Video Game',
						params: [],
						wikitext: '{{Infobox Video Game}}'
					};
					var wikitext2 = 'Lorem Ipsum {{Infobox Video Game}} Dorem Sum';
					var infobox2 = getInfoboxTemplate(wikitext2);
					// expect same output

					assert.deepEqual(infobox1, expected, 'Just the infobox');
					assert.deepEqual(infobox2, expected, 'Inside other text');
				});
				QUnit.test('Real infobox', function(assert) {
					var wikitext =
						'{{notability|date=February 2018}}\n' +
						'{{Infobox video game\n' +
						'| collapsible = \n' +
						'| state = \n' +
						'| italic title = \n' +
						'| title = Attack Force\n' +
						'| image = \n' +
						'| alt = \n' +
						'| caption = \n' +
						'| developer = [[Big Five Software]]\n' +
						'| publisher = [[Big Five Software]]\n' +
						'| designer = Bill Hogue<ref name="giantlist">{{cite web|last1=Hague|first1=James|title=The Giant List of Classic Game Programmers|url=http://dadgum.com/giantlist/}}</ref><br>Jeff Konyu<ref name="giantlist"/>\n' +
						'| series = \n' +
						'| platforms = [[TRS-80]]\n' +
						'| released = {{Video game release|WW|1980}}\n' +
						'| genre = \n' +
						'| modes = \n' +
						'}}\n' +
						"'''''Attack Force''''' is a 1980 video game developed by [[Big Five Software]] for the [[TRS-80]] 16K.";
					var infobox = getInfoboxTemplate(wikitext);
					var expected = {
						name: 'Infobox video game',
						params: [
							{
								name: 'collapsible',
								value: '',
								wikitext: { name: '| collapsible =', value: ' \n' }
							},
							{
								name: 'state',
								value: '',
								wikitext: { name: '| state =', value: ' \n' }
							},
							{
								name: 'italic title',
								value: '',
								wikitext: { name: '| italic title =', value: ' \n' }
							},
							{
								name: 'title',
								value: 'Attack Force',
								wikitext: { name: '| title =', value: ' Attack Force\n' }
							},
							{
								name: 'image',
								value: '',
								wikitext: { name: '| image =', value: ' \n' }
							},
							{
								name: 'alt',
								value: '',
								wikitext: { name: '| alt =', value: ' \n' }
							},
							{
								name: 'caption',
								value: '',
								wikitext: { name: '| caption =', value: ' \n' }
							},
							{
								name: 'developer',
								value: '[[Big Five Software]]',
								wikitext: {
									name: '| developer =',
									value: ' [[Big Five Software]]\n'
								}
							},
							{
								name: 'publisher',
								value: '[[Big Five Software]]',
								wikitext: {
									name: '| publisher =',
									value: ' [[Big Five Software]]\n'
								}
							},
							{
								name: 'designer',
								value:
									'Bill Hogue<ref name="giantlist">{{cite web|last1=Hague|first1=James|title=The Giant List of Classic Game Programmers|url=http://dadgum.com/giantlist/}}</ref><br>Jeff Konyu<ref name="giantlist"/>',
								wikitext: {
									name: '| designer =',
									value:
										' Bill Hogue<ref name="giantlist">{{cite web|last1=Hague|first1=James|title=The Giant List of Classic Game Programmers|url=http://dadgum.com/giantlist/}}</ref><br>Jeff Konyu<ref name="giantlist"/>\n'
								}
							},
							{
								name: 'series',
								value: '',
								wikitext: { name: '| series =', value: ' \n' }
							},
							{
								name: 'platforms',
								value: '[[TRS-80]]',
								wikitext: { name: '| platforms =', value: ' [[TRS-80]]\n' }
							},
							{
								name: 'released',
								value: '{{Video game release|WW|1980}}',
								wikitext: {
									name: '| released =',
									value: ' {{Video game release|WW|1980}}\n'
								}
							},
							{
								name: 'genre',
								value: '',
								wikitext: { name: '| genre =', value: ' \n' }
							},
							{
								name: 'modes',
								value: '',
								wikitext: { name: '| modes =', value: ' \n' }
							}
						],
						wikitext:
							'{{Infobox video game\n' +
							'| collapsible = \n' +
							'| state = \n' +
							'| italic title = \n' +
							'| title = Attack Force\n' +
							'| image = \n' +
							'| alt = \n' +
							'| caption = \n' +
							'| developer = [[Big Five Software]]\n' +
							'| publisher = [[Big Five Software]]\n' +
							'| designer = Bill Hogue<ref name="giantlist">{{cite web|last1=Hague|first1=James|title=The Giant List of Classic Game Programmers|url=http://dadgum.com/giantlist/}}</ref><br>Jeff Konyu<ref name="giantlist"/>\n' +
							'| series = \n' +
							'| platforms = [[TRS-80]]\n' +
							'| released = {{Video game release|WW|1980}}\n' +
							'| genre = \n' +
							'| modes = \n' +
							'}}\n'
					};
					assert.deepEqual(infobox, expected, 'Attack Force (video game)');
				});

				QUnit.module('toSentanceCase');
				QUnit.test('Starting with letters', function(assert) {
					var sentanceCased1 = toSentanceCase('one');
					var expected1 = 'One';
					var sentanceCased2 = toSentanceCase('Two');
					var expected2 = 'Two';
					var sentanceCased3 = toSentanceCase('thRee');
					var expected3 = 'ThRee';
					var sentanceCased4 = toSentanceCase('FOUR');
					var expected4 = 'FOUR';
					assert.equal(sentanceCased1, expected1, 'From lowercase');
					assert.equal(sentanceCased2, expected2, 'From sentance case');
					assert.equal(sentanceCased3, expected3, 'From capital in middle');
					assert.equal(sentanceCased4, expected4, 'From upper case');
				});
				QUnit.test('Not starting with letters', function(assert) {
					var sentanceCased1 = toSentanceCase('1 is one');
					var expected1 = '1 is one';
					var sentanceCased2 = toSentanceCase('{{Infobox foo}}');
					var expected2 = '{{Infobox foo}}';
					var sentanceCased3 = toSentanceCase('_e');
					var expected3 = '_e';
					var sentanceCased4 = toSentanceCase('§₳♥');
					var expected4 = '§₳♥';
					assert.equal(sentanceCased1, expected1, 'Starting with number');
					assert.equal(sentanceCased2, expected2, 'Starting with brace');
					assert.equal(sentanceCased3, expected3, 'Starting with underscore');
					assert.equal(sentanceCased4, expected4, 'All symbols');
				});
				//checkInfobox
				QUnit.module('Infobox check');
				var infoboxesExpectedToPass = [
					'{{Infobox video game}}',
					'{{Infobox Arcade Game}}',
					'{{Infobox Videogame}}',
					'{{Infobox cvg}}',
					'{{Infobox Arcade game}}',
					'{{Infobox arcade}}',
					'{{GameInfobox}}',
					'{{Infobox CVG}}',
					'{{Infobox vg}}',
					'{{Infobox Video Game}}',
					'{{Infobox VG}}',
					'{{IOS App}}',
					'{{Infobox video games}}',
					'{{Vg infobox}}',
					'{{Infobox videogame}}',
					'{{Infobox console game}}',
					'{{Infobox computer game}}',
					'{{Videogame infobox}}',
					'{{Video game infobox}}',
					'{{Infobox video game series}}',
					'{{Infobox VG series}}',
					'{{Infobox video game franchise}}',
					'{{infobox video game}}',
					'{{infobox Arcade Game}}',
					'{{infobox Videogame}}',
					'{{infobox cvg}}',
					'{{infobox Arcade game}}',
					'{{infobox arcade}}',
					'{{gameInfobox}}',
					'{{infobox CVG}}',
					'{{infobox vg}}',
					'{{infobox Video Game}}',
					'{{infobox VG}}',
					'{{iOS App}}',
					'{{infobox video games}}',
					'{{vg infobox}}',
					'{{infobox videogame}}',
					'{{infobox console game}}',
					'{{infobox computer game}}',
					'{{videogame infobox}}',
					'{{video game infobox}}',
					'{{infobox video game series}}',
					'{{infobox VG series}}',
					'{{infobox video game franchise}}'
				];
				infoboxesExpectedToPass.forEach(infobox => {
					QUnit.test(infobox, function(assert) {
						let infoboxObject = getInfoboxTemplate(infobox);
						let checkedInfobox = checkInfobox(infoboxObject);
						assert.deepEqual(checkedInfobox, infoboxObject, infobox + ' passes check');
					});
				});
				QUnit.module('Infobox check (expected fail)');
				var infoboxesExpectedToFail = [
					'{{Video Infobox game}}',
					'{{Infobox book}}',
					'{{Infobox foo}}',
					'{{video Infobox game}}',
					'{{infobox book}}',
					'{{infobox foo}}'
				];
				infoboxesExpectedToFail.forEach(infobox => {
					QUnit.test(infobox, function(assert) {
						let infoboxObject = getInfoboxTemplate(infobox);
						assert.throws(function() {
							return checkInfobox(infoboxObject);
						}, infobox + ' throws an error');
					});
				});

				QUnit.module('Make infobox wikitext');
				QUnit.test('Add a param', function(assert) {
					var newParams = [{ name: 'image', value: 'Foo.png' }];
					var infobox1 = getInfoboxTemplate('{{Infobox Video Game}}');
					var wikitext1 = makeInfoboxWikitext(infobox1, newParams);
					var expected1 = '{{Infobox Video Game\n|image = Foo.png\n}}';
					var infobox2 = getInfoboxTemplate('{{Infobox Video Game\n|developer=DEV\n}}');
					var wikitext2 = makeInfoboxWikitext(infobox2, newParams);
					var expected2 = '{{Infobox Video Game\n|developer=DEV\n|image = Foo.png\n}}';
					assert.equal(wikitext1, expected1, 'Empty infobox');
					assert.equal(wikitext2, expected2, 'With one other param');
				});
				QUnit.test('Override empty param', function(assert) {
					var newParams = [{ name: 'image', value: 'Foo.png' }];
					var infobox1 = getInfoboxTemplate('{{Infobox Video Game\n| image = \n}}');
					var wikitext1 = makeInfoboxWikitext(infobox1, newParams);
					var expected1 = '{{Infobox Video Game\n| image = Foo.png\n}}';
					var infobox2 = getInfoboxTemplate(
						'{{Infobox Video Game\n| image = \n| developer = DEV\n}}'
					);
					var wikitext2 = makeInfoboxWikitext(infobox2, newParams);
					var expected2 =
						'{{Infobox Video Game\n| image = Foo.png\n| developer = DEV\n}}';
					assert.equal(wikitext1, expected1, 'Otherwise empty infobox');
					assert.equal(wikitext2, expected2, 'With one other param');
				});
				QUnit.test('Override 1 empty param, 1 non-empty param', function(assert) {
					var newParams = [
						{ name: 'image', value: 'Foo.png' },
						{ name: 'developer', value: '[[DEV]]' }
					];
					var infobox1 = getInfoboxTemplate(
						'{{Infobox Video Game\n| image = \n| developer = NotTheDev\n}}'
					);
					var wikitext1 = makeInfoboxWikitext(infobox1, newParams);
					var expected1 =
						'{{Infobox Video Game\n| image = Foo.png\n| developer = [[DEV]]\n}}';
					var infobox2 = getInfoboxTemplate(
						'{{Infobox Video Game\n|  image = \n|  publisher = PUB\n|  developer = NotTheDev\n}}'
					);
					var wikitext2 = makeInfoboxWikitext(infobox2, newParams);
					var expected2 =
						'{{Infobox Video Game\n|  image = Foo.png\n|  publisher = PUB\n|  developer = [[DEV]]\n}}';
					assert.equal(wikitext1, expected1, 'No other params');
					assert.equal(wikitext2, expected2, 'With one other param');
				});

				QUnit.module('Make description text');
				QUnit.test('No platforms', function(assert) {
					var description = makeDescriptionText(new mw.Title('title'), 'dev', 'pub', []);
					var expected =
						'==Summary==\n{{Non-free use rationale video game cover\n' +
						'| Article = Title\n' +
						'| Use = Infobox\n' +
						'| Publisher = pub\n' +
						'| Developer = dev\n' +
						'}}\n' +
						'==Licensing==\n{{Non-free video game cover}}';
					assert.equal(description, expected, 'No platforms');
				});
				QUnit.test('One platform', function(assert) {
					var description = makeDescriptionText(new mw.Title('title'), 'dev', 'pub', [
						'Platform'
					]);
					var expected =
						'==Summary==\n{{Non-free use rationale video game cover\n' +
						'| Article = Title\n' +
						'| Use = Infobox\n' +
						'| Publisher = pub\n' +
						'| Developer = dev\n' +
						'}}\n' +
						'==Licensing==\n{{Non-free video game cover|Platform}}';
					assert.equal(description, expected, 'One platform');
				});
				QUnit.test('Two platforms', function(assert) {
					var description = makeDescriptionText(new mw.Title('title'), 'dev', 'pub', [
						'Platform1',
						'Platform2'
					]);
					var expected =
						'==Summary==\n{{Non-free use rationale video game cover\n' +
						'| Article = Title\n' +
						'| Use = Infobox\n' +
						'| Publisher = pub\n' +
						'| Developer = dev\n' +
						'}}\n' +
						'==Licensing==\n{{Non-free video game cover|Platform1|Platform2}}';
					assert.equal(description, expected, 'Two platforms');
				});
			}); // end of "get script QUnit"
		}); // end of "when set up for testing" callback
	} // end of "else if a covery test page"
	/* ========== end of SANDBOX USAGE ONLY (remove when updating main script) ========== */
}; // end of main wrapper function

mw.loader
	.using([
		'mediawiki.util',
		'mediawiki.api',
		'oojs-ui-core',
		'oojs-ui-widgets',
		'oojs-ui-windows'
	])
	.then(setupCovery);

/*);
*/
// </nowiki>

/*
    QUnit.module("Name for group of tests");
    QUnit.test("Some test", function( assert ) {
        assert.ok(someCondition, "Description");
        assert.notOk(someCondition, "Description");
        assert.equal(firstVar, secondVar, "Description")
        assert.deepEqual(firstObject, secondObject, "Description")
    });
    QUnit.test("Some async test", function( assert ) {
        assert.expect();
        var done = assert.async();
        $.when(
            // Some async code...
        ).then(function() {
            // asserts go here, then...
            done();
        })
    });
*/