mirror of
https://github.com/discourse/discourse.git
synced 2024-12-01 09:03:56 +08:00
cd24eff5d9
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.
838 lines
30 KiB
Ruby
838 lines
30 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'rails_helper'
|
|
|
|
describe Theme do
|
|
after do
|
|
Theme.clear_cache!
|
|
end
|
|
|
|
before do
|
|
I18n.locale = :en
|
|
end
|
|
|
|
fab! :user do
|
|
Fabricate(:user)
|
|
end
|
|
|
|
let(:guardian) do
|
|
Guardian.new(user)
|
|
end
|
|
|
|
let(:theme) { Fabricate(:theme, user: user) }
|
|
let(:child) { Fabricate(:theme, user: user, component: true) }
|
|
it 'can properly clean up color schemes' do
|
|
scheme = ColorScheme.create!(theme_id: theme.id, name: 'test')
|
|
scheme2 = ColorScheme.create!(theme_id: theme.id, name: 'test2')
|
|
|
|
Fabricate(:theme, color_scheme_id: scheme2.id)
|
|
|
|
theme.destroy!
|
|
scheme2.reload
|
|
|
|
expect(scheme2).not_to eq(nil)
|
|
expect(scheme2.theme_id).to eq(nil)
|
|
expect(ColorScheme.find_by(id: scheme.id)).to eq(nil)
|
|
end
|
|
|
|
it 'can support child themes' do
|
|
child.set_field(target: :common, name: "header", value: "World")
|
|
child.set_field(target: :desktop, name: "header", value: "Desktop")
|
|
child.set_field(target: :mobile, name: "header", value: "Mobile")
|
|
|
|
child.save!
|
|
|
|
expect(Theme.lookup_field(child.id, :desktop, "header")).to eq("World\nDesktop")
|
|
expect(Theme.lookup_field(child.id, "mobile", :header)).to eq("World\nMobile")
|
|
|
|
child.set_field(target: :common, name: "header", value: "Worldie")
|
|
child.save!
|
|
|
|
expect(Theme.lookup_field(child.id, :mobile, :header)).to eq("Worldie\nMobile")
|
|
|
|
parent = Fabricate(:theme, user: user)
|
|
|
|
parent.set_field(target: :common, name: "header", value: "Common Parent")
|
|
parent.set_field(target: :mobile, name: "header", value: "Mobile Parent")
|
|
|
|
parent.save!
|
|
|
|
parent.add_relative_theme!(:child, child)
|
|
|
|
expect(Theme.lookup_field(parent.id, :mobile, "header")).to eq("Common Parent\nMobile Parent\nWorldie\nMobile")
|
|
|
|
end
|
|
|
|
it 'can support parent themes' do
|
|
child.add_relative_theme!(:parent, theme)
|
|
expect(child.parent_themes).to eq([theme])
|
|
end
|
|
|
|
it "can automatically disable for mismatching version" do
|
|
expect(theme.supported?).to eq(true)
|
|
theme.create_remote_theme!(remote_url: "", minimum_discourse_version: "99.99.99")
|
|
theme.save!
|
|
expect(theme.supported?).to eq(false)
|
|
|
|
expect(Theme.transform_ids([theme.id])).to be_empty
|
|
end
|
|
|
|
xit "#transform_ids works with nil values" do
|
|
# Used in safe mode
|
|
expect(Theme.transform_ids([nil])).to eq([nil])
|
|
end
|
|
|
|
it '#transform_ids filters out disabled components' do
|
|
theme.add_relative_theme!(:child, child)
|
|
expect(Theme.transform_ids([theme.id], extend: true)).to eq([theme.id, child.id])
|
|
child.update!(enabled: false)
|
|
expect(Theme.transform_ids([theme.id], extend: true)).to eq([theme.id])
|
|
end
|
|
|
|
it "doesn't allow multi-level theme components" do
|
|
grandchild = Fabricate(:theme, user: user)
|
|
grandparent = Fabricate(:theme, user: user)
|
|
|
|
expect do
|
|
child.add_relative_theme!(:child, grandchild)
|
|
end.to raise_error(Discourse::InvalidParameters, I18n.t("themes.errors.no_multilevels_components"))
|
|
|
|
expect do
|
|
grandparent.add_relative_theme!(:child, theme)
|
|
end.to raise_error(Discourse::InvalidParameters, I18n.t("themes.errors.no_multilevels_components"))
|
|
end
|
|
|
|
it "doesn't allow a child to be user selectable" do
|
|
child.update(user_selectable: true)
|
|
expect(child.errors.full_messages).to contain_exactly(I18n.t("themes.errors.component_no_user_selectable"))
|
|
end
|
|
|
|
it "doesn't allow a child to be set as the default theme" do
|
|
expect do
|
|
child.set_default!
|
|
end.to raise_error(Discourse::InvalidParameters, I18n.t("themes.errors.component_no_default"))
|
|
end
|
|
|
|
it "doesn't allow a component to have color scheme" do
|
|
scheme = ColorScheme.create!(name: "test")
|
|
child.update(color_scheme: scheme)
|
|
expect(child.errors.full_messages).to contain_exactly(I18n.t("themes.errors.component_no_color_scheme"))
|
|
end
|
|
|
|
it 'should correct bad html in body_tag_baked and head_tag_baked' do
|
|
theme.set_field(target: :common, name: "head_tag", value: "<b>I am bold")
|
|
theme.save!
|
|
|
|
expect(Theme.lookup_field(theme.id, :desktop, "head_tag")).to eq("<b>I am bold</b>")
|
|
end
|
|
|
|
it 'should precompile fragments in body and head tags' do
|
|
with_template = <<HTML
|
|
<script type='text/x-handlebars' name='template'>
|
|
{{hello}}
|
|
</script>
|
|
<script type='text/x-handlebars' data-template-name='raw_template.raw'>
|
|
{{hello}}
|
|
</script>
|
|
HTML
|
|
theme.set_field(target: :common, name: "header", value: with_template)
|
|
theme.save!
|
|
|
|
field = theme.theme_fields.find_by(target_id: Theme.targets[:common], name: 'header')
|
|
baked = Theme.lookup_field(theme.id, :mobile, "header")
|
|
|
|
expect(baked).to include(field.javascript_cache.url)
|
|
expect(field.javascript_cache.content).to include('HTMLBars')
|
|
expect(field.javascript_cache.content).to include('raw-handlebars')
|
|
end
|
|
|
|
it 'can destroy unbaked theme without errors' do
|
|
with_template = <<HTML
|
|
<script type='text/x-handlebars' name='template'>
|
|
{{hello}}
|
|
</script>
|
|
<script type='text/x-handlebars' data-template-name='raw_template.raw'>
|
|
{{hello}}
|
|
</script>
|
|
HTML
|
|
theme.set_field(target: :common, name: "header", value: with_template)
|
|
theme.save!
|
|
|
|
field = theme.theme_fields.find_by(target_id: Theme.targets[:common], name: 'header')
|
|
baked = Theme.lookup_field(theme.id, :mobile, "header")
|
|
ThemeField.where(id: field.id).update_all(compiler_version: 0) # update_all to avoid callbacks
|
|
|
|
field.reload.destroy!
|
|
end
|
|
|
|
it 'should create body_tag_baked on demand if needed' do
|
|
theme.set_field(target: :common, name: :body_tag, value: "<b>test")
|
|
theme.save
|
|
|
|
ThemeField.update_all(value_baked: nil)
|
|
|
|
expect(Theme.lookup_field(theme.id, :desktop, :body_tag)).to match(/<b>test<\/b>/)
|
|
end
|
|
|
|
it 'can find fields for multiple themes' do
|
|
theme2 = Fabricate(:theme)
|
|
|
|
theme.set_field(target: :common, name: :body_tag, value: "<b>testtheme1</b>")
|
|
theme2.set_field(target: :common, name: :body_tag, value: "<b>theme2test</b>")
|
|
theme.save!
|
|
theme2.save!
|
|
|
|
field = Theme.lookup_field([theme.id, theme2.id], :desktop, :body_tag)
|
|
expect(field).to match(/<b>testtheme1<\/b>/)
|
|
expect(field).to match(/<b>theme2test<\/b>/)
|
|
end
|
|
|
|
describe "#switch_to_component!" do
|
|
it "correctly converts a theme to component" do
|
|
theme.add_relative_theme!(:child, child)
|
|
scheme = ColorScheme.create!(name: 'test')
|
|
theme.update!(color_scheme_id: scheme.id, user_selectable: true)
|
|
theme.set_default!
|
|
|
|
theme.switch_to_component!
|
|
theme.reload
|
|
|
|
expect(theme.component).to eq(true)
|
|
expect(theme.user_selectable).to eq(false)
|
|
expect(theme.default?).to eq(false)
|
|
expect(theme.color_scheme_id).to eq(nil)
|
|
expect(ChildTheme.where(parent_theme: theme).exists?).to eq(false)
|
|
end
|
|
end
|
|
|
|
describe "#switch_to_theme!" do
|
|
it "correctly converts a component to theme" do
|
|
theme.add_relative_theme!(:child, child)
|
|
|
|
child.switch_to_theme!
|
|
theme.reload
|
|
child.reload
|
|
|
|
expect(child.component).to eq(false)
|
|
expect(ChildTheme.where(child_theme: child).exists?).to eq(false)
|
|
end
|
|
end
|
|
|
|
describe ".transform_ids" do
|
|
let!(:orphan1) { Fabricate(:theme, component: true) }
|
|
let!(:child) { Fabricate(:theme, component: true) }
|
|
let!(:child2) { Fabricate(:theme, component: true) }
|
|
let!(:orphan2) { Fabricate(:theme, component: true) }
|
|
let!(:orphan3) { Fabricate(:theme, component: true) }
|
|
let!(:orphan4) { Fabricate(:theme, component: true) }
|
|
|
|
before do
|
|
theme.add_relative_theme!(:child, child)
|
|
theme.add_relative_theme!(:child, child2)
|
|
end
|
|
|
|
it "returns an empty array if no ids are passed" do
|
|
expect(Theme.transform_ids([])).to eq([])
|
|
end
|
|
|
|
it "adds the child themes of the parent" do
|
|
sorted = [child.id, child2.id].sort
|
|
|
|
expect(Theme.transform_ids([theme.id])).to eq([theme.id, *sorted])
|
|
|
|
expect(Theme.transform_ids([theme.id, orphan1.id, orphan2.id])).to eq([theme.id, orphan1.id, *sorted, orphan2.id])
|
|
end
|
|
|
|
it "doesn't insert children when extend is false" do
|
|
fake_id = orphan2.id
|
|
fake_id2 = orphan3.id
|
|
fake_id3 = orphan4.id
|
|
|
|
expect(Theme.transform_ids([theme.id], extend: false)).to eq([theme.id])
|
|
expect(Theme.transform_ids([theme.id, fake_id3, fake_id, fake_id2, fake_id2], extend: false))
|
|
.to eq([theme.id, fake_id, fake_id2, fake_id3])
|
|
end
|
|
end
|
|
|
|
context "plugin api" do
|
|
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]
|
|
end
|
|
|
|
it "transpiles ES6 code" do
|
|
html = <<HTML
|
|
<script type='text/discourse-plugin' version='0.1'>
|
|
const x = 1;
|
|
</script>
|
|
HTML
|
|
|
|
baked, javascript_cache, field = transpile(html)
|
|
expect(baked).to include(javascript_cache.url)
|
|
|
|
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
|
|
html = <<HTML
|
|
<script type='text/discourse-plugin' version='0.1'>
|
|
const x = 1;
|
|
x = 2;
|
|
</script>
|
|
HTML
|
|
|
|
baked, javascript_cache = transpile(html)
|
|
expect(baked).to include(javascript_cache.url)
|
|
expect(javascript_cache.content).to include('var x = 1;')
|
|
expect(javascript_cache.content).to include('x = (_readOnlyError("x"), 2);')
|
|
end
|
|
end
|
|
|
|
context 'theme upload vars' do
|
|
let :image do
|
|
file_from_fixtures("logo.png")
|
|
end
|
|
|
|
it 'can handle uploads based of ThemeField' do
|
|
upload = UploadCreator.new(image, "logo.png").create_for(-1)
|
|
theme.set_field(target: :common, name: :logo, upload_id: upload.id, type: :theme_upload_var)
|
|
theme.set_field(target: :common, name: :scss, value: 'body {background-image: url($logo)}')
|
|
theme.save!
|
|
|
|
# make sure we do not nuke it
|
|
freeze_time (SiteSetting.clean_orphan_uploads_grace_period_hours + 1).hours.from_now
|
|
Jobs::CleanUpUploads.new.execute(nil)
|
|
|
|
expect(Upload.where(id: upload.id)).to be_exist
|
|
|
|
# no error for theme field
|
|
theme.reload
|
|
expect(theme.theme_fields.find_by(name: :scss).error).to eq(nil)
|
|
|
|
scss, _map = Stylesheet::Manager.new(:desktop_theme, theme.id).compile(force: true)
|
|
expect(scss).to include(upload.url)
|
|
end
|
|
end
|
|
|
|
context "theme settings" do
|
|
it "allows values to be used in scss" do
|
|
theme.set_field(target: :settings, name: :yaml, value: "background_color: red\nfont_size: 25px")
|
|
theme.set_field(target: :common, name: :scss, value: 'body {background-color: $background_color; font-size: $font-size}')
|
|
theme.save!
|
|
|
|
scss, _map = Stylesheet::Manager.new(:desktop_theme, theme.id).compile(force: true)
|
|
expect(scss).to include("background-color:red")
|
|
expect(scss).to include("font-size:25px")
|
|
|
|
setting = theme.settings.find { |s| s.name == :font_size }
|
|
setting.value = '30px'
|
|
theme.save!
|
|
|
|
scss, _map = Stylesheet::Manager.new(:desktop_theme, theme.id).compile(force: true)
|
|
expect(scss).to include("font-size:30px")
|
|
|
|
# Escapes correctly. If not, compiling this would throw an exception
|
|
setting.value = <<~MULTILINE
|
|
\#{$fakeinterpolatedvariable}
|
|
andanothervalue 'withquotes'; margin: 0;
|
|
MULTILINE
|
|
|
|
theme.set_field(target: :common, name: :scss, value: 'body {font-size: quote($font-size)}')
|
|
theme.save!
|
|
|
|
scss, _map = Stylesheet::Manager.new(:desktop_theme, theme.id).compile(force: true)
|
|
expect(scss).to include('font-size:"#{$fakeinterpolatedvariable}\a andanothervalue \'withquotes\'; margin: 0;\a"')
|
|
end
|
|
|
|
it "allows values to be used in JS" do
|
|
theme.name = 'awesome theme"'
|
|
theme.set_field(target: :settings, name: :yaml, value: "name: bob")
|
|
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!
|
|
|
|
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 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!
|
|
|
|
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)
|
|
end
|
|
|
|
it 'is empty when the settings are invalid' do
|
|
theme.set_field(target: :settings, name: :yaml, value: 'nil_setting: ')
|
|
theme.save!
|
|
|
|
expect(theme.settings).to be_empty
|
|
end
|
|
end
|
|
|
|
it 'correctly caches theme ids' do
|
|
Theme.destroy_all
|
|
|
|
theme
|
|
theme2 = Fabricate(:theme)
|
|
|
|
expect(Theme.theme_ids).to contain_exactly(theme.id, theme2.id)
|
|
expect(Theme.user_theme_ids).to eq([])
|
|
|
|
theme.update!(user_selectable: true)
|
|
|
|
expect(Theme.user_theme_ids).to contain_exactly(theme.id)
|
|
|
|
theme2.update!(user_selectable: true)
|
|
expect(Theme.user_theme_ids).to contain_exactly(theme.id, theme2.id)
|
|
|
|
theme.update!(user_selectable: false)
|
|
theme2.update!(user_selectable: false)
|
|
|
|
theme.set_default!
|
|
expect(Theme.user_theme_ids).to contain_exactly(theme.id)
|
|
|
|
theme.destroy
|
|
theme2.destroy
|
|
|
|
expect(Theme.theme_ids).to eq([])
|
|
expect(Theme.user_theme_ids).to eq([])
|
|
end
|
|
|
|
it 'correctly caches user_themes template' do
|
|
Theme.destroy_all
|
|
|
|
json = Site.json_for(guardian)
|
|
user_themes = JSON.parse(json)["user_themes"]
|
|
expect(user_themes).to eq([])
|
|
|
|
theme = Fabricate(:theme, name: "bob", user_selectable: true)
|
|
theme.save!
|
|
|
|
json = Site.json_for(guardian)
|
|
user_themes = JSON.parse(json)["user_themes"].map { |t| t["name"] }
|
|
expect(user_themes).to eq(["bob"])
|
|
|
|
theme.name = "sam"
|
|
theme.save!
|
|
|
|
json = Site.json_for(guardian)
|
|
user_themes = JSON.parse(json)["user_themes"].map { |t| t["name"] }
|
|
expect(user_themes).to eq(["sam"])
|
|
|
|
Theme.destroy_all
|
|
|
|
json = Site.json_for(guardian)
|
|
user_themes = JSON.parse(json)["user_themes"]
|
|
expect(user_themes).to eq([])
|
|
end
|
|
|
|
def cached_settings(id)
|
|
Theme.find_by(id: id).included_settings.to_json
|
|
end
|
|
|
|
it 'clears color scheme cache correctly' do
|
|
Theme.destroy_all
|
|
|
|
cs = Fabricate(:color_scheme, name: 'Fancy', color_scheme_colors: [
|
|
Fabricate(:color_scheme_color, name: 'header_primary', hex: 'F0F0F0'),
|
|
Fabricate(:color_scheme_color, name: 'header_background', hex: '1E1E1E'),
|
|
Fabricate(:color_scheme_color, name: 'tertiary', hex: '858585')
|
|
])
|
|
|
|
theme = Fabricate(:theme,
|
|
user_selectable: true,
|
|
user: Fabricate(:admin),
|
|
color_scheme_id: cs.id
|
|
)
|
|
|
|
theme.set_default!
|
|
|
|
expect(ColorScheme.hex_for_name('header_primary')).to eq('F0F0F0')
|
|
|
|
Theme.clear_default!
|
|
|
|
expect(ColorScheme.hex_for_name('header_primary')).to eq('333333')
|
|
end
|
|
|
|
it "correctly notifies about theme changes" do
|
|
cs1 = Fabricate(:color_scheme)
|
|
cs2 = Fabricate(:color_scheme)
|
|
|
|
theme = Fabricate(:theme,
|
|
user_selectable: true,
|
|
user: user,
|
|
color_scheme_id: cs1.id
|
|
)
|
|
|
|
messages = MessageBus.track_publish do
|
|
theme.save!
|
|
end.filter { |m| m.channel == "/file-change" }
|
|
expect(messages.count).to eq(1)
|
|
expect(messages.first.data.map { |d| d[:target] }).to contain_exactly(:desktop_theme, :mobile_theme)
|
|
|
|
# With color scheme change:
|
|
messages = MessageBus.track_publish do
|
|
theme.color_scheme_id = cs2.id
|
|
theme.save!
|
|
end.filter { |m| m.channel == "/file-change" }
|
|
expect(messages.count).to eq(1)
|
|
expect(messages.first.data.map { |d| d[:target] }).to contain_exactly(:admin, :desktop, :desktop_theme, :mobile, :mobile_theme)
|
|
end
|
|
|
|
it 'includes theme_uploads in settings' do
|
|
Theme.destroy_all
|
|
|
|
upload = UploadCreator.new(file_from_fixtures("logo.png"), "logo.png").create_for(-1)
|
|
theme.set_field(type: :theme_upload_var, target: :common, name: "bob", upload_id: upload.id)
|
|
theme.save!
|
|
|
|
json = JSON.parse(cached_settings(theme.id))
|
|
|
|
expect(json["theme_uploads"]["bob"]).to eq(upload.url)
|
|
end
|
|
|
|
it 'handles settings cache correctly' do
|
|
Theme.destroy_all
|
|
|
|
expect(cached_settings(theme.id)).to eq("{}")
|
|
|
|
theme.set_field(target: :settings, name: "yaml", value: "boolean_setting: true")
|
|
theme.save!
|
|
expect(cached_settings(theme.id)).to match(/\"boolean_setting\":true/)
|
|
|
|
theme.settings.first.value = "false"
|
|
theme.save!
|
|
expect(cached_settings(theme.id)).to match(/\"boolean_setting\":false/)
|
|
|
|
child.set_field(target: :settings, name: "yaml", value: "integer_setting: 54")
|
|
|
|
child.save!
|
|
theme.add_relative_theme!(:child, child)
|
|
|
|
json = cached_settings(theme.id)
|
|
expect(json).to match(/\"boolean_setting\":false/)
|
|
expect(json).to match(/\"integer_setting\":54/)
|
|
|
|
expect(cached_settings(child.id)).to eq("{\"integer_setting\":54}")
|
|
|
|
child.destroy!
|
|
json = cached_settings(theme.id)
|
|
expect(json).not_to match(/\"integer_setting\":54/)
|
|
expect(json).to match(/\"boolean_setting\":false/)
|
|
end
|
|
|
|
describe "convert_settings" do
|
|
|
|
it 'can migrate a list field to a string field with json schema' do
|
|
theme.set_field(target: :settings, name: :yaml, value: "valid_json_schema_setting:\n default: \"green,globe\"\n type: \"list\"")
|
|
theme.save!
|
|
|
|
setting = theme.settings.find { |s| s.name == :valid_json_schema_setting }
|
|
setting.value = "red,globe|green,cog|brown,users"
|
|
theme.save!
|
|
|
|
expect(setting.type).to eq(ThemeSetting.types[:list])
|
|
|
|
yaml = File.read("#{Rails.root}/spec/fixtures/theme_settings/valid_settings.yaml")
|
|
theme.set_field(target: :settings, name: "yaml", value: yaml)
|
|
theme.save!
|
|
|
|
theme.convert_settings
|
|
setting = theme.settings.find { |s| s.name == :valid_json_schema_setting }
|
|
|
|
expect(JSON.parse(setting.value)).to eq(JSON.parse('[{"color":"red","icon":"globe"},{"color":"green","icon":"cog"},{"color":"brown","icon":"users"}]'))
|
|
expect(setting.type).to eq(ThemeSetting.types[:string])
|
|
end
|
|
|
|
it 'does not update setting if data does not validate against json schema' do
|
|
theme.set_field(target: :settings, name: :yaml, value: "valid_json_schema_setting:\n default: \"green,globe\"\n type: \"list\"")
|
|
theme.save!
|
|
|
|
setting = theme.settings.find { |s| s.name == :valid_json_schema_setting }
|
|
|
|
# json_schema_settings.yaml defines only two properties per object and disallows additionalProperties
|
|
setting.value = "red,globe,hey|green,cog,hey|brown,users,nay"
|
|
theme.save!
|
|
|
|
yaml = File.read("#{Rails.root}/spec/fixtures/theme_settings/valid_settings.yaml")
|
|
theme.set_field(target: :settings, name: "yaml", value: yaml)
|
|
theme.save!
|
|
|
|
expect { theme.convert_settings }.to raise_error("Schema validation failed")
|
|
|
|
setting.value = "red,globe|green,cog|brown"
|
|
theme.save!
|
|
|
|
expect { theme.convert_settings }.not_to raise_error
|
|
|
|
setting = theme.settings.find { |s| s.name == :valid_json_schema_setting }
|
|
expect(setting.type).to eq(ThemeSetting.types[:string])
|
|
end
|
|
|
|
it 'warns when the theme has modified the setting type but data cannot be converted' do
|
|
begin
|
|
@orig_logger = Rails.logger
|
|
Rails.logger = FakeLogger.new
|
|
|
|
theme.set_field(target: :settings, name: :yaml, value: "valid_json_schema_setting:\n default: \"\"\n type: \"list\"")
|
|
theme.save!
|
|
|
|
setting = theme.settings.find { |s| s.name == :valid_json_schema_setting }
|
|
setting.value = "red,globe"
|
|
theme.save!
|
|
|
|
theme.set_field(target: :settings, name: :yaml, value: "valid_json_schema_setting:\n default: \"\"\n type: \"string\"")
|
|
theme.save!
|
|
|
|
theme.convert_settings
|
|
expect(setting.value).to eq("red,globe")
|
|
expect(Rails.logger.warnings[0]).to include("Theme setting type has changed but cannot be converted.")
|
|
ensure
|
|
Rails.logger = @orig_logger
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "theme translations" do
|
|
it "can list working theme_translation_manager objects" do
|
|
en_translation = ThemeField.create!(theme_id: theme.id, name: "en", type_id: ThemeField.types[:yaml], target_id: Theme.targets[:translations], value: <<~YAML)
|
|
en:
|
|
theme_metadata:
|
|
description: "Description of my theme"
|
|
group_of_translations:
|
|
translation1: en test1
|
|
translation2: en test2
|
|
base_translation1: en test3
|
|
base_translation2: en test4
|
|
YAML
|
|
fr_translation = ThemeField.create!(theme_id: theme.id, name: "fr", type_id: ThemeField.types[:yaml], target_id: Theme.targets[:translations], value: <<~YAML)
|
|
fr:
|
|
group_of_translations:
|
|
translation2: fr test2
|
|
base_translation2: fr test4
|
|
base_translation3: fr test5
|
|
YAML
|
|
|
|
I18n.locale = :fr
|
|
theme.update_translation("group_of_translations.translation1", "overriddentest1")
|
|
translations = theme.translations
|
|
theme.reload
|
|
|
|
expect(translations.map(&:key)).to eq([
|
|
"group_of_translations.translation1",
|
|
"group_of_translations.translation2",
|
|
"base_translation1",
|
|
"base_translation2",
|
|
"base_translation3"
|
|
])
|
|
|
|
expect(translations.map(&:default)).to eq([
|
|
"en test1",
|
|
"fr test2",
|
|
"en test3",
|
|
"fr test4",
|
|
"fr test5"
|
|
])
|
|
|
|
expect(translations.map(&:value)).to eq([
|
|
"overriddentest1",
|
|
"fr test2",
|
|
"en test3",
|
|
"fr test4",
|
|
"fr test5"
|
|
])
|
|
end
|
|
|
|
it "can list internal theme_translation_manager objects" do
|
|
en_translation = ThemeField.create!(theme_id: theme.id, name: "en", type_id: ThemeField.types[:yaml], target_id: Theme.targets[:translations], value: <<~YAML)
|
|
en:
|
|
theme_metadata:
|
|
description: "Description of my theme"
|
|
another_translation: en test4
|
|
YAML
|
|
translations = theme.internal_translations
|
|
expect(translations.map(&:key)).to contain_exactly("theme_metadata.description")
|
|
expect(translations.map(&:value)).to contain_exactly("Description of my theme")
|
|
end
|
|
|
|
it "can create a hash of overridden values" do
|
|
en_translation = ThemeField.create!(theme_id: theme.id, name: "en", type_id: ThemeField.types[:yaml], target_id: Theme.targets[:translations], value: <<~YAML)
|
|
en:
|
|
group_of_translations:
|
|
translation1: en test1
|
|
YAML
|
|
|
|
theme.update_translation("group_of_translations.translation1", "overriddentest1")
|
|
I18n.locale = :fr
|
|
theme.update_translation("group_of_translations.translation1", "overriddentest2")
|
|
theme.reload
|
|
expect(theme.translation_override_hash).to eq(
|
|
"en" => {
|
|
"group_of_translations" => {
|
|
"translation1" => "overriddentest1"
|
|
}
|
|
},
|
|
"fr" => {
|
|
"group_of_translations" => {
|
|
"translation1" => "overriddentest2"
|
|
}
|
|
}
|
|
)
|
|
end
|
|
|
|
it "fall back when listing baked field" do
|
|
theme2 = Fabricate(:theme)
|
|
|
|
en_translation = ThemeField.create!(theme_id: theme.id, name: "en", type_id: ThemeField.types[:yaml], target_id: Theme.targets[:translations], value: '')
|
|
fr_translation = ThemeField.create!(theme_id: theme.id, name: "fr", type_id: ThemeField.types[:yaml], target_id: Theme.targets[:translations], value: '')
|
|
|
|
en_translation2 = ThemeField.create!(theme_id: theme2.id, name: "en", type_id: ThemeField.types[:yaml], target_id: Theme.targets[:translations], value: '')
|
|
|
|
expect(Theme.list_baked_fields([theme.id, theme2.id], :translations, 'fr').map(&:id)).to contain_exactly(fr_translation.id, en_translation2.id)
|
|
end
|
|
end
|
|
|
|
describe "automatic recompile" do
|
|
it 'must recompile after bumping theme_field version' do
|
|
def stub_const(target, const, value)
|
|
old = target.const_get(const)
|
|
target.send(:remove_const, const)
|
|
target.const_set(const, value)
|
|
yield
|
|
ensure
|
|
target.send(:remove_const, const)
|
|
target.const_set(const, old)
|
|
end
|
|
|
|
child.set_field(target: :common, name: "header", value: "World")
|
|
child.set_field(target: :extra_js, name: "test.js.es6", value: "const hello = 'world';")
|
|
child.save!
|
|
|
|
first_common_value = Theme.lookup_field(child.id, :desktop, "header")
|
|
first_extra_js_value = Theme.lookup_field(child.id, :extra_js, nil)
|
|
|
|
Theme.stubs(:compiler_version).returns("SOME_NEW_HASH") do
|
|
second_common_value = Theme.lookup_field(child.id, :desktop, "header")
|
|
second_extra_js_value = Theme.lookup_field(child.id, :extra_js, nil)
|
|
|
|
new_common_compiler_version = ThemeField.find_by(theme_id: child.id, name: "header").compiler_version
|
|
new_extra_js_compiler_version = ThemeField.find_by(theme_id: child.id, name: "test.js.es6").compiler_version
|
|
|
|
expect(first_common_value).to eq(second_common_value)
|
|
expect(first_extra_js_value).to eq(second_extra_js_value)
|
|
|
|
expect(new_common_compiler_version).to eq("SOME_NEW_HASH")
|
|
expect(new_extra_js_compiler_version).to eq("SOME_NEW_HASH")
|
|
end
|
|
end
|
|
|
|
it 'recompiles when the hostname changes' do
|
|
theme.set_field(target: :settings, name: :yaml, value: "name: bob")
|
|
theme_field = theme.set_field(target: :common, name: :after_header, value: '<script>console.log("hello world");</script>')
|
|
theme.save!
|
|
|
|
expect(Theme.lookup_field(theme.id, :common, :after_header)).to include("_ws=#{Discourse.current_hostname}")
|
|
|
|
SiteSetting.force_hostname = "someotherhostname.com"
|
|
Theme.clear_cache!
|
|
|
|
expect(Theme.lookup_field(theme.id, :common, :after_header)).to include("_ws=someotherhostname.com")
|
|
end
|
|
end
|
|
|
|
describe "extra_scss" do
|
|
let(:scss) { "body { background: red}" }
|
|
let(:second_file_scss) { "p { color: blue};" }
|
|
let(:child_scss) { "body { background: green}" }
|
|
|
|
let(:theme) { Fabricate(:theme).tap { |t|
|
|
t.set_field(target: :extra_scss, name: "my_files/magic", value: scss)
|
|
t.set_field(target: :extra_scss, name: "my_files/magic2", value: second_file_scss)
|
|
t.save!
|
|
}}
|
|
|
|
let(:child_theme) { Fabricate(:theme).tap { |t|
|
|
t.component = true
|
|
t.set_field(target: :extra_scss, name: "my_files/moremagic", value: child_scss)
|
|
t.save!
|
|
theme.add_relative_theme!(:child, t)
|
|
}}
|
|
|
|
let(:compiler) {
|
|
manager = Stylesheet::Manager.new(:desktop_theme, theme.id)
|
|
manager.compile(force: true)
|
|
}
|
|
|
|
it "works when importing file by path" do
|
|
theme.set_field(target: :common, name: :scss, value: '@import "my_files/magic";')
|
|
theme.save!
|
|
|
|
css, _map = compiler
|
|
expect(css).to include("body{background:red}")
|
|
end
|
|
|
|
it "works when importing multiple files" do
|
|
theme.set_field(target: :common, name: :scss, value: '@import "my_files/magic"; @import "my_files/magic2"')
|
|
theme.save!
|
|
|
|
css, _map = compiler
|
|
expect(css).to include("body{background:red}")
|
|
expect(css).to include("p{color:blue}")
|
|
end
|
|
|
|
it "works for child themes" do
|
|
child_theme.set_field(target: :common, name: :scss, value: '@import "my_files/moremagic"')
|
|
child_theme.save!
|
|
|
|
manager = Stylesheet::Manager.new(:desktop_theme, child_theme.id)
|
|
css, _map = manager.compile(force: true)
|
|
expect(css).to include("body{background:green}")
|
|
end
|
|
end
|
|
|
|
describe "scss_variables" do
|
|
it "is empty by default" do
|
|
expect(theme.scss_variables).to eq(nil)
|
|
end
|
|
|
|
it "includes settings and uploads when set" do
|
|
theme.set_field(target: :settings, name: :yaml, value: "background_color: red\nfont_size: 25px")
|
|
upload = UploadCreator.new(file_from_fixtures("logo.png"), "logo.png").create_for(-1)
|
|
theme.set_field(type: :theme_upload_var, target: :common, name: "bobby", upload_id: upload.id)
|
|
theme.save!
|
|
|
|
expect(theme.scss_variables).to include("$background_color: unquote(\"red\")")
|
|
expect(theme.scss_variables).to include("$font_size: unquote(\"25px\")")
|
|
expect(theme.scss_variables).to include("$bobby: ")
|
|
end
|
|
end
|
|
|
|
end
|