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 : / /m ysite . invalid / uploads / default / optimized / 1 X / 6 d749a141f513f88f167e750e528515002043da1_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 : / /no tdiscourse . 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 (
2024-02-16 19:16:54 +08:00
" <script defer= \" \" src= \" #{ theme_field . javascript_cache . url } \" data-theme-id= \" 1 \" nonce= \" #{ ThemeField :: CSP_NONCE_PLACEHOLDER } \" ></script> " ,
2022-06-20 09:47:37 +08:00
)
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 (
2024-02-16 19:16:54 +08:00
" <script defer= \" \" src= \" #{ field . javascript_cache . url } \" data-theme-id= \" 1 \" nonce= \" #{ ThemeField :: CSP_NONCE_PLACEHOLDER } \" ></script> " ,
2022-06-20 09:47:37 +08:00
)
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 (
2024-02-16 19:16:54 +08:00
" <script defer= \" \" src= \" #{ javascript_cache . url } \" data-theme-id= \" 1 \" nonce= \" #{ ThemeField :: CSP_NONCE_PLACEHOLDER } \" ></script> " ,
2022-06-20 09:47:37 +08:00
)
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 (
2024-02-08 10:20:59 +08:00
I18n . t ( " #{ key } .data_type_inclusion " , name : " invalid_type_setting " ) ,
2018-03-05 08:04:23 +08:00
)
end
it " generates errors when default value is not within allowed range " do
field = create_yaml_field ( get_fixture ( " invalid " ) )
2024-02-21 08:08:26 +08:00
expect ( field . error ) . to include (
I18n . t (
" #{ key } .default_value_not_valid " ,
name : " default_out_of_range " ,
error_messages : [ I18n . t ( " #{ key } .number_value_not_valid_min_max " , min : 1 , max : 20 ) ] . join (
" " ,
) ,
) ,
)
2018-03-05 08:04:23 +08:00
expect ( field . error ) . to include (
2024-02-21 08:08:26 +08:00
I18n . t (
" #{ key } .default_value_not_valid " ,
name : " string_default_out_of_range " ,
error_messages : [ I18n . t ( " #{ key } .string_value_not_valid_min " , min : 20 ) ] . join ( " " ) ,
) ,
2024-02-27 09:16:37 +08:00
)
end
it " generates the right errors when setting of type objects have default values which does not matches the schema " do
field = create_yaml_field ( get_fixture ( " invalid " ) )
expect ( field . error ) . to include (
" Setting `invalid_default_objects_setting` default value isn't valid. The property at JSON Pointer '/0/required_string' must be present. The property at JSON Pointer '/1/min_5_chars_string' must be at least 5 characters long. The property at JSON Pointer '/1/children/0/required_integer' must be present. " ,
2018-03-05 08:04:23 +08:00
)
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 (
2024-02-16 19:16:54 +08:00
" <script defer src= \" #{ fr1 . javascript_cache . url } \" data-theme-id= \" #{ fr1 . theme_id } \" nonce= \" #{ ThemeField :: CSP_NONCE_PLACEHOLDER } \" ></script> " ,
2022-06-20 09:47:37 +08:00
)
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