This repository has been archived on 2023-03-18. You can view files and clone it, but cannot push or open issues or pull requests.
osr-discourse-src/app/assets/javascripts/discourse/tests/setup-tests.js
Osama Sayegh cd24eff5d9
FEATURE: Introduce theme/component QUnit tests (take 2) (#12661)
This commit allows themes and theme components to have QUnit tests. To add tests to your theme/component, create a top-level directory in your theme and name it `test`, and Discourse will save all the files in that directory (and its sub-directories) as "tests files" in the database. While tests files/directories are not required to be organized in a specific way, we recommend that you follow Discourse core's tests [structure](https://github.com/discourse/discourse/tree/master/app/assets/javascripts/discourse/tests).

Writing theme tests should be identical to writing plugins or core tests; all the `import` statements and APIs that you see in core (or plugins) to define/setup tests should just work in themes.

You do need a working Discourse install to run theme tests, and you have 2 ways to run theme tests:

* In the browser at the `/qunit` route. `/qunit` will run tests of all active themes/components as well as core and plugins. The `/qunit` now accepts a `theme_name` or `theme_url` params that you can use to run tests of a specific theme/component like so: `/qunit?theme_name=<your_theme_name>`.

* In the command line using the `themes:qunit` rake task. This take is meant to run tests of a single theme/component so you need to provide it with a theme name or URL like so: `bundle exec rake themes:qunit[name=<theme_name>]` or `bundle exec rake themes:qunit[url=<theme_url>]`.

There are some refactors to how Discourse processes JavaScript that comes with themes/components, and these refactors may break your JS customizations; see https://meta.discourse.org/t/upcoming-core-changes-that-may-break-some-themes-components-april-12/186252?u=osama for details on how you can check if your themes/components are affected and what you need to do to fix them.

This commit also improves theme error handling in Discourse. We will now be able to catch errors that occur when theme initializers are run and prevent them from breaking the site and other themes/components.
2021-04-12 15:02:58 +03:00

306 lines
8.1 KiB
JavaScript

import {
applyPretender,
exists,
resetSite,
} from "discourse/tests/helpers/qunit-helpers";
import createPretender, {
applyDefaultHandlers,
pretenderHelpers,
} from "discourse/tests/helpers/create-pretender";
import {
currentSettings,
resetSettings,
} from "discourse/tests/helpers/site-settings";
import { getOwner, setDefaultOwner } from "discourse-common/lib/get-owner";
import { setApplication, setResolver } from "@ember/test-helpers";
import { setupS3CDN, setupURL } from "discourse-common/lib/get-url";
import Application from "../app";
import MessageBus from "message-bus-client";
import PreloadStore from "discourse/lib/preload-store";
import { resetSettings as resetThemeSettings } from "discourse/lib/theme-settings-store";
import QUnit from "qunit";
import { ScrollingDOMMethods } from "discourse/mixins/scrolling";
import Session from "discourse/models/session";
import User from "discourse/models/user";
import bootbox from "bootbox";
import { buildResolver } from "discourse-common/resolver";
import { clearAppEventsCache } from "discourse/services/app-events";
import { createHelperContext } from "discourse-common/lib/helpers";
import deprecated from "discourse-common/lib/deprecated";
import { flushMap } from "discourse/models/store";
import { registerObjects } from "discourse/pre-initializers/inject-discourse-objects";
import { setupApplicationTest } from "ember-qunit";
import sinon from "sinon";
const Plugin = $.fn.modal;
const Modal = Plugin.Constructor;
function AcceptanceModal(option, _relatedTarget) {
return this.each(function () {
let $this = $(this);
let data = $this.data("bs.modal");
let options = $.extend(
{},
Modal.DEFAULTS,
$this.data(),
typeof option === "object" && option
);
if (!data) {
$this.data("bs.modal", (data = new Modal(this, options)));
}
data.$body = $("#ember-testing");
if (typeof option === "string") {
data[option](_relatedTarget);
} else if (options.show) {
data.show(_relatedTarget);
}
});
}
let app;
let started = false;
function createApplication(config, settings) {
app = Application.create(config);
setApplication(app);
setResolver(buildResolver("discourse").create({ namespace: app }));
let container = app.__registry__.container();
app.__container__ = container;
setDefaultOwner(container);
if (!started) {
app.start();
started = true;
}
app.SiteSettings = settings;
registerObjects(container, app);
return app;
}
function setupTestsCommon(application, container, config) {
QUnit.config.hidepassed = true;
// Let's customize QUnit options a bit
QUnit.config.urlConfig = QUnit.config.urlConfig.filter(
(c) => ["dockcontainer", "nocontainer"].indexOf(c.id) === -1
);
application.rootElement = "#ember-testing";
application.setupForTesting();
application.injectTestHelpers();
sinon.config = {
injectIntoThis: false,
injectInto: null,
properties: ["spy", "stub", "mock", "clock", "sandbox"],
useFakeTimers: true,
useFakeServer: false,
};
// Stop the message bus so we don't get ajax calls
MessageBus.stop();
// disable logster error reporting
if (window.Logster) {
window.Logster.enabled = false;
} else {
window.Logster = { enabled: false };
}
$.fn.modal = AcceptanceModal;
let server;
Object.defineProperty(window, "server", {
get() {
deprecated(
"Accessing the global variable `server` is deprecated. Use a `pretend()` method instead.",
{
since: "2.6.0.beta.3",
dropFrom: "2.6.0",
}
);
return server;
},
});
Object.defineProperty(window, "sandbox", {
get() {
deprecated(
"Accessing the global variable `sandbox` is deprecated. Import `sinon` instead",
{
since: "2.6.0.beta.4",
dropFrom: "2.6.0",
}
);
return sinon;
},
});
Object.defineProperty(window, "exists", {
get() {
deprecated(
"Accessing the global function `exists` is deprecated. Import it instead.",
{
since: "2.6.0.beta.4",
dropFrom: "2.6.0",
}
);
return exists;
},
});
QUnit.testStart(function (ctx) {
bootbox.$body = $("#ember-testing");
let settings = resetSettings();
resetThemeSettings();
if (config) {
// Ember CLI testing environment
app = createApplication(config, settings);
}
server = createPretender;
server.handlers = [];
applyDefaultHandlers(server);
server.prepareBody = function (body) {
if (body && typeof body === "object") {
return JSON.stringify(body);
}
return body;
};
if (QUnit.config.logAllRequests) {
server.handledRequest = function (verb, path) {
// eslint-disable-next-line no-console
console.log("REQ: " + verb + " " + path);
};
}
server.unhandledRequest = function (verb, path) {
if (QUnit.config.logAllRequests) {
// eslint-disable-next-line no-console
console.log("REQ: " + verb + " " + path + " missing");
}
const error =
"Unhandled request in test environment: " + path + " (" + verb + ")";
// eslint-disable-next-line no-console
console.error(error);
throw new Error(error);
};
server.checkPassthrough = (request) =>
request.requestHeaders["Discourse-Script"];
applyPretender(ctx.module, server, pretenderHelpers());
setupURL(null, "http://localhost:3000", "");
setupS3CDN(null, null);
Session.resetCurrent();
User.resetCurrent();
let site = resetSite(settings);
createHelperContext({
siteSettings: settings,
capabilities: {},
site,
});
PreloadStore.reset();
sinon.stub(ScrollingDOMMethods, "screenNotFull");
sinon.stub(ScrollingDOMMethods, "bindOnScroll");
sinon.stub(ScrollingDOMMethods, "unbindOnScroll");
// Unless we ever need to test this, let's leave it off.
$.fn.autocomplete = function () {};
});
QUnit.testDone(function () {
sinon.restore();
// Destroy any modals
$(".modal-backdrop").remove();
flushMap();
if (!setupApplicationTest) {
// ensures any event not removed is not leaking between tests
// most likely in intialisers, other places (controller, component...)
// should be fixed in code
clearAppEventsCache(getOwner(this));
}
MessageBus.unsubscribe("*");
server = null;
});
// Load ES6 tests
function getUrlParameter(name) {
name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
let regex = new RegExp("[\\?&]" + name + "=([^&#]*)");
let results = regex.exec(location.search);
return results === null
? ""
: decodeURIComponent(results[1].replace(/\+/g, " "));
}
let skipCore = getUrlParameter("qunit_skip_core") === "1";
let pluginPath = getUrlParameter("qunit_single_plugin")
? "/" + getUrlParameter("qunit_single_plugin") + "/"
: "/plugins/";
let themeOnly = getUrlParameter("theme_name") || getUrlParameter("theme_url");
if (getUrlParameter("qunit_disable_auto_start") === "1") {
QUnit.config.autostart = false;
}
Object.keys(requirejs.entries).forEach(function (entry) {
let isTest = /\-test/.test(entry);
let regex = new RegExp(pluginPath);
let isPlugin = regex.test(entry);
let isTheme = /^discourse\/theme\-\d+\/.+/.test(entry);
if (!isTest) {
return;
}
if (themeOnly) {
if (isTheme) {
require(entry, null, null, true);
}
return;
}
if (!skipCore || isPlugin) {
require(entry, null, null, true);
}
});
// forces 0 as duration for all jquery animations
jQuery.fx.off = true;
setApplication(application);
setDefaultOwner(application.__container__);
resetSite();
}
export function setupTestsLegacy(application) {
app = application;
setResolver(buildResolver("discourse").create({ namespace: app }));
setupTestsCommon(application, app.__container__);
app.SiteSettings = currentSettings();
app.start();
}
export default function setupTests(config) {
let settings = resetSettings();
app = createApplication(config, settings);
setupTestsCommon(app, app.__container__, config);
}