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
David Taylor 3513835722
DEV: Improve and document __container__ workaround in tests (#15498)
Modern Ember only sets up a container when the ApplicationInstance is booted. We have legacy code which relies on having access to a container before boot (e.g. during pre-initializers).

In production we run with the default `autoboot` flag, which triggers Ember's internal `_globalsMode` flag, which sets up an ApplicationInstance immediately when an Application is initialized (via the `_buildDeprecatedInstance` method).

In tests, we worked around the problem by creating a fresh container, and placing a reference to it under `Discourse.__container__`.

HOWEVER, Ember was still creating a Container instance for each ApplicationInstance to use internally, and make available to EmberObjects via injection. The `Discourse.__container__` instance we created was barely used at all.

Having two different Container instances in play could cause some weird issues. For example, I noticed the problem because the `appEvents` instance held by DiscourseURL was different to the `appEvents` instance held by all the Ember components in our app. This meant that events triggered by DiscourseURL were not picked up by components in test mode.

This commit makes the hack more robust by ensuring that Ember re-uses the Container instance which we created pre-boot. This means we only have one Container instance in play, and makes `appEvents` work reliably across all parts of the app. It also adds detailed comments describing the hack, to help future travelers.

Hopefully in future we can remove this hack entirely, but it will require significant refactoring to our initialization process in Core and Plugins.

The mapping-router and map-routes initializer are updated to avoid the need for `container.lookup` during teardown. This isn't allowed under modern Ember, but was previously working for us because the pre-initializer was using the 'fake' container which was not ember-managed.
2022-01-10 10:34:08 +00:00

447 lines
12 KiB
JavaScript

import {
applyPretender,
exists,
resetSite,
} from "discourse/tests/helpers/qunit-helpers";
import pretender, {
applyDefaultHandlers,
pretenderHelpers,
resetPretender,
} from "discourse/tests/helpers/create-pretender";
import {
currentSettings,
resetSettings,
} from "discourse/tests/helpers/site-settings";
import { 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 { createHelperContext } from "discourse-common/lib/helpers";
import deprecated from "discourse-common/lib/deprecated";
import { flushMap } from "discourse/services/store";
import { registerObjects } from "discourse/pre-initializers/inject-discourse-objects";
import sinon from "sinon";
import { run } from "@ember/runloop";
import { isLegacyEmber } from "discourse-common/config/environment";
import { clearState as clearPresenceState } from "discourse/tests/helpers/presence-pretender";
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 = Object.assign(
{},
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) {
if (app) {
run(app, "destroy");
}
app = Application.create(config);
setApplication(app);
setResolver(buildResolver("discourse").create({ namespace: app }));
// Modern Ember only sets up a container when the ApplicationInstance
// is booted. We have legacy code which relies on having access to a container
// before boot (e.g. during pre-initializers)
//
// This hack sets up a container early, then stubs the container setup method
// so that Ember will use the same container instance when it boots the ApplicationInstance
//
// Note that this hack is not required in production because we use the default `autoboot` flag,
// which triggers the internal `_globalsMode` flag, which sets up an ApplicationInstance immediately when
// an Application is initialized (via the `_buildDeprecatedInstance` method).
//
// In the future, we should move away from relying on the `container` before the ApplicationInstance
// is booted, and then remove this hack.
let container = app.__registry__.container();
app.__container__ = container;
setDefaultOwner(container);
sinon
.stub(Object.getPrototypeOf(app.__registry__), "container")
.callsFake((opts) => {
container.owner = opts.owner;
container.registry = opts.owner.__registry__;
return container;
});
if (!started) {
app.start();
started = true;
}
app.SiteSettings = settings;
registerObjects(app);
return app;
}
function setupToolbar() {
// Most default toolbar items aren't useful for Discourse
QUnit.config.urlConfig = QUnit.config.urlConfig.reject((c) =>
[
"noglobals",
"notrycatch",
"nolint",
"devmode",
"dockcontainer",
"nocontainer",
].includes(c.id)
);
QUnit.config.urlConfig.push({
id: "qunit_skip_core",
label: "Skip Core",
value: "1",
});
QUnit.config.urlConfig.push({
id: "qunit_skip_plugins",
label: "Skip Plugins",
value: "1",
});
const pluginNames = new Set();
Object.keys(requirejs.entries).forEach((moduleName) => {
const found = moduleName.match(/\/plugins\/([\w-]+)\//);
if (found && moduleName.match(/\-test/)) {
pluginNames.add(found[1]);
}
});
QUnit.config.urlConfig.push({
id: "qunit_single_plugin",
label: "Plugin",
value: Array.from(pluginNames),
});
}
function reportMemoryUsageAfterTests() {
QUnit.done(() => {
const usageBytes = performance.memory?.usedJSHeapSize;
let result;
if (usageBytes) {
result = `${(usageBytes / Math.pow(2, 30)).toFixed(3)}GB`;
} else {
result = "(performance.memory api unavailable)";
}
writeSummaryLine(`Used JS Heap Size: ${result}`);
});
}
function writeSummaryLine(message) {
// eslint-disable-next-line no-console
console.log(`\n${message}\n`);
if (window.Testem) {
window.Testem.useCustomAdapter(function (socket) {
socket.emit("test-metadata", "summary-line", {
message,
});
});
}
}
function setupTestsCommon(application, container, config) {
QUnit.config.hidepassed = true;
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;
},
});
let setupData;
const setupDataElement = document.getElementById("data-discourse-setup");
if (setupDataElement) {
setupData = setupDataElement.dataset;
setupDataElement.remove();
}
QUnit.testStart(function (ctx) {
bootbox.$body = $("#ember-testing");
let settings = resetSettings();
resetThemeSettings();
if (config) {
// Ember CLI testing environment
app = createApplication(config, settings);
}
const cdn = setupData ? setupData.cdn : null;
const baseUri = setupData ? setupData.baseUri : "";
setupURL(cdn, "http://localhost:3000", baseUri);
if (setupData && setupData.s3BaseUrl) {
setupS3CDN(setupData.s3BaseUrl, setupData.s3Cdn);
} else {
setupS3CDN(null, null);
}
server = pretender;
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());
Session.resetCurrent();
if (setupData) {
const session = Session.current();
session.markdownItURL = setupData.markdownItUrl;
session.highlightJsPath = setupData.highlightJsPath;
}
User.resetCurrent();
let site = resetSite(settings);
createHelperContext({
siteSettings: settings,
capabilities: {},
site,
registry: app.__registry__,
});
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();
resetPretender();
clearPresenceState();
// Destroy any modals
$(".modal-backdrop").remove();
flushMap();
MessageBus.unsubscribe("*");
server = null;
});
if (getUrlParameter("qunit_disable_auto_start") === "1") {
QUnit.config.autostart = false;
}
let skipCore =
getUrlParameter("qunit_single_plugin") ||
getUrlParameter("qunit_skip_core") === "1";
let singlePlugin = getUrlParameter("qunit_single_plugin");
let skipPlugins = !singlePlugin && getUrlParameter("qunit_skip_plugins");
if (skipCore && !getUrlParameter("qunit_skip_core")) {
replaceUrlParameter("qunit_skip_core", "1");
}
if (!skipPlugins && getUrlParameter("qunit_skip_plugins")) {
replaceUrlParameter("qunit_skip_plugins", null);
}
const shouldLoadModule = (name) => {
if (!/\-test/.test(name)) {
return false;
}
const isPlugin = name.match(/\/plugins\//);
const isCore = !isPlugin;
const pluginName = name.match(/\/plugins\/([\w-]+)\//)?.[1];
if (skipCore && isCore) {
return false;
} else if (skipPlugins && isPlugin) {
return false;
} else if (singlePlugin && singlePlugin !== pluginName) {
return false;
}
return true;
};
if (isLegacyEmber()) {
Object.keys(requirejs.entries).forEach(function (entry) {
if (shouldLoadModule(entry)) {
require(entry, null, null, true);
}
});
} else {
// Ember CLI
const emberCliTestLoader = require("ember-cli-test-loader/test-support/index");
emberCliTestLoader.addModuleExcludeMatcher(
(name) => !shouldLoadModule(name)
);
}
// forces 0 as duration for all jquery animations
// eslint-disable-next-line no-undef
jQuery.fx.off = true;
setupToolbar();
reportMemoryUsageAfterTests();
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);
sinon.restore();
}
function getUrlParameter(name) {
const queryParams = new URLSearchParams(window.location.search);
return queryParams.get(name);
}
function replaceUrlParameter(name, value) {
const queryParams = new URLSearchParams(window.location.search);
if (value === null) {
queryParams.delete(name);
} else {
queryParams.set(name, value);
}
history.replaceState(null, null, "?" + queryParams.toString());
QUnit.begin(() => {
QUnit.config[name] = value;
const formElement = document.querySelector(
`#qunit-testrunner-toolbar [name=${name}]`
);
if (formElement?.type === "checkbox") {
formElement.checked = !!value;
} else if (formElement) {
formElement.value = value;
}
});
}