FEATURE: Introduce theme/component QUnit tests (#12517)

FEATURE: Introduce theme/component QUnit tests (#12517)

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.

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 internal code that’s responsible for processing themes/components in Discourse, most notably:

  • <script type="text/discourse-plugin"> tags are automatically converted to modules.

  • The theme-settings service is removed in favor of a simple lib file responsible for managing theme settings. This was done to allow us to register/lookup theme settings very early in our Ember app lifecycle and because there was no reason for it to be an Ember service.

These refactors should 100% backward compatible and invisible to theme developers.

diff --git a/app/assets/javascripts/discourse/app/helpers/theme-helpers.js b/app/assets/javascripts/discourse/app/helpers/theme-helpers.js
index 9898775..477b89b 100644
--- a/app/assets/javascripts/discourse/app/helpers/theme-helpers.js
+++ b/app/assets/javascripts/discourse/app/helpers/theme-helpers.js
@@ -1,6 +1,7 @@
-import { helperContext, registerUnbound } from "discourse-common/lib/helpers";
+import { registerUnbound } from "discourse-common/lib/helpers";
 import I18n from "I18n";
 import deprecated from "discourse-common/lib/deprecated";
+import { getSetting as getThemeSetting } from "discourse/lib/theme-settings-store";
 
 registerUnbound("theme-i18n", (themeId, key, params) => {
   return I18n.t(`theme_translations.${themeId}.${key}`, params);
@@ -18,5 +19,6 @@ registerUnbound("theme-setting", (themeId, key, hash) => {
       { since: "v2.2.0.beta8", dropFrom: "v2.3.0" }
     );
   }
-  return helperContext().themeSettings.getSetting(themeId, key);
+
+  return getThemeSetting(themeId, key);
 });
diff --git a/app/assets/javascripts/discourse/app/initializers/auto-load-modules.js b/app/assets/javascripts/discourse/app/initializers/auto-load-modules.js
index b985d85..fa2ca51 100644
--- a/app/assets/javascripts/discourse/app/initializers/auto-load-modules.js
+++ b/app/assets/javascripts/discourse/app/initializers/auto-load-modules.js
@@ -19,7 +19,6 @@ export function autoLoadModules(container, registry) {
 
   let context = {
     siteSettings: container.lookup("site-settings:main"),
-    themeSettings: container.lookup("service:theme-settings"),
     keyValueStore: container.lookup("key-value-store:main"),
     capabilities: container.lookup("capabilities:main"),
     currentUser: container.lookup("current-user:main"),
diff --git a/app/assets/javascripts/discourse/app/lib/theme-settings-store.js b/app/assets/javascripts/discourse/app/lib/theme-settings-store.js
new file mode 100644
index 0000000..6503d6b
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/lib/theme-settings-store.js
@@ -0,0 +1,47 @@
+import { get } from "@ember/object";
+
+const originalSettings = {};
+const settings = {};
+
+export function registerSettings(
+  themeId,
+  settingsObject,
+  { force = false } = {}
+) {
+  if (settings[themeId] && !force) {
+    return;
+  }
+  originalSettings[themeId] = Object.assign({}, settingsObject);
+  const s = {};
+  Object.keys(settingsObject).forEach((key) => {
+    Object.defineProperty(s, key, {
+      enumerable: true,
+      get() {
+        return settingsObject[key];
+      },
+      set(newVal) {
+        settingsObject[key] = newVal;
+      },
+    });
+  });
+  settings[themeId] = s;
+}
+
+export function getSetting(themeId, settingKey) {
+  if (settings[themeId]) {
+    return get(settings[themeId], settingKey);
+  }
+  return null;
+}
+
+export function getObjectForTheme(themeId) {
+  return settings[themeId];
+}
+
+export function resetSettings() {
+  Object.keys(originalSettings).forEach((themeId) => {
+    Object.keys(originalSettings[themeId]).forEach((key) => {
+      settings[themeId][key] = originalSettings[themeId][key];
+    });
+  });
+}
diff --git a/app/assets/javascripts/discourse/app/services/theme-settings.js b/app/assets/javascripts/discourse/app/services/theme-settings.js
deleted file mode 100644
index 3a1afbd..0000000
--- a/app/assets/javascripts/discourse/app/services/theme-settings.js
+++ /dev/null
@@ -1,26 +0,0 @@
-import Service from "@ember/service";
-import { get } from "@ember/object";
-
-export default Service.extend({
-  settings: null,
-
-  init() {
-    this._super(...arguments);
-    this._settings = {};
-  },
-
-  registerSettings(themeId, settingsObject) {
-    this._settings[themeId] = settingsObject;
-  },
-
-  getSetting(themeId, settingsKey) {
-    if (this._settings[themeId]) {
-      return get(this._settings[themeId], settingsKey);
-    }
-    return null;
-  },
-
-  getObjectForTheme(themeId) {
-    return this._settings[themeId];
-  },
-});
diff --git a/app/assets/javascripts/discourse/tests/setup-tests.js b/app/assets/javascripts/discourse/tests/setup-tests.js
index 066d4ff..df1c06c 100644
--- a/app/assets/javascripts/discourse/tests/setup-tests.js
+++ b/app/assets/javascripts/discourse/tests/setup-tests.js
@@ -17,6 +17,7 @@ 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";
@@ -154,6 +155,7 @@ function setupTestsCommon(application, container, config) {
   QUnit.testStart(function (ctx) {
     bootbox.$body = $("#ember-testing");
     let settings = resetSettings();
+    resetThemeSettings();
 
     if (config) {
       // Ember CLI testing environment
@@ -251,6 +253,8 @@ function setupTestsCommon(application, container, config) {
   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;
   }
@@ -259,8 +263,20 @@ function setupTestsCommon(application, container, config) {
     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 (isTest && (!skipCore || isPlugin)) {
+    if (!skipCore || isPlugin) {
       require(entry, null, null, true);
     }
   });
diff --git a/app/assets/javascripts/discourse/tests/test_helper.js b/app/assets/javascripts/discourse/tests/test_helper.js
index 595e95e..affd1b3 100644
--- a/app/assets/javascripts/discourse/tests/test_helper.js
+++ b/app/assets/javascripts/discourse/tests/test_helper.js
@@ -41,13 +41,3 @@
 //= require setup-tests
 //= require test-shims
 //= require jquery.magnific-popup.min.js
-
-document.write(
-  '<div id="ember-testing-container"><div id="ember-testing"></div></div>'
-);
-document.write(
-  "<style>#ember-testing-container { position: fixed; background: white; bottom: 0; right: 0; width: 640px; height: 384px; overflow: auto; z-index: 9999; border: 1px solid #ccc; transform: translateZ(0)} #ember-testing { zoom: 50%; }</style>"
-);
-
-let setupTestsLegacy = require("discourse/tests/setup-tests").setupTestsLegacy;
-setupTestsLegacy(window.Discourse);
diff --git a/app/assets/javascripts/discourse/tests/test_starter.js b/app/assets/javascripts/discourse/tests/test_starter.js
new file mode 100644
index 0000000..77b69af
--- /dev/null
+++ b/app/assets/javascripts/discourse/tests/test_starter.js
@@ -0,0 +1,11 @@
+// discourse-skip-module
+
+document.write(
+  '<div id="ember-testing-container"><div id="ember-testing"></div></div>'
+);
+document.write(
+  "<style>#ember-testing-container { position: fixed; background: white; bottom: 0; right: 0; width: 640px; height: 384px; overflow: auto; z-index: 9999; border: 1px solid #ccc; transform: translateZ(0)} #ember-testing { zoom: 50%; }</style>"
+);
+
+let setupTestsLegacy = require("discourse/tests/setup-tests").setupTestsLegacy;
+setupTestsLegacy(window.Discourse);
diff --git a/app/controllers/qunit_controller.rb b/app/controllers/qunit_controller.rb
index f042d0a..5e71847 100644
--- a/app/controllers/qunit_controller.rb
+++ b/app/controllers/qunit_controller.rb
@@ -1,11 +1,26 @@
 # frozen_string_literal: true
 
 class QunitController < ApplicationController
-  skip_before_action :check_xhr, :preload_json, :redirect_to_login_if_required

[... diff too long, it was truncated ...]

GitHub sha: a53d8d3e

This commit appears in #12517 which was approved by eviltrout. It was merged by OsamaSayegh.