2017-04-20 04:46:28 +08:00
|
|
|
# encoding: utf-8
|
2019-04-30 08:27:42 +08:00
|
|
|
# frozen_string_literal: true
|
2017-04-20 04:46:28 +08:00
|
|
|
|
2022-07-28 10:27:38 +08:00
|
|
|
RSpec.describe ThemeField do
|
2023-11-10 06:47:59 +08:00
|
|
|
fab!(:theme)
|
2023-06-10 02:53:21 +08:00
|
|
|
|
|
|
|
before do
|
|
|
|
SvgSprite.clear_plugin_svg_sprite_cache!
|
|
|
|
ThemeJavascriptCompiler.disable_terser!
|
|
|
|
end
|
|
|
|
|
2022-10-19 01:20:10 +08:00
|
|
|
after { ThemeJavascriptCompiler.enable_terser! }
|
2018-03-05 08:04:23 +08:00
|
|
|
|
2018-08-08 12:46:34 +08:00
|
|
|
describe "scope: find_by_theme_ids" do
|
|
|
|
it "returns result in the specified order" do
|
|
|
|
theme2 = Fabricate(:theme)
|
|
|
|
theme3 = Fabricate(:theme)
|
|
|
|
|
|
|
|
(0..1).each do |num|
|
|
|
|
ThemeField.create!(theme: theme, target_id: num, name: "header", value: "<a>html</a>")
|
|
|
|
ThemeField.create!(theme: theme2, target_id: num, name: "header", value: "<a>html</a>")
|
|
|
|
ThemeField.create!(theme: theme3, target_id: num, name: "header", value: "<a>html</a>")
|
|
|
|
end
|
|
|
|
|
|
|
|
expect(ThemeField.find_by_theme_ids([theme3.id, theme.id, theme2.id]).pluck(:theme_id)).to eq(
|
|
|
|
[theme3.id, theme3.id, theme.id, theme.id, theme2.id, theme2.id],
|
|
|
|
)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-10-15 12:55:23 +08:00
|
|
|
it "does not insert a script tag when there are no inline script" do
|
|
|
|
theme_field =
|
|
|
|
ThemeField.create!(theme_id: 1, target_id: 0, name: "body_tag", value: "<div>new div</div>")
|
2019-04-12 18:36:08 +08:00
|
|
|
theme_field.ensure_baked!
|
2018-10-15 12:55:23 +08:00
|
|
|
expect(theme_field.value_baked).to_not include("<script")
|
|
|
|
end
|
|
|
|
|
2019-11-12 22:30:19 +08:00
|
|
|
it "adds an error when optimized image links are included" do
|
|
|
|
theme_field = ThemeField.create!(theme_id: 1, target_id: 0, name: "body_tag", value: <<~HTML)
|
|
|
|
<img src="http://mysite.invalid/uploads/default/optimized/1X/6d749a141f513f88f167e750e528515002043da1_2_1282x1000.png"/>
|
|
|
|
HTML
|
|
|
|
theme_field.ensure_baked!
|
|
|
|
expect(theme_field.error).to include(I18n.t("themes.errors.optimized_link"))
|
|
|
|
|
|
|
|
theme_field = ThemeField.create!(theme_id: 1, target_id: 0, name: "scss", value: <<~SCSS)
|
|
|
|
body {
|
|
|
|
background: url(http://mysite.invalid/uploads/default/optimized/1X/6d749a141f513f88f167e750e528515002043da1_2_1282x1000.png);
|
|
|
|
}
|
|
|
|
SCSS
|
|
|
|
theme_field.ensure_baked!
|
|
|
|
expect(theme_field.error).to include(I18n.t("themes.errors.optimized_link"))
|
|
|
|
|
|
|
|
theme_field.update(value: <<~SCSS)
|
|
|
|
body {
|
|
|
|
background: url(http://notdiscourse.invalid/optimized/my_image.png);
|
|
|
|
}
|
|
|
|
SCSS
|
|
|
|
theme_field.ensure_baked!
|
|
|
|
expect(theme_field.error).to eq(nil)
|
|
|
|
end
|
|
|
|
|
2018-10-15 12:55:23 +08:00
|
|
|
it "only extracts inline javascript to an external file" do
|
2018-10-18 14:17:10 +08:00
|
|
|
html = <<~HTML
|
2020-05-07 04:57:14 +08:00
|
|
|
<script type="text/discourse-plugin" version="0.8">
|
|
|
|
var a = "inline discourse plugin";
|
|
|
|
</script>
|
|
|
|
<script type="text/template" data-template="custom-template">
|
|
|
|
<div>custom script type</div>
|
|
|
|
</script>
|
|
|
|
<script>
|
|
|
|
var b = "inline raw script";
|
|
|
|
</script>
|
|
|
|
<script type="texT/jAvasCripT">
|
|
|
|
var c = "text/javascript";
|
|
|
|
</script>
|
|
|
|
<script type="application/javascript">
|
|
|
|
var d = "application/javascript";
|
|
|
|
</script>
|
|
|
|
<script src="/external-script.js"></script>
|
2018-10-18 14:17:10 +08:00
|
|
|
HTML
|
2018-10-15 12:55:23 +08:00
|
|
|
|
|
|
|
theme_field = ThemeField.create!(theme_id: 1, target_id: 0, name: "header", value: html)
|
2019-04-12 18:36:08 +08:00
|
|
|
theme_field.ensure_baked!
|
2022-06-20 09:47:37 +08:00
|
|
|
expect(theme_field.value_baked).to include(
|
|
|
|
"<script defer=\"\" src=\"#{theme_field.javascript_cache.url}\" data-theme-id=\"1\"></script>",
|
|
|
|
)
|
2018-10-15 12:55:23 +08:00
|
|
|
expect(theme_field.value_baked).to include("external-script.js")
|
2018-11-02 04:01:46 +08:00
|
|
|
expect(theme_field.value_baked).to include('<script type="text/template"')
|
|
|
|
expect(theme_field.javascript_cache.content).to include('a = "inline discourse plugin"')
|
|
|
|
expect(theme_field.javascript_cache.content).to include('b = "inline raw script"')
|
|
|
|
expect(theme_field.javascript_cache.content).to include('c = "text/javascript"')
|
|
|
|
expect(theme_field.javascript_cache.content).to include('d = "application/javascript"')
|
|
|
|
end
|
|
|
|
|
|
|
|
it "adds newlines between the extracted javascripts" do
|
|
|
|
html = <<~HTML
|
2020-05-07 04:57:14 +08:00
|
|
|
<script>var a = 10</script>
|
|
|
|
<script>var b = 10</script>
|
2018-11-02 04:01:46 +08:00
|
|
|
HTML
|
|
|
|
|
DEV: Correctly tag heredocs (#16061)
This allows text editors to use correct syntax coloring for the heredoc sections.
Heredoc tag names we use:
languages: SQL, JS, RUBY, LUA, HTML, CSS, SCSS, SH, HBS, XML, YAML/YML, MF, ICS
other: MD, TEXT/TXT, RAW, EMAIL
2022-03-01 03:50:55 +08:00
|
|
|
extracted = <<~JS
|
2020-05-07 04:57:14 +08:00
|
|
|
var a = 10
|
|
|
|
var b = 10
|
DEV: Correctly tag heredocs (#16061)
This allows text editors to use correct syntax coloring for the heredoc sections.
Heredoc tag names we use:
languages: SQL, JS, RUBY, LUA, HTML, CSS, SCSS, SH, HBS, XML, YAML/YML, MF, ICS
other: MD, TEXT/TXT, RAW, EMAIL
2022-03-01 03:50:55 +08:00
|
|
|
JS
|
2018-11-02 04:01:46 +08:00
|
|
|
|
|
|
|
theme_field = ThemeField.create!(theme_id: 1, target_id: 0, name: "header", value: html)
|
2019-04-12 18:36:08 +08:00
|
|
|
theme_field.ensure_baked!
|
2019-01-17 19:46:11 +08:00
|
|
|
expect(theme_field.javascript_cache.content).to include(extracted)
|
2018-10-15 12:55:23 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
it "correctly extracts and generates errors for transpiled js" do
|
2017-04-20 04:46:28 +08:00
|
|
|
html = <<HTML
|
|
|
|
<script type="text/discourse-plugin" version="0.8">
|
|
|
|
badJavaScript(;
|
|
|
|
</script>
|
|
|
|
HTML
|
2017-08-31 12:06:56 +08:00
|
|
|
|
2017-05-03 04:01:01 +08:00
|
|
|
field = ThemeField.create!(theme_id: 1, target_id: 0, name: "header", value: html)
|
2019-04-12 18:36:08 +08:00
|
|
|
field.ensure_baked!
|
2017-04-20 04:46:28 +08:00
|
|
|
expect(field.error).not_to eq(nil)
|
2022-06-20 09:47:37 +08:00
|
|
|
expect(field.value_baked).to include(
|
|
|
|
"<script defer=\"\" src=\"#{field.javascript_cache.url}\" data-theme-id=\"1\"></script>",
|
|
|
|
)
|
2022-10-17 22:04:04 +08:00
|
|
|
expect(field.javascript_cache.content).to include("[THEME 1 'Default'] Compile error")
|
2017-08-31 12:06:56 +08:00
|
|
|
|
|
|
|
field.update!(value: "")
|
2019-04-12 18:36:08 +08:00
|
|
|
field.ensure_baked!
|
2017-04-20 04:46:28 +08:00
|
|
|
expect(field.error).to eq(nil)
|
|
|
|
end
|
|
|
|
|
2018-04-03 17:53:00 +08:00
|
|
|
it "allows us to use theme settings in handlebars templates" do
|
|
|
|
html = <<HTML
|
|
|
|
<script type='text/x-handlebars' data-template-name='my-template'>
|
|
|
|
<div class="testing-div">{{themeSettings.string_setting}}</div>
|
|
|
|
</script>
|
|
|
|
HTML
|
|
|
|
|
2019-04-12 18:36:08 +08:00
|
|
|
ThemeField.create!(
|
|
|
|
theme_id: 1,
|
|
|
|
target_id: 3,
|
|
|
|
name: "yaml",
|
|
|
|
value: "string_setting: \"test text \\\" 123!\"",
|
|
|
|
).ensure_baked!
|
2018-10-15 12:55:23 +08:00
|
|
|
theme_field = ThemeField.create!(theme_id: 1, target_id: 0, name: "head_tag", value: html)
|
2019-04-12 18:36:08 +08:00
|
|
|
theme_field.ensure_baked!
|
2018-10-15 12:55:23 +08:00
|
|
|
javascript_cache = theme_field.javascript_cache
|
2018-04-03 17:53:00 +08:00
|
|
|
|
2022-06-20 09:47:37 +08:00
|
|
|
expect(theme_field.value_baked).to include(
|
|
|
|
"<script defer=\"\" src=\"#{javascript_cache.url}\" data-theme-id=\"1\"></script>",
|
|
|
|
)
|
2018-10-15 12:55:23 +08:00
|
|
|
expect(javascript_cache.content).to include("testing-div")
|
|
|
|
expect(javascript_cache.content).to include("string_setting")
|
2019-01-17 19:46:11 +08:00
|
|
|
expect(javascript_cache.content).to include("test text \\\" 123!")
|
2022-09-04 19:01:10 +08:00
|
|
|
expect(javascript_cache.content).to include(
|
|
|
|
"define(\"discourse/theme-#{theme_field.theme_id}/discourse/templates/my-template\"",
|
|
|
|
)
|
2018-04-03 17:53:00 +08:00
|
|
|
end
|
|
|
|
|
2017-04-20 04:46:28 +08:00
|
|
|
it "correctly generates errors for transpiled css" do
|
|
|
|
css = "body {"
|
2017-05-03 04:01:01 +08:00
|
|
|
field = ThemeField.create!(theme_id: 1, target_id: 0, name: "scss", value: css)
|
2019-04-12 18:36:08 +08:00
|
|
|
field.ensure_baked!
|
2017-04-20 04:46:28 +08:00
|
|
|
expect(field.error).not_to eq(nil)
|
2021-02-03 02:09:41 +08:00
|
|
|
|
|
|
|
field.value = "@import 'missingfile';"
|
2017-04-20 04:46:28 +08:00
|
|
|
field.save!
|
2019-04-12 18:36:08 +08:00
|
|
|
field.ensure_baked!
|
2023-02-07 23:24:57 +08:00
|
|
|
expect(field.error).to include("Error: Can't find stylesheet to import.")
|
2017-04-21 04:55:09 +08:00
|
|
|
|
2021-02-03 02:09:41 +08:00
|
|
|
field.value = "body {color: blue};"
|
|
|
|
field.save!
|
|
|
|
field.ensure_baked!
|
2017-04-20 04:46:28 +08:00
|
|
|
expect(field.error).to eq(nil)
|
|
|
|
end
|
2017-05-11 02:43:05 +08:00
|
|
|
|
2019-04-12 18:36:08 +08:00
|
|
|
it "allows importing scss files" do
|
2021-02-03 02:09:41 +08:00
|
|
|
main_field =
|
|
|
|
theme.set_field(
|
|
|
|
target: :common,
|
|
|
|
name: :scss,
|
|
|
|
value: ".class1{color: red}\n@import 'rootfile1';\n@import 'rootfile3';",
|
2019-04-12 18:36:08 +08:00
|
|
|
)
|
|
|
|
theme.set_field(
|
|
|
|
target: :extra_scss,
|
|
|
|
name: "rootfile1",
|
|
|
|
value: ".class2{color:green}\n@import 'foldername/subfile1';",
|
2023-01-09 19:18:21 +08:00
|
|
|
)
|
2019-04-12 18:36:08 +08:00
|
|
|
theme.set_field(target: :extra_scss, name: "rootfile2", value: ".class3{color:green} ")
|
|
|
|
theme.set_field(
|
|
|
|
target: :extra_scss,
|
|
|
|
name: "foldername/subfile1",
|
|
|
|
value: ".class4{color:yellow}\n@import 'subfile2';",
|
|
|
|
)
|
|
|
|
theme.set_field(
|
|
|
|
target: :extra_scss,
|
|
|
|
name: "foldername/subfile2",
|
|
|
|
value: ".class5{color:yellow}\n@import '../rootfile2';",
|
|
|
|
)
|
2021-02-03 02:09:41 +08:00
|
|
|
theme.set_field(target: :extra_scss, name: "rootfile3", value: ".class6{color:green} ")
|
2019-04-12 18:36:08 +08:00
|
|
|
|
|
|
|
theme.save!
|
|
|
|
result = main_field.compile_scss[0]
|
|
|
|
|
|
|
|
expect(result).to include(".class1")
|
|
|
|
expect(result).to include(".class2")
|
|
|
|
expect(result).to include(".class3")
|
|
|
|
expect(result).to include(".class4")
|
|
|
|
expect(result).to include(".class5")
|
2021-02-03 02:09:41 +08:00
|
|
|
expect(result).to include(".class6")
|
2019-04-12 18:36:08 +08:00
|
|
|
end
|
|
|
|
|
2019-06-03 17:41:00 +08:00
|
|
|
it "correctly handles extra JS fields" do
|
2020-04-07 00:24:59 +08:00
|
|
|
js_field =
|
|
|
|
theme.set_field(
|
|
|
|
target: :extra_js,
|
|
|
|
name: "discourse/controllers/discovery.js.es6",
|
|
|
|
value: "import 'discourse/lib/ajax'; console.log('hello from .js.es6');",
|
|
|
|
)
|
2022-04-07 05:58:10 +08:00
|
|
|
_js_2_field =
|
|
|
|
theme.set_field(
|
|
|
|
target: :extra_js,
|
|
|
|
name: "discourse/controllers/discovery-2.js",
|
|
|
|
value: "import 'discourse/lib/ajax'; console.log('hello from .js');",
|
|
|
|
)
|
2019-06-03 17:41:00 +08:00
|
|
|
hbs_field =
|
|
|
|
theme.set_field(
|
|
|
|
target: :extra_js,
|
|
|
|
name: "discourse/templates/discovery.hbs",
|
|
|
|
value: "{{hello-world}}",
|
|
|
|
)
|
2020-02-12 03:38:12 +08:00
|
|
|
raw_hbs_field =
|
|
|
|
theme.set_field(
|
|
|
|
target: :extra_js,
|
|
|
|
name: "discourse/templates/discovery.hbr",
|
|
|
|
value: "{{hello-world}}",
|
|
|
|
)
|
|
|
|
hbr_field =
|
|
|
|
theme.set_field(
|
|
|
|
target: :extra_js,
|
|
|
|
name: "discourse/templates/other_discovery.hbr",
|
|
|
|
value: "{{hello-world}}",
|
|
|
|
)
|
2019-06-03 17:41:00 +08:00
|
|
|
unknown_field =
|
|
|
|
theme.set_field(
|
|
|
|
target: :extra_js,
|
|
|
|
name: "discourse/controllers/discovery.blah",
|
|
|
|
value: "this wont work",
|
|
|
|
)
|
|
|
|
theme.save!
|
|
|
|
|
2021-04-12 20:02:58 +08:00
|
|
|
js_field.reload
|
2022-10-17 22:04:04 +08:00
|
|
|
expect(js_field.value_baked).to eq("baked")
|
|
|
|
expect(js_field.value_baked).to eq("baked")
|
|
|
|
expect(js_field.value_baked).to eq("baked")
|
2019-06-03 17:41:00 +08:00
|
|
|
|
|
|
|
# All together
|
2022-09-01 18:50:46 +08:00
|
|
|
expect(theme.javascript_cache.content).to include(
|
2023-09-05 17:16:12 +08:00
|
|
|
"define(\"discourse/theme-#{theme.id}/discourse/templates/discovery\", [\"exports\", ",
|
2022-09-01 18:50:46 +08:00
|
|
|
)
|
2020-05-06 00:15:03 +08:00
|
|
|
expect(theme.javascript_cache.content).to include('addRawTemplate("discovery"')
|
2021-04-12 20:02:58 +08:00
|
|
|
expect(theme.javascript_cache.content).to include(
|
2023-08-19 01:15:23 +08:00
|
|
|
"define(\"discourse/theme-#{theme.id}/discourse/controllers/discovery\"",
|
2021-04-12 20:02:58 +08:00
|
|
|
)
|
|
|
|
expect(theme.javascript_cache.content).to include(
|
2023-08-19 01:15:23 +08:00
|
|
|
"define(\"discourse/theme-#{theme.id}/discourse/controllers/discovery-2\"",
|
2021-04-12 20:02:58 +08:00
|
|
|
)
|
2022-08-09 18:53:24 +08:00
|
|
|
expect(theme.javascript_cache.content).to include("const settings =")
|
2022-10-17 22:04:04 +08:00
|
|
|
expect(theme.javascript_cache.content).to include(
|
|
|
|
"[THEME #{theme.id} '#{theme.name}'] Compile error: unknown file extension 'blah' (discourse/controllers/discovery.blah)",
|
|
|
|
)
|
2022-10-19 01:20:10 +08:00
|
|
|
|
|
|
|
# Check sourcemap
|
|
|
|
expect(theme.javascript_cache.source_map).to eq(nil)
|
|
|
|
ThemeJavascriptCompiler.enable_terser!
|
|
|
|
js_field.update(compiler_version: "0")
|
|
|
|
theme.save!
|
|
|
|
|
|
|
|
expect(theme.javascript_cache.source_map).not_to eq(nil)
|
|
|
|
map = JSON.parse(theme.javascript_cache.source_map)
|
|
|
|
|
|
|
|
expect(map["sources"]).to contain_exactly(
|
|
|
|
"discourse/controllers/discovery-2.js",
|
|
|
|
"discourse/controllers/discovery.blah",
|
|
|
|
"discourse/controllers/discovery.js",
|
|
|
|
"discourse/templates/discovery.js",
|
|
|
|
"discovery.js",
|
|
|
|
"other_discovery.js",
|
|
|
|
)
|
|
|
|
expect(map["sourceRoot"]).to eq("theme-#{theme.id}/")
|
|
|
|
expect(map["sourcesContent"].length).to eq(6)
|
2019-06-03 17:41:00 +08:00
|
|
|
end
|
|
|
|
|
2017-12-19 23:10:44 +08:00
|
|
|
def create_upload_theme_field!(name)
|
|
|
|
ThemeField
|
|
|
|
.create!(
|
|
|
|
theme_id: 1,
|
|
|
|
target_id: 0,
|
|
|
|
value: "",
|
|
|
|
type_id: ThemeField.types[:theme_upload_var],
|
|
|
|
name: name,
|
2019-04-12 18:36:08 +08:00
|
|
|
)
|
|
|
|
.tap { |tf| tf.ensure_baked! }
|
2017-12-19 23:10:44 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
it "ensures we don't use invalid SCSS variable names" do
|
|
|
|
expect { create_upload_theme_field!("42") }.to raise_error(ActiveRecord::RecordInvalid)
|
|
|
|
expect { create_upload_theme_field!("a42") }.not_to raise_error
|
|
|
|
end
|
|
|
|
|
2018-03-05 08:04:23 +08:00
|
|
|
def get_fixture(type)
|
|
|
|
File.read("#{Rails.root}/spec/fixtures/theme_settings/#{type}_settings.yaml")
|
|
|
|
end
|
|
|
|
|
|
|
|
def create_yaml_field(value)
|
|
|
|
field =
|
|
|
|
ThemeField.create!(
|
|
|
|
theme_id: 1,
|
|
|
|
target_id: Theme.targets[:settings],
|
|
|
|
name: "yaml",
|
|
|
|
value: value,
|
|
|
|
)
|
2019-04-12 18:36:08 +08:00
|
|
|
field.ensure_baked!
|
2018-03-05 08:04:23 +08:00
|
|
|
field
|
|
|
|
end
|
|
|
|
|
|
|
|
let(:key) { "themes.settings_errors" }
|
|
|
|
|
2019-03-08 22:49:06 +08:00
|
|
|
it "forces re-transpilation of theme JS when settings YAML changes" do
|
|
|
|
settings_field =
|
|
|
|
ThemeField.create!(
|
|
|
|
theme: theme,
|
|
|
|
target_id: Theme.targets[:settings],
|
|
|
|
name: "yaml",
|
|
|
|
value: "setting: 5",
|
|
|
|
)
|
|
|
|
|
|
|
|
html = <<~HTML
|
|
|
|
<script type="text/discourse-plugin" version="0.8">
|
|
|
|
alert(settings.setting);
|
|
|
|
</script>
|
|
|
|
HTML
|
|
|
|
|
|
|
|
js_field =
|
|
|
|
ThemeField.create!(
|
|
|
|
theme: theme,
|
|
|
|
target_id: ThemeField.types[:html],
|
|
|
|
name: "header",
|
|
|
|
value: html,
|
|
|
|
)
|
|
|
|
old_value_baked = js_field.value_baked
|
|
|
|
settings_field.update!(value: "setting: 66")
|
|
|
|
js_field.reload
|
|
|
|
|
|
|
|
expect(js_field.value_baked).to eq(nil)
|
|
|
|
js_field.ensure_baked!
|
|
|
|
expect(js_field.value_baked).to be_present
|
|
|
|
expect(js_field.value_baked).not_to eq(old_value_baked)
|
|
|
|
end
|
|
|
|
|
2018-03-05 08:04:23 +08:00
|
|
|
it "generates errors for bad YAML" do
|
|
|
|
yaml = "invalid_setting 5"
|
|
|
|
field = create_yaml_field(yaml)
|
|
|
|
expect(field.error).to eq(I18n.t("#{key}.invalid_yaml"))
|
|
|
|
|
|
|
|
field.value = "valid_setting: true"
|
|
|
|
field.save!
|
2019-04-12 18:36:08 +08:00
|
|
|
field.ensure_baked!
|
2018-03-05 08:04:23 +08:00
|
|
|
expect(field.error).to eq(nil)
|
|
|
|
end
|
|
|
|
|
|
|
|
it "generates errors when default value's type doesn't match setting type" do
|
|
|
|
field = create_yaml_field(get_fixture("invalid"))
|
|
|
|
expect(field.error).to include(
|
|
|
|
I18n.t("#{key}.default_not_match_type", name: "no_match_setting"),
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
it "generates errors when no default value is passed" do
|
|
|
|
field = create_yaml_field(get_fixture("invalid"))
|
|
|
|
expect(field.error).to include(
|
|
|
|
I18n.t("#{key}.default_value_missing", name: "no_default_setting"),
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
it "generates errors when invalid type is passed" do
|
|
|
|
field = create_yaml_field(get_fixture("invalid"))
|
|
|
|
expect(field.error).to include(
|
|
|
|
I18n.t("#{key}.data_type_not_a_number", name: "invalid_type_setting"),
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
it "generates errors when default value is not within allowed range" do
|
|
|
|
field = create_yaml_field(get_fixture("invalid"))
|
|
|
|
expect(field.error).to include(I18n.t("#{key}.default_out_range", name: "default_out_of_range"))
|
|
|
|
expect(field.error).to include(
|
|
|
|
I18n.t("#{key}.default_out_range", name: "string_default_out_of_range"),
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
it "works correctly when valid yaml is provided" do
|
|
|
|
field = create_yaml_field(get_fixture("valid"))
|
|
|
|
expect(field.error).to be_nil
|
|
|
|
end
|
2019-01-17 19:46:11 +08:00
|
|
|
|
|
|
|
describe "locale fields" do
|
|
|
|
let!(:theme) { Fabricate(:theme) }
|
|
|
|
let!(:theme2) { Fabricate(:theme) }
|
|
|
|
let!(:theme3) { Fabricate(:theme) }
|
|
|
|
|
|
|
|
let!(:en1) do
|
2020-05-07 04:57:14 +08:00
|
|
|
ThemeField.create!(
|
|
|
|
theme: theme,
|
|
|
|
target_id: Theme.targets[:translations],
|
|
|
|
name: "en",
|
|
|
|
value: {
|
|
|
|
en: {
|
|
|
|
somestring1: "helloworld",
|
|
|
|
group: {
|
|
|
|
key1: "enval1",
|
|
|
|
},
|
|
|
|
},
|
2019-01-17 19:46:11 +08:00
|
|
|
}.deep_stringify_keys.to_yaml,
|
|
|
|
)
|
2023-01-09 19:18:21 +08:00
|
|
|
end
|
2019-01-17 19:46:11 +08:00
|
|
|
let!(:fr1) do
|
|
|
|
ThemeField.create!(
|
|
|
|
theme: theme,
|
2020-05-07 04:57:14 +08:00
|
|
|
target_id: Theme.targets[:translations],
|
2019-01-17 19:46:11 +08:00
|
|
|
name: "fr",
|
2023-01-09 19:18:21 +08:00
|
|
|
value: {
|
|
|
|
fr: {
|
2019-01-17 19:46:11 +08:00
|
|
|
somestring1: "bonjourworld",
|
2023-01-09 19:18:21 +08:00
|
|
|
group: {
|
2019-01-17 19:46:11 +08:00
|
|
|
key2: "frval2",
|
2023-01-09 19:18:21 +08:00
|
|
|
},
|
|
|
|
},
|
2019-01-17 19:46:11 +08:00
|
|
|
}.deep_stringify_keys.to_yaml,
|
2023-01-09 19:18:21 +08:00
|
|
|
)
|
|
|
|
end
|
2019-01-17 19:46:11 +08:00
|
|
|
let!(:fr2) do
|
|
|
|
ThemeField.create!(
|
2020-05-07 04:57:14 +08:00
|
|
|
theme: theme2,
|
2019-01-17 19:46:11 +08:00
|
|
|
target_id: Theme.targets[:translations],
|
|
|
|
name: "fr",
|
|
|
|
value: "",
|
2023-01-09 19:18:21 +08:00
|
|
|
)
|
|
|
|
end
|
2020-05-07 04:57:14 +08:00
|
|
|
let!(:en2) do
|
2019-01-17 19:46:11 +08:00
|
|
|
ThemeField.create!(
|
|
|
|
theme: theme2,
|
|
|
|
target_id: Theme.targets[:translations],
|
2020-05-07 04:57:14 +08:00
|
|
|
name: "en",
|
2019-01-17 19:46:11 +08:00
|
|
|
value: "",
|
|
|
|
)
|
2023-01-09 19:18:21 +08:00
|
|
|
end
|
2019-01-17 19:46:11 +08:00
|
|
|
let!(:ca3) do
|
|
|
|
ThemeField.create!(
|
|
|
|
theme: theme3,
|
|
|
|
target_id: Theme.targets[:translations],
|
|
|
|
name: "ca",
|
|
|
|
value: "",
|
|
|
|
)
|
2023-01-09 19:18:21 +08:00
|
|
|
end
|
2019-01-17 19:46:11 +08:00
|
|
|
let!(:en3) do
|
|
|
|
ThemeField.create!(
|
|
|
|
theme: theme3,
|
|
|
|
target_id: Theme.targets[:translations],
|
2020-05-07 04:57:14 +08:00
|
|
|
name: "en",
|
|
|
|
value: "",
|
|
|
|
)
|
|
|
|
end
|
2019-01-17 19:46:11 +08:00
|
|
|
|
|
|
|
describe "scopes" do
|
2019-02-26 22:22:02 +08:00
|
|
|
it "filter_locale_fields returns results in the correct order" do
|
|
|
|
expect(
|
|
|
|
ThemeField.find_by_theme_ids([theme3.id, theme.id, theme2.id]).filter_locale_fields(
|
2020-05-07 04:57:14 +08:00
|
|
|
%w[en fr],
|
2023-01-09 19:18:21 +08:00
|
|
|
),
|
2019-01-17 19:46:11 +08:00
|
|
|
).to eq([en3, en1, fr1, en2, fr2])
|
|
|
|
end
|
|
|
|
|
|
|
|
it "find_first_locale_fields returns only the first locale for each theme" do
|
|
|
|
expect(
|
2020-05-07 04:57:14 +08:00
|
|
|
ThemeField.find_first_locale_fields([theme3.id, theme.id, theme2.id], %w[ca en fr]),
|
2019-01-17 19:46:11 +08:00
|
|
|
).to eq([ca3, en1, en2])
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe "#raw_translation_data" do
|
|
|
|
it "errors if the top level key is incorrect" do
|
|
|
|
fr1.update(value: { wrongkey: { somestring1: "bonjourworld" } }.deep_stringify_keys.to_yaml)
|
|
|
|
expect { fr1.raw_translation_data }.to raise_error(ThemeTranslationParser::InvalidYaml)
|
|
|
|
end
|
|
|
|
|
|
|
|
it "errors if there are multiple top level keys" do
|
|
|
|
fr1.update(
|
|
|
|
value: {
|
|
|
|
fr: {
|
|
|
|
somestring1: "bonjourworld",
|
|
|
|
},
|
|
|
|
otherkey: "hello",
|
|
|
|
}.deep_stringify_keys.to_yaml,
|
|
|
|
)
|
|
|
|
expect { fr1.raw_translation_data }.to raise_error(ThemeTranslationParser::InvalidYaml)
|
|
|
|
end
|
|
|
|
|
|
|
|
it "errors if YAML includes arrays" do
|
|
|
|
fr1.update(value: { fr: %w[val1 val2] }.deep_stringify_keys.to_yaml)
|
|
|
|
expect { fr1.raw_translation_data }.to raise_error(ThemeTranslationParser::InvalidYaml)
|
|
|
|
end
|
|
|
|
|
|
|
|
it "errors if YAML has invalid syntax" do
|
|
|
|
fr1.update(value: "fr: 'valuewithoutclosequote")
|
|
|
|
expect { fr1.raw_translation_data }.to raise_error(ThemeTranslationParser::InvalidYaml)
|
|
|
|
end
|
2022-09-03 00:28:18 +08:00
|
|
|
|
|
|
|
it "works when locale file doesn't contain translations" do
|
|
|
|
fr1.update(value: "fr:")
|
|
|
|
expect(fr1.translation_data).to eq(
|
|
|
|
fr: {
|
|
|
|
},
|
|
|
|
en: {
|
|
|
|
somestring1: "helloworld",
|
|
|
|
group: {
|
|
|
|
key1: "enval1",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
)
|
|
|
|
end
|
2019-01-17 19:46:11 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
describe "#translation_data" do
|
|
|
|
it "loads correctly" do
|
|
|
|
expect(fr1.translation_data).to eq(
|
|
|
|
fr: {
|
|
|
|
somestring1: "bonjourworld",
|
|
|
|
group: {
|
|
|
|
key2: "frval2",
|
|
|
|
},
|
|
|
|
},
|
2020-05-07 04:57:14 +08:00
|
|
|
en: {
|
|
|
|
somestring1: "helloworld",
|
|
|
|
group: {
|
|
|
|
key1: "enval1",
|
|
|
|
},
|
|
|
|
},
|
2019-01-17 19:46:11 +08:00
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
it "raises errors for the current locale" do
|
|
|
|
fr1.update(value: { wrongkey: "hello" }.deep_stringify_keys.to_yaml)
|
|
|
|
expect { fr1.translation_data }.to raise_error(ThemeTranslationParser::InvalidYaml)
|
|
|
|
end
|
|
|
|
|
|
|
|
it "doesn't raise errors for the fallback locale" do
|
|
|
|
en1.update(value: { wrongkey: "hello" }.deep_stringify_keys.to_yaml)
|
|
|
|
expect(fr1.translation_data).to eq(
|
|
|
|
fr: {
|
|
|
|
somestring1: "bonjourworld",
|
|
|
|
group: {
|
|
|
|
key2: "frval2",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
it "merges any overrides" do
|
|
|
|
# Overrides in the current locale (so in tests that will be english)
|
|
|
|
theme.update_translation("group.key1", "overriddentest1")
|
|
|
|
theme.reload
|
|
|
|
expect(fr1.translation_data).to eq(
|
|
|
|
fr: {
|
|
|
|
somestring1: "bonjourworld",
|
|
|
|
group: {
|
|
|
|
key2: "frval2",
|
|
|
|
},
|
|
|
|
},
|
2020-05-07 04:57:14 +08:00
|
|
|
en: {
|
|
|
|
somestring1: "helloworld",
|
|
|
|
group: {
|
|
|
|
key1: "overriddentest1",
|
|
|
|
},
|
|
|
|
},
|
2019-01-17 19:46:11 +08:00
|
|
|
)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe "javascript cache" do
|
|
|
|
it "is generated correctly" do
|
|
|
|
fr1.ensure_baked!
|
2022-06-20 09:47:37 +08:00
|
|
|
expect(fr1.value_baked).to include(
|
|
|
|
"<script defer src='#{fr1.javascript_cache.url}' data-theme-id='#{fr1.theme_id}'></script>",
|
|
|
|
)
|
2019-01-17 19:46:11 +08:00
|
|
|
expect(fr1.javascript_cache.content).to include("bonjourworld")
|
|
|
|
expect(fr1.javascript_cache.content).to include("helloworld")
|
|
|
|
expect(fr1.javascript_cache.content).to include("enval1")
|
|
|
|
end
|
2023-11-15 03:53:27 +08:00
|
|
|
|
|
|
|
it "is recreated when data changes" do
|
|
|
|
t = Fabricate(:theme)
|
|
|
|
t.set_field(
|
|
|
|
target: "translations",
|
|
|
|
name: "fr",
|
|
|
|
value: { fr: { mykey: "initial value" } }.deep_stringify_keys.to_yaml,
|
|
|
|
)
|
|
|
|
t.save!
|
|
|
|
|
|
|
|
field = t.theme_fields.find_by(target_id: Theme.targets[:translations], name: "fr")
|
|
|
|
expect(field.javascript_cache.content).to include("initial value")
|
|
|
|
|
|
|
|
t.set_field(
|
|
|
|
target: "translations",
|
|
|
|
name: "fr",
|
|
|
|
value: { fr: { mykey: "new value" } }.deep_stringify_keys.to_yaml,
|
|
|
|
)
|
|
|
|
t.save!
|
|
|
|
|
|
|
|
field = t.theme_fields.find_by(target_id: Theme.targets[:translations], name: "fr")
|
|
|
|
expect(field.javascript_cache.reload.content).to include("new value")
|
|
|
|
end
|
|
|
|
|
|
|
|
it "is recreated when fallback data changes" do
|
|
|
|
t = Fabricate(:theme)
|
|
|
|
t.set_field(
|
|
|
|
target: "translations",
|
|
|
|
name: "fr",
|
|
|
|
value: { fr: {} }.deep_stringify_keys.to_yaml,
|
|
|
|
)
|
|
|
|
t.set_field(
|
|
|
|
target: "translations",
|
|
|
|
name: "en",
|
|
|
|
value: { en: { myotherkey: "initial value" } }.deep_stringify_keys.to_yaml,
|
|
|
|
)
|
|
|
|
t.save!
|
|
|
|
|
|
|
|
field = t.theme_fields.find_by(target_id: Theme.targets[:translations], name: "fr")
|
|
|
|
expect(field.javascript_cache.content).to include("initial value")
|
|
|
|
|
|
|
|
t.set_field(
|
|
|
|
target: "translations",
|
|
|
|
name: "en",
|
|
|
|
value: { en: { myotherkey: "new value" } }.deep_stringify_keys.to_yaml,
|
|
|
|
)
|
|
|
|
t.save!
|
|
|
|
|
|
|
|
field = t.theme_fields.find_by(target_id: Theme.targets[:translations], name: "fr")
|
|
|
|
expect(field.javascript_cache.reload.content).to include("new value")
|
|
|
|
end
|
2019-01-17 19:46:11 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
describe "prefix injection" do
|
|
|
|
it "injects into JS" do
|
|
|
|
html = <<~HTML
|
2020-05-07 04:57:14 +08:00
|
|
|
<script type="text/discourse-plugin" version="0.8">
|
|
|
|
var a = "inline discourse plugin";
|
|
|
|
</script>
|
2019-01-17 19:46:11 +08:00
|
|
|
HTML
|
|
|
|
|
|
|
|
theme_field =
|
|
|
|
ThemeField.create!(theme_id: theme.id, target_id: 0, name: "head_tag", value: html)
|
2019-04-12 18:36:08 +08:00
|
|
|
theme_field.ensure_baked!
|
2019-01-17 19:46:11 +08:00
|
|
|
javascript_cache = theme_field.javascript_cache
|
|
|
|
expect(javascript_cache.content).to include("inline discourse plugin")
|
|
|
|
expect(javascript_cache.content).to include("theme_translations.#{theme.id}.")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2022-07-28 00:14:14 +08:00
|
|
|
describe "SVG sprite theme fields" do
|
2023-03-15 02:11:45 +08:00
|
|
|
let :svg_content do
|
2023-03-21 00:41:23 +08:00
|
|
|
"<svg><symbol id='test'></symbol></svg>"
|
2023-03-15 02:11:45 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
let :upload_file do
|
|
|
|
tmp = Tempfile.new("test.svg")
|
|
|
|
File.write(tmp.path, svg_content)
|
|
|
|
tmp
|
|
|
|
end
|
|
|
|
|
|
|
|
after { upload_file.unlink }
|
|
|
|
|
|
|
|
let(:upload) do
|
|
|
|
UploadCreator.new(upload_file, "test.svg", for_theme: true).create_for(
|
|
|
|
Discourse::SYSTEM_USER_ID,
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
2020-11-25 07:49:12 +08:00
|
|
|
let(:theme) { Fabricate(:theme) }
|
|
|
|
let(:theme_field) do
|
|
|
|
ThemeField.create!(
|
|
|
|
theme: theme,
|
|
|
|
target_id: 0,
|
|
|
|
name: SvgSprite.theme_sprite_variable_name,
|
|
|
|
upload: upload,
|
|
|
|
value: "",
|
|
|
|
value_baked: "baked",
|
|
|
|
type_id: ThemeField.types[:theme_upload_var],
|
|
|
|
)
|
2023-01-09 19:18:21 +08:00
|
|
|
end
|
2020-11-25 07:49:12 +08:00
|
|
|
|
|
|
|
it "is rebaked when upload changes" do
|
2023-03-15 02:11:45 +08:00
|
|
|
fname = "custom-theme-icon-sprite.svg"
|
|
|
|
sprite = UploadCreator.new(file_from_fixtures(fname), fname, for_theme: true).create_for(-1)
|
|
|
|
theme_field.update(upload: sprite)
|
2020-11-25 07:49:12 +08:00
|
|
|
expect(theme_field.value_baked).to eq(nil)
|
|
|
|
end
|
2021-07-15 03:18:29 +08:00
|
|
|
|
|
|
|
it "clears SVG sprite cache when upload is deleted" do
|
2023-03-15 02:11:45 +08:00
|
|
|
theme_field
|
2023-03-21 00:41:23 +08:00
|
|
|
expect(SvgSprite.custom_svgs(theme.id).size).to eq(1)
|
2021-07-15 03:18:29 +08:00
|
|
|
|
|
|
|
theme_field.destroy!
|
2023-03-21 00:41:23 +08:00
|
|
|
expect(SvgSprite.custom_svgs(theme.id).size).to eq(0)
|
2021-07-15 03:18:29 +08:00
|
|
|
end
|
2021-07-26 10:35:27 +08:00
|
|
|
|
|
|
|
it "crashes gracefully when svg is invalid" do
|
|
|
|
FileStore::LocalStore.any_instance.stubs(:path_for).returns(nil)
|
|
|
|
expect(theme_field.validate_svg_sprite_xml).to match("Error with icons-sprite")
|
|
|
|
end
|
2023-09-02 01:22:58 +08:00
|
|
|
|
|
|
|
it "raises an error when sprite is too big" do
|
|
|
|
fname = "theme-icon-sprite.svg"
|
|
|
|
symbols = ""
|
|
|
|
|
2023-09-13 13:00:26 +08:00
|
|
|
3500.times do |i|
|
2023-09-02 01:22:58 +08:00
|
|
|
id = "icon-id-#{i}"
|
|
|
|
path =
|
|
|
|
"M#{rand(1..100)} 2.18-2.087.277-4.216.42-6.378.42s-4.291-.143-6.378-.42c-1.085-.144-1.872-1.086-1.872-2.18v-4.25m16.5 0a2.18 2.18 0 00.75-1.661V8.706c0-1.081-.768-2.015-1.837-2.175a48.114 48.114 0 00-3.413-.387m4.5 8.006c-.194.165-.42.295-.673.38A23.978 23.978 0 0112 15.75c-2.648 0-5.195-.429-7.577-1.22a2.016 2.016 0 01-.673-.38m0 0A2.18 2.18 0 013 12.489V8.706c0-1.081.768-2.015 1.837-2.175a48.111 48.111 0 013.413-.387m7.5 0V5.25A2.25 2.25 0 0013.5 3h-3a2.25 .008z"
|
|
|
|
symbols += "<symbol id='#{id}' viewBox='0 0 100 100'><path d='#{path}'/></symbol>\n"
|
|
|
|
end
|
|
|
|
|
|
|
|
contents =
|
|
|
|
"<?xml version='1.0' encoding='UTF-8'?><svg><symbol id='customthemeicon' viewBox='0 0 100 100'><path d='M0 0h1ssss00v100H0z'/></symbol>#{symbols}</svg>"
|
|
|
|
|
|
|
|
sprite =
|
|
|
|
UploadCreator.new(file_from_contents(contents, fname), fname, for_theme: true).create_for(
|
|
|
|
-1,
|
|
|
|
)
|
|
|
|
|
|
|
|
theme_field.update(upload: sprite)
|
|
|
|
|
|
|
|
expect(theme_field.validate_svg_sprite_xml).to match("Error with icons-sprite")
|
|
|
|
end
|
2020-11-25 07:49:12 +08:00
|
|
|
end
|
|
|
|
|
2022-07-28 00:14:14 +08:00
|
|
|
describe "local js assets" do
|
2022-04-07 05:58:10 +08:00
|
|
|
let :js_content do
|
|
|
|
"// not transpiled; console.log('hello world');"
|
|
|
|
end
|
|
|
|
|
|
|
|
let :upload_file do
|
|
|
|
tmp = Tempfile.new(%w[jsfile .js])
|
|
|
|
File.write(tmp.path, js_content)
|
|
|
|
tmp
|
|
|
|
end
|
|
|
|
|
|
|
|
after { upload_file.unlink }
|
|
|
|
|
|
|
|
it "correctly handles local JS asset caching" do
|
2023-01-09 19:18:21 +08:00
|
|
|
upload =
|
2022-04-07 05:58:10 +08:00
|
|
|
UploadCreator.new(upload_file, "test.js", for_theme: true).create_for(
|
|
|
|
Discourse::SYSTEM_USER_ID,
|
2023-01-09 19:18:21 +08:00
|
|
|
)
|
2022-04-07 05:58:10 +08:00
|
|
|
|
|
|
|
js_field =
|
|
|
|
theme.set_field(
|
|
|
|
target: :common,
|
|
|
|
type_id: ThemeField.types[:theme_upload_var],
|
|
|
|
name: "test_js",
|
|
|
|
upload_id: upload.id,
|
|
|
|
)
|
|
|
|
|
|
|
|
common_field =
|
|
|
|
theme.set_field(
|
|
|
|
target: :common,
|
|
|
|
name: "head_tag",
|
|
|
|
value: "<script>let c = 'd';</script>",
|
|
|
|
type: :html,
|
|
|
|
)
|
|
|
|
|
|
|
|
theme.set_field(target: :settings, type: :yaml, name: "yaml", value: "hello: world")
|
|
|
|
|
|
|
|
theme.set_field(
|
|
|
|
target: :extra_js,
|
|
|
|
name: "discourse/controllers/discovery.js.es6",
|
|
|
|
value: "import 'discourse/lib/ajax'; console.log('hello from .js.es6');",
|
|
|
|
)
|
|
|
|
|
|
|
|
theme.save!
|
|
|
|
|
|
|
|
# a bit fragile, but at least we test it properly
|
|
|
|
[
|
|
|
|
theme.reload.javascript_cache.content,
|
|
|
|
common_field.reload.javascript_cache.content,
|
|
|
|
].each do |js|
|
|
|
|
js_to_eval = <<~JS
|
|
|
|
var settings;
|
|
|
|
var window = {};
|
|
|
|
var require = function(name) {
|
|
|
|
if(name == "discourse/lib/theme-settings-store") {
|
|
|
|
return({
|
|
|
|
registerSettings: function(id, s) {
|
|
|
|
settings = s;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
window.require = require;
|
|
|
|
#{js}
|
|
|
|
settings
|
|
|
|
JS
|
|
|
|
|
|
|
|
ctx = MiniRacer::Context.new
|
|
|
|
val = ctx.eval(js_to_eval)
|
|
|
|
ctx.dispose
|
|
|
|
|
|
|
|
expect(val["theme_uploads"]["test_js"]).to eq(js_field.upload.url)
|
|
|
|
expect(val["theme_uploads_local"]["test_js"]).to eq(js_field.javascript_cache.local_url)
|
2022-07-29 05:20:52 +08:00
|
|
|
expect(val["theme_uploads_local"]["test_js"]).to start_with("/theme-javascripts/")
|
2022-04-07 05:58:10 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
# this is important, we do not want local_js_urls to leak into scss
|
|
|
|
expect(theme.scss_variables).to include("$hello: unquote(\"world\");")
|
|
|
|
expect(theme.scss_variables).to include("$test_js: unquote(\"#{upload.url}\");")
|
|
|
|
|
|
|
|
expect(theme.scss_variables).not_to include("theme_uploads")
|
|
|
|
end
|
|
|
|
end
|
FEATURE: Theme settings migrations (#24071)
This commit introduces a new feature that allows theme developers to manage the transformation of theme settings over time. Similar to Rails migrations, the theme settings migration system enables developers to write and execute migrations for theme settings, ensuring a smooth transition when changes are required in the format or structure of setting values.
Example use cases for the theme settings migration system:
1. Renaming a theme setting.
2. Changing the data type of a theme setting (e.g., transforming a string setting containing comma-separated values into a proper list setting).
3. Altering the format of data stored in a theme setting.
All of these use cases and more are now possible while preserving theme setting values for sites that have already modified their theme settings.
Usage:
1. Create a top-level directory called `migrations` in your theme/component, and then within the `migrations` directory create another directory called `settings`.
2. Inside the `migrations/settings` directory, create a JavaScript file using the format `XXXX-some-name.js`, where `XXXX` is a unique 4-digit number, and `some-name` is a descriptor of your choice that describes the migration.
3. Within the JavaScript file, define and export (as the default) a function called `migrate`. This function will receive a `Map` object and must also return a `Map` object (it's acceptable to return the same `Map` object that the function received).
4. The `Map` object received by the `migrate` function will include settings that have been overridden or changed by site administrators. Settings that have never been changed from the default will not be included.
5. The keys and values contained in the `Map` object that the `migrate` function returns will replace all the currently changed settings of the theme.
6. Migrations are executed in numerical order based on the XXXX segment in the migration filenames. For instance, `0001-some-migration.js` will be executed before `0002-another-migration.js`.
Here's a complete example migration script that renames a setting from `setting_with_old_name` to `setting_with_new_name`:
```js
// File name: 0001-rename-setting.js
export default function migrate(settings) {
if (settings.has("setting_with_old_name")) {
settings.set("setting_with_new_name", settings.get("setting_with_old_name"));
}
return settings;
}
```
Internal topic: t/109980
2023-11-02 13:10:15 +08:00
|
|
|
|
|
|
|
describe "migration JavaScript field" do
|
|
|
|
it "must match a specific format for filename" do
|
|
|
|
field = Fabricate(:migration_theme_field, theme: theme)
|
|
|
|
field.name = "12-some-name"
|
|
|
|
|
|
|
|
expect(field.valid?).to eq(false)
|
|
|
|
expect(field.errors.full_messages).to contain_exactly(
|
|
|
|
I18n.t("themes.import_error.migrations.invalid_filename", filename: "12-some-name"),
|
|
|
|
)
|
|
|
|
|
|
|
|
field.name = "00012-some-name"
|
|
|
|
|
|
|
|
expect(field.valid?).to eq(false)
|
|
|
|
expect(field.errors.full_messages).to contain_exactly(
|
|
|
|
I18n.t("themes.import_error.migrations.invalid_filename", filename: "00012-some-name"),
|
|
|
|
)
|
|
|
|
|
|
|
|
field.name = "0012some-name"
|
|
|
|
|
|
|
|
expect(field.valid?).to eq(false)
|
|
|
|
expect(field.errors.full_messages).to contain_exactly(
|
|
|
|
I18n.t("themes.import_error.migrations.invalid_filename", filename: "0012some-name"),
|
|
|
|
)
|
|
|
|
|
|
|
|
field.name = "0012"
|
|
|
|
|
|
|
|
expect(field.valid?).to eq(false)
|
|
|
|
expect(field.errors.full_messages).to contain_exactly(
|
|
|
|
I18n.t("themes.import_error.migrations.invalid_filename", filename: "0012"),
|
|
|
|
)
|
|
|
|
|
|
|
|
field.name = "0012-something"
|
|
|
|
|
|
|
|
expect(field.valid?).to eq(true)
|
|
|
|
end
|
|
|
|
|
|
|
|
it "doesn't allow weird characters in the name" do
|
|
|
|
field = Fabricate(:migration_theme_field, theme: theme)
|
|
|
|
field.name = "0012-ëèard"
|
|
|
|
|
|
|
|
expect(field.valid?).to eq(false)
|
|
|
|
expect(field.errors.full_messages).to contain_exactly(
|
|
|
|
I18n.t("themes.import_error.migrations.invalid_filename", filename: "0012-ëèard"),
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
it "imposes a limit on the name part in the filename" do
|
|
|
|
stub_const(ThemeField, "MIGRATION_NAME_PART_MAX_LENGTH", 10) do
|
|
|
|
field = Fabricate(:migration_theme_field, theme: theme)
|
|
|
|
field.name = "0012-#{"a" * 11}"
|
|
|
|
|
|
|
|
expect(field.valid?).to eq(false)
|
|
|
|
expect(field.errors.full_messages).to contain_exactly(
|
|
|
|
I18n.t("themes.import_error.migrations.name_too_long", count: 10),
|
|
|
|
)
|
|
|
|
|
|
|
|
field.name = "0012-#{"a" * 10}"
|
|
|
|
|
|
|
|
expect(field.valid?).to eq(true)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2017-04-20 04:46:28 +08:00
|
|
|
end
|