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.
This commit is contained in:
parent
18777d9108
commit
cd24eff5d9
|
@ -1,8 +1,10 @@
|
|||
import Application from "@ember/application";
|
||||
import Mousetrap from "mousetrap";
|
||||
import { buildResolver } from "discourse-common/resolver";
|
||||
import { isTesting } from "discourse-common/config/environment";
|
||||
|
||||
const _pluginCallbacks = [];
|
||||
let _themeErrors = [];
|
||||
|
||||
const Discourse = Application.extend({
|
||||
rootElement: "#main",
|
||||
|
@ -19,14 +21,37 @@ const Discourse = Application.extend({
|
|||
Resolver: buildResolver("discourse"),
|
||||
|
||||
_prepareInitializer(moduleName) {
|
||||
const module = requirejs(moduleName, null, null, true);
|
||||
if (!module) {
|
||||
throw new Error(moduleName + " must export an initializer.");
|
||||
const themeId = moduleThemeId(moduleName);
|
||||
let module = null;
|
||||
|
||||
try {
|
||||
module = requirejs(moduleName, null, null, true);
|
||||
|
||||
if (!module) {
|
||||
throw new Error(moduleName + " must export an initializer.");
|
||||
}
|
||||
} catch (err) {
|
||||
if (!themeId || isTesting()) {
|
||||
throw err;
|
||||
}
|
||||
_themeErrors.push([themeId, err]);
|
||||
fireThemeErrorEvent();
|
||||
return;
|
||||
}
|
||||
|
||||
const init = module.default;
|
||||
const oldInitialize = init.initialize;
|
||||
init.initialize = (app) => oldInitialize.call(init, app.__container__, app);
|
||||
init.initialize = (app) => {
|
||||
try {
|
||||
return oldInitialize.call(init, app.__container__, app);
|
||||
} catch (err) {
|
||||
if (!themeId || isTesting()) {
|
||||
throw err;
|
||||
}
|
||||
_themeErrors.push([themeId, err]);
|
||||
fireThemeErrorEvent();
|
||||
}
|
||||
};
|
||||
|
||||
return init;
|
||||
},
|
||||
|
@ -37,9 +62,15 @@ const Discourse = Application.extend({
|
|||
|
||||
Object.keys(requirejs._eak_seen).forEach((key) => {
|
||||
if (/\/pre\-initializers\//.test(key)) {
|
||||
this.initializer(this._prepareInitializer(key));
|
||||
const initializer = this._prepareInitializer(key);
|
||||
if (initializer) {
|
||||
this.initializer(initializer);
|
||||
}
|
||||
} else if (/\/(api\-)?initializers\//.test(key)) {
|
||||
this.instanceInitializer(this._prepareInitializer(key));
|
||||
const initializer = this._prepareInitializer(key);
|
||||
if (initializer) {
|
||||
this.instanceInitializer(initializer);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -60,4 +91,22 @@ const Discourse = Application.extend({
|
|||
},
|
||||
});
|
||||
|
||||
function moduleThemeId(moduleName) {
|
||||
const match = moduleName.match(/^discourse\/theme\-(\d+)\//);
|
||||
if (match) {
|
||||
return parseInt(match[1], 10);
|
||||
}
|
||||
}
|
||||
|
||||
function fireThemeErrorEvent() {
|
||||
const event = new CustomEvent("discourse-theme-error");
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
|
||||
export function getAndClearThemeErrors() {
|
||||
const copy = _themeErrors;
|
||||
_themeErrors = [];
|
||||
return copy;
|
||||
}
|
||||
|
||||
export default Discourse;
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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];
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
import getURL, { getURLWithCDN } from "discourse-common/lib/get-url";
|
||||
import Handlebars from "handlebars";
|
||||
import I18n from "I18n";
|
||||
import { deepMerge } from "discourse-common/lib/object";
|
||||
import { escape } from "pretty-text/sanitizer";
|
||||
import { helperContext } from "discourse-common/lib/helpers";
|
||||
|
@ -444,40 +443,6 @@ export function postRNWebviewMessage(prop, value) {
|
|||
}
|
||||
}
|
||||
|
||||
function reportToLogster(name, error) {
|
||||
const data = {
|
||||
message: `${name} theme/component is throwing errors`,
|
||||
stacktrace: error.stack,
|
||||
};
|
||||
|
||||
Ember.$.ajax(getURL("/logs/report_js_error"), {
|
||||
data,
|
||||
type: "POST",
|
||||
cache: false,
|
||||
});
|
||||
}
|
||||
// this function is used in lib/theme_javascript_compiler.rb
|
||||
export function rescueThemeError(name, error, api) {
|
||||
/* eslint-disable-next-line no-console */
|
||||
console.error(`"${name}" error:`, error);
|
||||
reportToLogster(name, error);
|
||||
|
||||
const currentUser = api.getCurrentUser();
|
||||
if (!currentUser || !currentUser.admin) {
|
||||
return;
|
||||
}
|
||||
|
||||
const path = getURL(`/admin/customize/themes`);
|
||||
const message = I18n.t("themes.broken_theme_alert", {
|
||||
theme: name,
|
||||
path: `<a href="${path}">${path}</a>`,
|
||||
});
|
||||
const alertDiv = document.createElement("div");
|
||||
alertDiv.classList.add("broken-theme-alert");
|
||||
alertDiv.innerHTML = `⚠️ ${message}`;
|
||||
document.body.prepend(alertDiv);
|
||||
}
|
||||
|
||||
const CODE_BLOCKS_REGEX = /^( |\t).*|`[^`]+`|^```[^]*?^```|\[code\][^]*?\[\/code\]/gm;
|
||||
// | ^ | ^ | ^ | ^ |
|
||||
// | | | |
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
import { isTesting } from "discourse-common/config/environment";
|
||||
import { getAndClearThemeErrors } from "discourse/app";
|
||||
import PreloadStore from "discourse/lib/preload-store";
|
||||
import getURL from "discourse-common/lib/get-url";
|
||||
import I18n from "I18n";
|
||||
|
||||
export default {
|
||||
name: "theme-errors-handler",
|
||||
after: "inject-discourse-objects",
|
||||
|
||||
initialize(container) {
|
||||
const currentUser = container.lookup("current-user:main");
|
||||
if (isTesting()) {
|
||||
return;
|
||||
}
|
||||
renderErrorNotices(currentUser);
|
||||
document.addEventListener("discourse-theme-error", () =>
|
||||
renderErrorNotices(currentUser)
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
function reportToLogster(name, error) {
|
||||
const data = {
|
||||
message: `${name} theme/component is throwing errors`,
|
||||
stacktrace: error.stack,
|
||||
};
|
||||
|
||||
Ember.$.ajax(getURL("/logs/report_js_error"), {
|
||||
data,
|
||||
type: "POST",
|
||||
cache: false,
|
||||
});
|
||||
}
|
||||
|
||||
function renderErrorNotices(currentUser) {
|
||||
getAndClearThemeErrors().forEach(([themeId, error]) => {
|
||||
const name =
|
||||
PreloadStore.get("activatedThemes")[themeId] || `(theme-id: ${themeId})`;
|
||||
/* eslint-disable-next-line no-console */
|
||||
console.error(`An error occurred in the "${name}" theme/component:`, error);
|
||||
reportToLogster(name, error);
|
||||
if (!currentUser || !currentUser.admin) {
|
||||
return;
|
||||
}
|
||||
const path = getURL("/admin/customize/themes");
|
||||
const message = I18n.t("themes.broken_theme_alert", {
|
||||
theme: name,
|
||||
path: `<a href="${path}">${path}</a>`,
|
||||
});
|
||||
const alertDiv = document.createElement("div");
|
||||
alertDiv.classList.add("broken-theme-alert");
|
||||
alertDiv.innerHTML = `⚠️ ${message}`;
|
||||
document.body.prepend(alertDiv);
|
||||
});
|
||||
}
|
|
@ -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];
|
||||
},
|
||||
});
|
|
@ -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 && (!skipCore || isPlugin)) {
|
||||
if (!isTest) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (themeOnly) {
|
||||
if (isTheme) {
|
||||
require(entry, null, null, true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!skipCore || isPlugin) {
|
||||
require(entry, null, null, true);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
|
@ -570,6 +570,7 @@ class ApplicationController < ActionController::Base
|
|||
store_preloaded("banner", banner_json)
|
||||
store_preloaded("customEmoji", custom_emoji)
|
||||
store_preloaded("isReadOnly", @readonly_mode.to_s)
|
||||
store_preloaded("activatedThemes", activated_themes_json)
|
||||
end
|
||||
|
||||
def preload_current_user_data
|
||||
|
@ -890,4 +891,10 @@ class ApplicationController < ActionController::Base
|
|||
end
|
||||
end
|
||||
|
||||
def activated_themes_json
|
||||
ids = @theme_ids&.compact
|
||||
return "{}" if ids.blank?
|
||||
ids = Theme.transform_ids(ids)
|
||||
Theme.where(id: ids).pluck(:id, :name).to_h.to_json
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,11 +1,26 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class QunitController < ApplicationController
|
||||
skip_before_action :check_xhr, :preload_json, :redirect_to_login_if_required
|
||||
skip_before_action *%i{
|
||||
check_xhr
|
||||
preload_json
|
||||
redirect_to_login_if_required
|
||||
}
|
||||
layout false
|
||||
|
||||
# only used in test / dev
|
||||
def index
|
||||
raise Discourse::InvalidAccess.new if Rails.env.production?
|
||||
if (theme_name = params[:theme_name]).present?
|
||||
theme = Theme.find_by(name: theme_name)
|
||||
raise Discourse::NotFound if theme.blank?
|
||||
elsif (theme_url = params[:theme_url]).present?
|
||||
theme = RemoteTheme.find_by(remote_url: theme_url)
|
||||
raise Discourse::NotFound if theme.blank?
|
||||
end
|
||||
if theme.present?
|
||||
request.env[:resolved_theme_ids] = [theme.id]
|
||||
request.env[:skip_theme_ids_transformation] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,7 +8,7 @@ class ThemeJavascriptsController < ApplicationController
|
|||
:preload_json,
|
||||
:redirect_to_login_if_required,
|
||||
:verify_authenticity_token,
|
||||
only: [:show]
|
||||
only: [:show, :show_tests]
|
||||
)
|
||||
|
||||
before_action :is_asset_path, :no_cookies, :apply_cdn_headers, only: [:show]
|
||||
|
@ -34,6 +34,29 @@ class ThemeJavascriptsController < ApplicationController
|
|||
send_file(cache_file, disposition: :inline)
|
||||
end
|
||||
|
||||
def show_tests
|
||||
raise Discourse::NotFound if Rails.env.production?
|
||||
|
||||
theme_id = params.require(:theme_id)
|
||||
theme = Theme.find(theme_id)
|
||||
content = ThemeField
|
||||
.where(
|
||||
theme_id: theme_id,
|
||||
target_id: Theme.targets[:tests_js]
|
||||
)
|
||||
.each(&:ensure_baked!)
|
||||
.map(&:value_baked)
|
||||
.join("\n")
|
||||
|
||||
ThemeJavascriptCompiler.force_default_settings(content, theme)
|
||||
|
||||
response.headers["Content-Length"] = content.size.to_s
|
||||
response.headers["Last-Modified"] = Time.zone.now.httpdate
|
||||
immutable_for(1.second)
|
||||
|
||||
send_data content, filename: "js-tests-theme-#{theme_id}.js", disposition: :inline
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def query
|
||||
|
|
|
@ -453,15 +453,30 @@ module ApplicationHelper
|
|||
end
|
||||
|
||||
def theme_lookup(name)
|
||||
Theme.lookup_field(theme_ids, mobile_view? ? :mobile : :desktop, name)
|
||||
Theme.lookup_field(
|
||||
theme_ids,
|
||||
mobile_view? ? :mobile : :desktop,
|
||||
name,
|
||||
skip_transformation: request.env[:skip_theme_ids_transformation].present?
|
||||
)
|
||||
end
|
||||
|
||||
def theme_translations_lookup
|
||||
Theme.lookup_field(theme_ids, :translations, I18n.locale)
|
||||
Theme.lookup_field(
|
||||
theme_ids,
|
||||
:translations,
|
||||
I18n.locale,
|
||||
skip_transformation: request.env[:skip_theme_ids_transformation].present?
|
||||
)
|
||||
end
|
||||
|
||||
def theme_js_lookup
|
||||
Theme.lookup_field(theme_ids, :extra_js, nil)
|
||||
Theme.lookup_field(
|
||||
theme_ids,
|
||||
:extra_js,
|
||||
nil,
|
||||
skip_transformation: request.env[:skip_theme_ids_transformation].present?
|
||||
)
|
||||
end
|
||||
|
||||
def discourse_stylesheet_link_tag(name, opts = {})
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module QunitHelper
|
||||
def theme_tests
|
||||
theme_ids = request.env[:resolved_theme_ids]
|
||||
return "" if theme_ids.blank?
|
||||
|
||||
skip_transformation = request.env[:skip_theme_ids_transformation]
|
||||
query = ThemeField
|
||||
.joins(:theme)
|
||||
.where(
|
||||
target_id: Theme.targets[:tests_js],
|
||||
theme_id: skip_transformation ? theme_ids : Theme.transform_ids(theme_ids)
|
||||
)
|
||||
.pluck(:theme_id)
|
||||
.uniq
|
||||
.map do |theme_id|
|
||||
src = "#{GlobalSetting.cdn_url}#{Discourse.base_path}/theme-javascripts/tests/#{theme_id}.js"
|
||||
"<script src='#{src}'></script>"
|
||||
end
|
||||
.join("\n")
|
||||
.html_safe
|
||||
end
|
||||
end
|
|
@ -128,7 +128,7 @@ class Theme < ActiveRecord::Base
|
|||
SvgSprite.expire_cache
|
||||
end
|
||||
|
||||
BASE_COMPILER_VERSION = 17
|
||||
BASE_COMPILER_VERSION = 48
|
||||
def self.compiler_version
|
||||
get_set_cache "compiler_version" do
|
||||
dependencies = [
|
||||
|
@ -262,11 +262,11 @@ class Theme < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
def self.lookup_field(theme_ids, target, field)
|
||||
def self.lookup_field(theme_ids, target, field, skip_transformation: false)
|
||||
return if theme_ids.blank?
|
||||
theme_ids = [theme_ids] unless Array === theme_ids
|
||||
|
||||
theme_ids = transform_ids(theme_ids)
|
||||
theme_ids = transform_ids(theme_ids) if !skip_transformation
|
||||
cache_key = "#{theme_ids.join(",")}:#{target}:#{field}:#{Theme.compiler_version}"
|
||||
lookup = @cache[cache_key]
|
||||
return lookup.html_safe if lookup
|
||||
|
@ -297,7 +297,7 @@ class Theme < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def self.targets
|
||||
@targets ||= Enum.new(common: 0, desktop: 1, mobile: 2, settings: 3, translations: 4, extra_scss: 5, extra_js: 6)
|
||||
@targets ||= Enum.new(common: 0, desktop: 1, mobile: 2, settings: 3, translations: 4, extra_scss: 5, extra_js: 6, tests_js: 7)
|
||||
end
|
||||
|
||||
def self.lookup_target(target_id)
|
||||
|
|
|
@ -94,10 +94,32 @@ class ThemeField < ActiveRecord::Base
|
|||
node.remove
|
||||
end
|
||||
|
||||
doc.css('script[type="text/discourse-plugin"]').each do |node|
|
||||
next unless node['version'].present?
|
||||
doc.css('script[type="text/discourse-plugin"]').each_with_index do |node, index|
|
||||
version = node['version']
|
||||
next if version.blank?
|
||||
|
||||
initializer_name = "theme-field" +
|
||||
"-#{self.id}" +
|
||||
"-#{Theme.targets[self.target_id]}" +
|
||||
"-#{ThemeField.types[self.type_id]}" +
|
||||
"-script-#{index + 1}"
|
||||
begin
|
||||
js_compiler.append_plugin_script(node.inner_html, node['version'])
|
||||
js = <<~JS
|
||||
import { withPluginApi } from "discourse/lib/plugin-api";
|
||||
|
||||
export default {
|
||||
name: #{initializer_name.inspect},
|
||||
after: "inject-objects",
|
||||
|
||||
initialize() {
|
||||
withPluginApi(#{version.inspect}, (api) => {
|
||||
#{node.inner_html}
|
||||
});
|
||||
}
|
||||
};
|
||||
JS
|
||||
|
||||
js_compiler.append_module(js, "discourse/initializers/#{initializer_name}", include_variables: true)
|
||||
rescue ThemeJavascriptCompiler::CompileError => ex
|
||||
errors << ex.message
|
||||
end
|
||||
|
@ -132,7 +154,7 @@ class ThemeField < ActiveRecord::Base
|
|||
begin
|
||||
case extension
|
||||
when "js.es6", "js"
|
||||
js_compiler.append_module(content, filename)
|
||||
js_compiler.append_module(content, filename, include_variables: true)
|
||||
when "hbs"
|
||||
js_compiler.append_ember_template(filename.sub("discourse/templates/", ""), content)
|
||||
when "hbr", "raw.hbs"
|
||||
|
@ -285,6 +307,10 @@ class ThemeField < ActiveRecord::Base
|
|||
Theme.targets[self.target_id] == :extra_js
|
||||
end
|
||||
|
||||
def js_tests_field?
|
||||
Theme.targets[self.target_id] == :tests_js
|
||||
end
|
||||
|
||||
def basic_scss_field?
|
||||
ThemeField.basic_targets.include?(Theme.targets[self.target_id].to_s) &&
|
||||
ThemeField.scss_fields.include?(self.name)
|
||||
|
@ -315,7 +341,7 @@ class ThemeField < ActiveRecord::Base
|
|||
self.error = nil unless self.error.present?
|
||||
self.compiler_version = Theme.compiler_version
|
||||
DB.after_commit { CSP::Extension.clear_theme_extensions_cache! }
|
||||
elsif extra_js_field?
|
||||
elsif extra_js_field? || js_tests_field?
|
||||
self.value_baked, self.error = process_extra_js(self.value)
|
||||
self.error = nil unless self.error.present?
|
||||
self.compiler_version = Theme.compiler_version
|
||||
|
@ -422,7 +448,7 @@ class ThemeField < ActiveRecord::Base
|
|||
hash = {}
|
||||
OPTIONS.each do |option|
|
||||
plural = :"#{option}s"
|
||||
hash[option] = @allowed_values[plural][0] if @allowed_values[plural] && @allowed_values[plural].length == 1
|
||||
hash[option] = @allowed_values[plural][0] if @allowed_values[plural]&.length == 1
|
||||
hash[option] = match[option] if hash[option].nil?
|
||||
end
|
||||
hash
|
||||
|
@ -457,6 +483,9 @@ class ThemeField < ActiveRecord::Base
|
|||
ThemeFileMatcher.new(regex: /^javascripts\/(?<name>.+)$/,
|
||||
targets: :extra_js, names: nil, types: :js,
|
||||
canonical: -> (h) { "javascripts/#{h[:name]}" }),
|
||||
ThemeFileMatcher.new(regex: /^test\/(?<name>.+)$/,
|
||||
targets: :tests_js, names: nil, types: :js,
|
||||
canonical: -> (h) { "test/#{h[:name]}" }),
|
||||
ThemeFileMatcher.new(regex: /^settings\.ya?ml$/,
|
||||
names: "yaml", types: :yaml, targets: :settings,
|
||||
canonical: -> (h) { "settings.yml" }),
|
||||
|
|
|
@ -5,6 +5,11 @@
|
|||
<%= discourse_color_scheme_stylesheets %>
|
||||
<%= stylesheet_link_tag "test_helper" %>
|
||||
<%= javascript_include_tag "test_helper" %>
|
||||
<%= theme_tests %>
|
||||
<%= theme_translations_lookup %>
|
||||
<%= theme_js_lookup %>
|
||||
<%= theme_lookup("head_tag") %>
|
||||
<%= javascript_include_tag "test_starter" %>
|
||||
<%= csrf_meta_tags %>
|
||||
<script src="<%= ExtraLocalesController.url('admin') %>"></script>
|
||||
<meta property="og:title" content="">
|
||||
|
|
|
@ -527,6 +527,7 @@ Discourse::Application.routes.draw do
|
|||
get "stylesheets/:name.css" => "stylesheets#show", constraints: { name: /[-a-z0-9_]+/ }
|
||||
get "color-scheme-stylesheet/:id(/:theme_id)" => "stylesheets#color_scheme", constraints: { format: :json }
|
||||
get "theme-javascripts/:digest.js" => "theme_javascripts#show", constraints: { digest: /\h{40}/ }
|
||||
get "theme-javascripts/tests/:theme_id.js" => "theme_javascripts#show_tests"
|
||||
|
||||
post "uploads/lookup-metadata" => "uploads#metadata"
|
||||
post "uploads" => "uploads#create"
|
||||
|
|
|
@ -61,7 +61,7 @@ task "qunit:test", [:timeout, :qunit_path] do |_, args|
|
|||
cmd = "node #{test_path}/run-qunit.js http://localhost:#{port}#{qunit_path}"
|
||||
options = { seed: (ENV["QUNIT_SEED"] || Random.new.seed), hidepassed: 1 }
|
||||
|
||||
%w{module filter qunit_skip_core qunit_single_plugin}.each do |arg|
|
||||
%w{module filter qunit_skip_core qunit_single_plugin theme_name theme_url}.each do |arg|
|
||||
options[arg] = ENV[arg.upcase] if ENV[arg.upcase].present?
|
||||
end
|
||||
|
||||
|
|
|
@ -96,3 +96,24 @@ task "themes:audit" => :environment do
|
|||
puts repo
|
||||
end
|
||||
end
|
||||
|
||||
desc "Run QUnit tests of a theme/component"
|
||||
task "themes:qunit", :theme_name_or_url do |t, args|
|
||||
name_or_url = args[:theme_name_or_url]
|
||||
if name_or_url.blank?
|
||||
raise "A theme name or URL must be provided."
|
||||
end
|
||||
if name_or_url =~ /^(url|name)=(.+)/
|
||||
cmd = "THEME_#{Regexp.last_match(1).upcase}=#{Regexp.last_match(2)} "
|
||||
cmd += `which rake`.strip + " qunit:test"
|
||||
sh cmd
|
||||
else
|
||||
raise <<~MSG
|
||||
Cannot parse passed argument #{name_or_url.inspect}.
|
||||
Usage:
|
||||
`bundle exec rake themes:unit[url=<theme_url>]`
|
||||
OR
|
||||
`bundle exec rake themes:unit[name=<theme_name>]`
|
||||
MSG
|
||||
end
|
||||
end
|
||||
|
|
|
@ -151,6 +151,18 @@ class ThemeJavascriptCompiler
|
|||
class CompileError < StandardError
|
||||
end
|
||||
|
||||
def self.force_default_settings(content, theme)
|
||||
settings_hash = {}
|
||||
theme.settings.each do |setting|
|
||||
settings_hash[setting.name] = setting.default
|
||||
end
|
||||
content.prepend <<~JS
|
||||
(function() {
|
||||
require("discourse/lib/theme-settings-store").registerSettings(#{theme.id}, #{settings_hash.to_json}, { force: true });
|
||||
})();
|
||||
JS
|
||||
end
|
||||
|
||||
attr_accessor :content
|
||||
|
||||
def initialize(theme_id, theme_name)
|
||||
|
@ -162,10 +174,8 @@ class ThemeJavascriptCompiler
|
|||
def prepend_settings(settings_hash)
|
||||
@content.prepend <<~JS
|
||||
(function() {
|
||||
if ('Discourse' in window && Discourse.__container__) {
|
||||
Discourse.__container__
|
||||
.lookup("service:theme-settings")
|
||||
.registerSettings(#{@theme_id}, #{settings_hash.to_json});
|
||||
if ('require' in window) {
|
||||
require("discourse/lib/theme-settings-store").registerSettings(#{@theme_id}, #{settings_hash.to_json});
|
||||
}
|
||||
})();
|
||||
JS
|
||||
|
@ -173,8 +183,14 @@ class ThemeJavascriptCompiler
|
|||
|
||||
# TODO Error handling for handlebars templates
|
||||
def append_ember_template(name, hbs_template)
|
||||
if !name.start_with?("javascripts/")
|
||||
prefix = "javascripts"
|
||||
prefix += "/" if !name.start_with?("/")
|
||||
name = prefix + name
|
||||
end
|
||||
name = name.inspect
|
||||
compiled = EmberTemplatePrecompiler.new(@theme_id).compile(hbs_template)
|
||||
# the `'Ember' in window` check is needed for no_ember pages
|
||||
content << <<~JS
|
||||
(function() {
|
||||
if ('Ember' in window) {
|
||||
|
@ -204,18 +220,19 @@ class ThemeJavascriptCompiler
|
|||
raise CompileError.new e.instance_variable_get(:@error) # e.message contains the entire template, which could be very long
|
||||
end
|
||||
|
||||
def append_plugin_script(script, api_version)
|
||||
@content << transpile(script, api_version)
|
||||
end
|
||||
|
||||
def append_raw_script(script)
|
||||
@content << script + "\n"
|
||||
end
|
||||
|
||||
def append_module(script, name, include_variables: true)
|
||||
script = "#{theme_variables}#{script}" if include_variables
|
||||
name = "discourse/theme-#{@theme_id}/#{name.gsub(/^discourse\//, '')}"
|
||||
script = "#{theme_settings}#{script}" if include_variables
|
||||
transpiler = DiscourseJsProcessor::Transpiler.new
|
||||
@content << transpiler.perform(script, "", name)
|
||||
@content << <<~JS
|
||||
if ('define' in window) {
|
||||
#{transpiler.perform(script, "", name).strip}
|
||||
}
|
||||
JS
|
||||
rescue MiniRacer::RuntimeError => ex
|
||||
raise CompileError.new ex.message
|
||||
end
|
||||
|
@ -226,36 +243,11 @@ class ThemeJavascriptCompiler
|
|||
|
||||
private
|
||||
|
||||
def theme_variables
|
||||
def theme_settings
|
||||
<<~JS
|
||||
const __theme_name__ = "#{@theme_name.gsub('"', "\\\"")}";
|
||||
const settings = Discourse.__container__
|
||||
.lookup("service:theme-settings")
|
||||
const settings = require("discourse/lib/theme-settings-store")
|
||||
.getObjectForTheme(#{@theme_id});
|
||||
const themePrefix = (key) => `theme_translations.#{@theme_id}.${key}`;
|
||||
JS
|
||||
end
|
||||
|
||||
def transpile(es6_source, version)
|
||||
transpiler = DiscourseJsProcessor::Transpiler.new(skip_module: true)
|
||||
wrapped = <<~PLUGIN_API_JS
|
||||
(function() {
|
||||
if ('Discourse' in window && typeof Discourse._registerPluginCode === 'function') {
|
||||
#{theme_variables}
|
||||
Discourse._registerPluginCode('#{version}', api => {
|
||||
try {
|
||||
#{es6_source}
|
||||
} catch(err) {
|
||||
const rescue = require("discourse/lib/utilities").rescueThemeError;
|
||||
rescue(__theme_name__, err, api);
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
||||
PLUGIN_API_JS
|
||||
|
||||
transpiler.perform(wrapped)
|
||||
rescue MiniRacer::RuntimeError => ex
|
||||
raise CompileError.new ex.message
|
||||
end
|
||||
end
|
||||
|
|
|
@ -127,4 +127,18 @@ describe ThemeJavascriptCompiler do
|
|||
expect(compiler.content.to_s).to include("addRawTemplate(\"#{name}.hbs\"")
|
||||
end
|
||||
end
|
||||
|
||||
describe "#append_ember_template" do
|
||||
let(:compiler) { ThemeJavascriptCompiler.new(1, 'marks') }
|
||||
it 'prepends `javascripts/` to template name if it is not prepended' do
|
||||
compiler.append_ember_template("/connectors/blah-1", "{{var}}")
|
||||
expect(compiler.content.to_s).to include('Ember.TEMPLATES["javascripts/connectors/blah-1"]')
|
||||
|
||||
compiler.append_ember_template("connectors/blah-2", "{{var}}")
|
||||
expect(compiler.content.to_s).to include('Ember.TEMPLATES["javascripts/connectors/blah-2"]')
|
||||
|
||||
compiler.append_ember_template("javascripts/connectors/blah-3", "{{var}}")
|
||||
expect(compiler.content.to_s).to include('Ember.TEMPLATES["javascripts/connectors/blah-3"]')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -40,6 +40,7 @@ describe RemoteTheme do
|
|||
"stylesheets/file.scss" => ".class1{color:red}",
|
||||
"stylesheets/empty.scss" => "",
|
||||
"javascripts/discourse/controllers/test.js.es6" => "console.log('test');",
|
||||
"test/acceptance/theme-test.js" => "assert.ok(true);",
|
||||
"common/header.html" => "I AM HEADER",
|
||||
"common/random.html" => "I AM SILLY",
|
||||
"common/embedded.scss" => "EMBED",
|
||||
|
@ -74,7 +75,7 @@ describe RemoteTheme do
|
|||
|
||||
expect(@theme.theme_modifier_set.serialize_topic_excerpts).to eq(true)
|
||||
|
||||
expect(@theme.theme_fields.length).to eq(10)
|
||||
expect(@theme.theme_fields.length).to eq(11)
|
||||
|
||||
mapped = Hash[*@theme.theme_fields.map { |f| ["#{f.target_id}-#{f.name}", f.value] }.flatten]
|
||||
|
||||
|
@ -88,8 +89,9 @@ describe RemoteTheme do
|
|||
expect(mapped["3-yaml"]).to eq("boolean_setting: true")
|
||||
|
||||
expect(mapped["4-en"]).to eq("sometranslations")
|
||||
expect(mapped["7-acceptance/theme-test.js"]).to eq("assert.ok(true);")
|
||||
|
||||
expect(mapped.length).to eq(10)
|
||||
expect(mapped.length).to eq(11)
|
||||
|
||||
expect(@theme.settings.length).to eq(1)
|
||||
expect(@theme.settings.first.value).to eq(true)
|
||||
|
|
|
@ -192,34 +192,22 @@ HTML
|
|||
unknown_field = theme.set_field(target: :extra_js, name: "discourse/controllers/discovery.blah", value: "this wont work")
|
||||
theme.save!
|
||||
|
||||
expected_js = <<~JS
|
||||
define("discourse/controllers/discovery", ["discourse/lib/ajax"], function (_ajax) {
|
||||
"use strict";
|
||||
js_field.reload
|
||||
expect(js_field.value_baked).to include("if ('define' in window) {")
|
||||
expect(js_field.value_baked).to include("define(\"discourse/theme-#{theme.id}/controllers/discovery\"")
|
||||
expect(js_field.value_baked).to include("console.log('hello from .js.es6');")
|
||||
|
||||
var __theme_name__ = "#{theme.name}";
|
||||
|
||||
var settings = Discourse.__container__.lookup("service:theme-settings").getObjectForTheme(#{theme.id});
|
||||
|
||||
var themePrefix = function themePrefix(key) {
|
||||
return "theme_translations.#{theme.id}.".concat(key);
|
||||
};
|
||||
|
||||
console.log('hello from .js.es6');
|
||||
});
|
||||
JS
|
||||
expect(js_field.reload.value_baked).to eq(expected_js.strip)
|
||||
|
||||
expect(hbs_field.reload.value_baked).to include('Ember.TEMPLATES["discovery"]')
|
||||
expect(hbs_field.reload.value_baked).to include('Ember.TEMPLATES["javascripts/discovery"]')
|
||||
expect(raw_hbs_field.reload.value_baked).to include('addRawTemplate("discovery"')
|
||||
expect(hbr_field.reload.value_baked).to include('addRawTemplate("other_discovery"')
|
||||
expect(unknown_field.reload.value_baked).to eq("")
|
||||
expect(unknown_field.reload.error).to eq(I18n.t("themes.compile_error.unrecognized_extension", extension: "blah"))
|
||||
|
||||
# All together
|
||||
expect(theme.javascript_cache.content).to include('Ember.TEMPLATES["discovery"]')
|
||||
expect(theme.javascript_cache.content).to include('Ember.TEMPLATES["javascripts/discovery"]')
|
||||
expect(theme.javascript_cache.content).to include('addRawTemplate("discovery"')
|
||||
expect(theme.javascript_cache.content).to include('define("discourse/controllers/discovery"')
|
||||
expect(theme.javascript_cache.content).to include('define("discourse/controllers/discovery-2"')
|
||||
expect(theme.javascript_cache.content).to include("define(\"discourse/theme-#{theme.id}/controllers/discovery\"")
|
||||
expect(theme.javascript_cache.content).to include("define(\"discourse/theme-#{theme.id}/controllers/discovery-2\"")
|
||||
expect(theme.javascript_cache.content).to include("var settings =")
|
||||
end
|
||||
|
||||
|
|
|
@ -126,24 +126,6 @@ describe Theme do
|
|||
expect(Theme.lookup_field(theme.id, :desktop, "head_tag")).to eq("<b>I am bold</b>")
|
||||
end
|
||||
|
||||
it "changing theme name should re-transpile HTML theme fields" do
|
||||
theme.update!(name: "old_name")
|
||||
html = <<~HTML
|
||||
<script type='text/discourse-plugin' version='0.1'>
|
||||
const x = 1;
|
||||
</script>
|
||||
HTML
|
||||
theme.set_field(target: :common, name: "head_tag", value: html)
|
||||
theme.save!
|
||||
field = theme.theme_fields.where(value: html).first
|
||||
old_value = field.value_baked
|
||||
|
||||
theme.update!(name: "new_name")
|
||||
field.reload
|
||||
new_value = field.value_baked
|
||||
expect(old_value).not_to eq(new_value)
|
||||
end
|
||||
|
||||
it 'should precompile fragments in body and head tags' do
|
||||
with_template = <<HTML
|
||||
<script type='text/x-handlebars' name='template'>
|
||||
|
@ -276,7 +258,7 @@ HTML
|
|||
def transpile(html)
|
||||
f = ThemeField.create!(target_id: Theme.targets[:mobile], theme_id: 1, name: "after_header", value: html)
|
||||
f.ensure_baked!
|
||||
[f.value_baked, f.javascript_cache]
|
||||
[f.value_baked, f.javascript_cache, f]
|
||||
end
|
||||
|
||||
it "transpiles ES6 code" do
|
||||
|
@ -286,10 +268,20 @@ HTML
|
|||
</script>
|
||||
HTML
|
||||
|
||||
baked, javascript_cache = transpile(html)
|
||||
baked, javascript_cache, field = transpile(html)
|
||||
expect(baked).to include(javascript_cache.url)
|
||||
expect(javascript_cache.content).to include('var x = 1;')
|
||||
expect(javascript_cache.content).to include("_registerPluginCode('0.1'")
|
||||
|
||||
expect(javascript_cache.content).to include("if ('define' in window) {")
|
||||
expect(javascript_cache.content).to include(
|
||||
"define(\"discourse/theme-#{field.theme_id}/initializers/theme-field-#{field.id}-mobile-html-script-1\""
|
||||
)
|
||||
expect(javascript_cache.content).to include(
|
||||
"settings = require(\"discourse/lib/theme-settings-store\").getObjectForTheme(#{field.theme_id});"
|
||||
)
|
||||
expect(javascript_cache.content).to include("name: \"theme-field-#{field.id}-mobile-html-script-1\",")
|
||||
expect(javascript_cache.content).to include("after: \"inject-objects\",")
|
||||
expect(javascript_cache.content).to include("(0, _pluginApi.withPluginApi)(\"0.1\", function (api) {")
|
||||
expect(javascript_cache.content).to include("var x = 1;")
|
||||
end
|
||||
|
||||
it "wraps constants calls in a readOnlyError function" do
|
||||
|
@ -369,83 +361,31 @@ HTML
|
|||
theme_field = theme.set_field(target: :common, name: :after_header, value: '<script type="text/discourse-plugin" version="1.0">alert(settings.name); let a = ()=>{};</script>')
|
||||
theme.save!
|
||||
|
||||
transpiled = <<~HTML
|
||||
(function() {
|
||||
if ('Discourse' in window && Discourse.__container__) {
|
||||
Discourse.__container__
|
||||
.lookup("service:theme-settings")
|
||||
.registerSettings(#{theme.id}, {"name":"bob"});
|
||||
}
|
||||
})();
|
||||
(function () {
|
||||
if ('Discourse' in window && typeof Discourse._registerPluginCode === 'function') {
|
||||
var __theme_name__ = "awesome theme\\\"";
|
||||
|
||||
var settings = Discourse.__container__.lookup("service:theme-settings").getObjectForTheme(#{theme.id});
|
||||
|
||||
var themePrefix = function themePrefix(key) {
|
||||
return "theme_translations.#{theme.id}.".concat(key);
|
||||
};
|
||||
|
||||
Discourse._registerPluginCode('1.0', function (api) {
|
||||
try {
|
||||
alert(settings.name);
|
||||
|
||||
var a = function a() {};
|
||||
} catch (err) {
|
||||
var rescue = require("discourse/lib/utilities").rescueThemeError;
|
||||
|
||||
rescue(__theme_name__, err, api);
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
||||
HTML
|
||||
|
||||
theme_field.reload
|
||||
expect(Theme.lookup_field(theme.id, :desktop, :after_header)).to include(theme_field.javascript_cache.url)
|
||||
expect(theme_field.javascript_cache.content).to eq(transpiled.strip)
|
||||
expect(theme_field.javascript_cache.content).to include("if ('require' in window) {")
|
||||
expect(theme_field.javascript_cache.content).to include(
|
||||
"require(\"discourse/lib/theme-settings-store\").registerSettings(#{theme_field.theme.id}, {\"name\":\"bob\"});"
|
||||
)
|
||||
expect(theme_field.javascript_cache.content).to include("if ('define' in window) {")
|
||||
expect(theme_field.javascript_cache.content).to include(
|
||||
"define(\"discourse/theme-#{theme_field.theme.id}/initializers/theme-field-#{theme_field.id}-common-html-script-1\","
|
||||
)
|
||||
expect(theme_field.javascript_cache.content).to include("name: \"theme-field-#{theme_field.id}-common-html-script-1\",")
|
||||
expect(theme_field.javascript_cache.content).to include("after: \"inject-objects\",")
|
||||
expect(theme_field.javascript_cache.content).to include("(0, _pluginApi.withPluginApi)(\"1.0\", function (api)")
|
||||
expect(theme_field.javascript_cache.content).to include("alert(settings.name)")
|
||||
expect(theme_field.javascript_cache.content).to include("var a = function a() {}")
|
||||
|
||||
setting = theme.settings.find { |s| s.name == :name }
|
||||
setting.value = 'bill'
|
||||
theme.save!
|
||||
|
||||
transpiled = <<~HTML
|
||||
(function() {
|
||||
if ('Discourse' in window && Discourse.__container__) {
|
||||
Discourse.__container__
|
||||
.lookup("service:theme-settings")
|
||||
.registerSettings(#{theme.id}, {"name":"bill"});
|
||||
}
|
||||
})();
|
||||
(function () {
|
||||
if ('Discourse' in window && typeof Discourse._registerPluginCode === 'function') {
|
||||
var __theme_name__ = "awesome theme\\\"";
|
||||
|
||||
var settings = Discourse.__container__.lookup("service:theme-settings").getObjectForTheme(#{theme.id});
|
||||
|
||||
var themePrefix = function themePrefix(key) {
|
||||
return "theme_translations.#{theme.id}.".concat(key);
|
||||
};
|
||||
|
||||
Discourse._registerPluginCode('1.0', function (api) {
|
||||
try {
|
||||
alert(settings.name);
|
||||
|
||||
var a = function a() {};
|
||||
} catch (err) {
|
||||
var rescue = require("discourse/lib/utilities").rescueThemeError;
|
||||
|
||||
rescue(__theme_name__, err, api);
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
||||
HTML
|
||||
|
||||
theme_field.reload
|
||||
expect(theme_field.javascript_cache.content).to include(
|
||||
"require(\"discourse/lib/theme-settings-store\").registerSettings(#{theme_field.theme.id}, {\"name\":\"bill\"});"
|
||||
)
|
||||
expect(Theme.lookup_field(theme.id, :desktop, :after_header)).to include(theme_field.javascript_cache.url)
|
||||
expect(theme_field.javascript_cache.content).to eq(transpiled.strip)
|
||||
end
|
||||
|
||||
it 'is empty when the settings are invalid' do
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe QunitController do
|
||||
let(:theme) { Fabricate(:theme, name: 'main-theme') }
|
||||
let(:component) { Fabricate(:theme, component: true, name: 'enabled-component') }
|
||||
let(:disabled_component) { Fabricate(:theme, component: true, enabled: false, name: 'disabled-component') }
|
||||
|
||||
before do
|
||||
Theme.destroy_all
|
||||
theme.set_default!
|
||||
component.add_relative_theme!(:parent, theme)
|
||||
disabled_component.add_relative_theme!(:parent, theme)
|
||||
[theme, component, disabled_component].each do |t|
|
||||
t.set_field(
|
||||
target: :extra_js,
|
||||
type: :js,
|
||||
name: "discourse/initializers/my-#{t.id}-initializer.js",
|
||||
value: "console.log(#{t.id});"
|
||||
)
|
||||
t.set_field(
|
||||
target: :tests_js,
|
||||
type: :js,
|
||||
name: "acceptance/some-test-#{t.id}.js",
|
||||
value: "assert.ok(#{t.id});"
|
||||
)
|
||||
t.save!
|
||||
end
|
||||
end
|
||||
|
||||
context "when no theme is specified" do
|
||||
it "includes tests of enabled theme + components" do
|
||||
get '/qunit'
|
||||
js_urls = JavascriptCache.where(theme_id: [theme.id, component.id]).map(&:url)
|
||||
expect(js_urls.size).to eq(2)
|
||||
js_urls.each do |url|
|
||||
expect(response.body).to include(url)
|
||||
end
|
||||
[theme, component].each do |t|
|
||||
expect(response.body).to include("/theme-javascripts/tests/#{t.id}.js")
|
||||
end
|
||||
|
||||
js_urls = JavascriptCache.where(theme_id: disabled_component).map(&:url)
|
||||
expect(js_urls.size).to eq(1)
|
||||
js_urls.each do |url|
|
||||
expect(response.body).not_to include(url)
|
||||
end
|
||||
expect(response.body).not_to include("/theme-javascripts/tests/#{disabled_component.id}.js")
|
||||
end
|
||||
end
|
||||
|
||||
context "when a theme is specified" do
|
||||
it "includes tests of the specified theme only" do
|
||||
[theme, disabled_component].each do |t|
|
||||
get "/qunit?theme_name=#{t.name}"
|
||||
js_urls = JavascriptCache.where(theme_id: t.id).map(&:url)
|
||||
expect(js_urls.size).to eq(1)
|
||||
js_urls.each do |url|
|
||||
expect(response.body).to include(url)
|
||||
end
|
||||
expect(response.body).to include("/theme-javascripts/tests/#{t.id}.js")
|
||||
|
||||
excluded = Theme.pluck(:id) - [t.id]
|
||||
js_urls = JavascriptCache.where(theme_id: excluded).map(&:url)
|
||||
expect(js_urls.size).to eq(2)
|
||||
js_urls.each do |url|
|
||||
expect(response.body).not_to include(url)
|
||||
end
|
||||
excluded.each do |id|
|
||||
expect(response.body).not_to include("/theme-javascripts/tests/#{id}.js")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -54,4 +54,24 @@ describe ThemeJavascriptsController do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#show_tests" do
|
||||
context "theme settings" do
|
||||
let(:component) { Fabricate(:theme, component: true, name: 'enabled-component') }
|
||||
|
||||
it "forces default values" do
|
||||
ThemeField.create!(
|
||||
theme: component,
|
||||
target_id: Theme.targets[:settings],
|
||||
name: "yaml",
|
||||
value: "num_setting: 5"
|
||||
)
|
||||
component.reload
|
||||
component.update_setting(:num_setting, 643)
|
||||
|
||||
get "/theme-javascripts/tests/#{component.id}.js"
|
||||
expect(response.body).to include("require(\"discourse/lib/theme-settings-store\").registerSettings(#{component.id}, {\"num_setting\":5}, { force: true });")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue