User:Evad37/Covery.js

This is an old revision of this page, as edited by Evad37 (talk | contribs) at 08:33, 7 October 2018 (.). The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.
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.
// <nowiki>
function loadDepenedencies() {
    return mw.loader.using(['mediawiki.util', 'mediawiki.api', 'mediawiki.RegExp', 'oojs-ui-core', 'oojs-ui-widgets', 'oojs-ui-windows']);
}

function setupForTesting() {
	/*
    $('<div>').attr('id', 'qunit').insertBefore('#firstHeading');
    // mw.loader.load( 'https://code.jquery.com/qunit/qunit-2.4.1.css', 'text/css' );
    return mw.loader.using('jquery.qunit');
    */
    return true;
}

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['*'];
    });
};

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: part
            };
        }
        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
                }
            };
        } 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.+?)\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');
    }
    var name = infoboxParts[1];
    var params = (infoboxParts[2]) ? getTemplateParameters(infoboxParts[2]) : null;
    return {
        name: name,
        params: params,
        wikitext: infoboxParts[0]
    };
};

/**
 * @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;
            var 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();
};

var makeDescriptionText = function makeDescriptionText(article, developer, publisher) {
    return '==Summary==\n{{Non-free use rationale video game cover\n' +
        '| Article = ' + article + '\n' +
        '| Use = Infobox\n' +
        '| Publisher = ' + publisher + '\n' +
        '| Developer = ' + developer + '\n}}\n' +
        '==Licensing==\n{{Non-free video game cover}}';
};

/**
 * @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 (using [[User:Evad37/Covery|Covery]])",
            "text": text,
            "file": file
        },
        { contentType: 'multipart/form-data' }
        /* 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"
        }
        */
    );
};

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, filename, caption, alt) {
    if (revisionWikitext.indexOf(infobox.wikitext) === -1) {
        return $.Deferred().reject('Edit conflict');
    }
    var newInfobox = makeInfoboxWikitext(infobox, [
        { name: 'image', value: filename },
        { name: 'caption', value: caption },
        { name: 'alt', value: alt }
    ]);
    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, fileTitle, caption, alt) {
    var filename = fileTitle.getMainText();
    return getRevisionWikitext(api, page)
        .then(function (revision) {
            return $.when(
                updateWikitext(revision.wikitext, infobox, filename, caption, alt),
                revision.timestamp
            );
        })
        .then(function (updatedWikitext, timestamp) {
            return editPage(api, page, updatedWikitext, timestamp);
        });
};
var updateTalkpageWikitext = function updateTalkpageWikitext(revisionWikitext) {
    /* Redirects to {{WikiProject Video games}} :
    //    Template:Cvgproj (redirect page) ‎ (links | edit)
    //    Template:WikiProject Video Games (redirect page) ‎ (links | edit)
    //    Template:WPVG (redirect page) ‎ (links | edit)
    //    Template:Vgproj (redirect page) ‎ (links | edit)
    //    Template:Wpvg (redirect page) ‎ (links | edit)
    //    Template:WP video games (redirect page) ‎ (links | edit)
    //    Template:WP cvg (redirect page) ‎ (links | edit)
    //    Template:WikiProject Rockstar Games (redirect page) ‎ (links | edit)
    //    Template:WGVG (redirect page) ‎ (links | edit)
    //    Template:WP Video games (redirect page) ‎ (links | edit)
    //    Template:WikiProject VG (redirect page) ‎ (links | edit)
    //    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', 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();
    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.fileSelectField = new OO.ui.FieldLayout(this.fileSelect, { label: 'Upload a file...', align: 'left' });
    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: 'left' });
    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.content.addItems([
        this.fileSelectField,
        this.urlInputField,
        this.titleInputField,
        this.imagePreviewField,
        this.captionInputField,
        this.altTextInputField,
        this.developerInputField,
        this.publisherInputField
    ]);

    /* 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' });

};

CoveryDialog.prototype.onFileChosen = function (filePromise, 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)
                );
                widgetUsed.popPending();
                widgetUsed.setIndicator('required');
                otherWidget.setDisabled(false);
                otherWidget.setIndicator(null);
                if (resizedFile.name) { self.titleInput.setValue(resizedFile.name); }
                self.onRequiredInputChange();
            },
            function (code) {
                var errorMessage = (code) ? 'An error occured: ' + code : 'An unexpected error occured';
                self.resizedFile = null;
                widgetUsed.popPending();
                widgetUsed.setIndicator('clear');
                widgetUsed.field.setErrors([errorMessage]);
                otherWidget.setDisabled(false);
                otherWidget.setIndicator(null);
                self.onRequiredInputChange();
            }
        );
};

CoveryDialog.prototype.onFileSelectChange = function (file) {
    this.onFileChosen(file, 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).then(function (result) { return result.blob(); });
    this.onFileChosen(filePromise, this.urlInput, this.fileSelect);
};

CoveryDialog.prototype.onTitleInputFlag = function (flag) {
    if (flag.invalid === true) {
        this.actions.setAbilities({
            upload: false
        });
    } else {
        this.onRequiredInputChange();
    }
}

// Only allow uploading if requirements are met
CoveryDialog.prototype.onRequiredInputChange = function (change) {
    var self = this;
    $.when(change && change.titleIsValid || this.titleInput.getValidity())
        .then(
            function () {
                var requirementsMet = (
                    !self.fileSelect.isPending() &&
                    !self.urlInput.isPending() &&
                    !!self.resizedFile &&
                    !!self.titleInput.getValue().length &&
                    !!self.developerInput.getValue().length &&
                    !!self.publisherInput.getValue().length
                );
                self.actions.setAbilities({
                    upload: requirementsMet
                });
            },
            function () {
                self.actions.setAbilities({
                    upload: false
                });
            }
        );
};

// 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.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];
                });
            });
        }, this);
};

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

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

        var fileTitle = mw.Title.newFromFileName(this.titleInput.getValue());
        return new OO.ui.Process(function () {
            return this.uploaded || uploadFile(
                this.api,
                this.resizedFile,
                makeDescriptionText(
                    this.pageName,
                    this.developerInput.getValue(),
                    this.publisherInput.getValue()
                ),
                fileTitle
            ).then(
                function () { return true; },
                function (errorCode) { 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) { 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);
                return this.updatedArticle || updatePage(
                    this.api,
                    this.pageName,
                    this.infobox,
                    fileTitle,
                    this.captionInput.getValue(),
                    this.altTextInput.getValue()
                ).then(
                    function () { return true; },
                    function (errorCode) { 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) { 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(function (infobox) {
            return showDialog({
                api: api,
                pageName: pageName,
                infobox: infobox
            });
        })
        .then(function (data) {
            if (data && data.sucess) {
                ___location.reload();
            }
        });
};

$.when(
    isSuitable(),
    loadDepenedencies(),
    $.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': 'Covery/1.0.0 ( https://en.wikipedia.org/wiki/User:Evad37/Covery )'
                }
            }
        });
        startCovery(api, config.wgPageName);
    });
});

if (mw.config.get('wgUserName') === 'Evad37') {
    $.when(
        loadDepenedencies(),
        setupForTesting(),
        $.ready()
    ).then(function () {
        var FakeApi = function () {
            this.realApi = new mw.Api({
                ajax: {
                    headers: {
                        'Api-User-Agent': 'Covery/1.0.0 ( 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);
            return $.Deferred().resolve({ result: true });
        };

        var portletLink = mw.util.addPortletLink('p-tb', '#', 'Test covery', 'tb-testcovery');
        $(portletLink).click(function (e) {
            e.preventDefault();
            startCovery(new FakeApi, mw.config.get('wgPageName'));
        });
        /*
        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();
            })
        });
        */
    })
}
// </nowiki>