discourse/spec/lib/stylesheet/manager_spec.rb
Rafael dos Santos Silva 2d1dbc6f96
FEATURE: Preload resources via link header (#18475)
Experiment moving from preload tags in the document head to preload information the the response headers.

While this is a minor improvement in most browsers (headers are parsed before the response body), this allows smart proxies like Cloudflare to "learn" from those headers and build HTTP 103 Early Hints for subsequent requests to the same URI, which will allow the user agent to download and parse our JS/CSS while we are waiting for the server to generate and stream the HTML response.

Co-authored-by: Penar Musaraj <pmusaraj@gmail.com>
2022-10-07 13:19:50 -03:00

867 lines
31 KiB
Ruby

# frozen_string_literal: true
require 'stylesheet/compiler'
RSpec.describe Stylesheet::Manager do
def manager(theme_id = nil)
Stylesheet::Manager.new(theme_id: theme_id)
end
it 'does not crash for missing theme' do
Theme.clear_default!
link = manager.stylesheet_link_tag(:embedded_theme)
expect(link).to eq("")
end
it "still returns something for no themes" do
link = manager.stylesheet_link_tag(:desktop, 'all')
expect(link).not_to eq("")
end
describe "themes with components" do
let(:child_theme) { Fabricate(:theme, component: true, name: "a component").tap { |c|
c.set_field(target: :common, name: "scss", value: ".child_common{.scss{color: red;}}")
c.set_field(target: :desktop, name: "scss", value: ".child_desktop{.scss{color: red;}}")
c.set_field(target: :mobile, name: "scss", value: ".child_mobile{.scss{color: red;}}")
c.set_field(target: :common, name: "embedded_scss", value: ".child_embedded{.scss{color: red;}}")
c.save!
}}
let(:theme) { Fabricate(:theme).tap { |t|
t.set_field(target: :common, name: "scss", value: ".common{.scss{color: red;}}")
t.set_field(target: :desktop, name: "scss", value: ".desktop{.scss{color: red;}}")
t.set_field(target: :mobile, name: "scss", value: ".mobile{.scss{color: red;}}")
t.set_field(target: :common, name: "embedded_scss", value: ".embedded{.scss{color: red;}}")
t.save!
t.add_relative_theme!(:child, child_theme)
}}
it "generates the right links for non-theme targets" do
manager = manager(nil)
hrefs = manager.stylesheet_details(:desktop, 'all')
expect(hrefs.length).to eq(1)
end
it 'can correctly compile theme css' do
manager = manager(theme.id)
old_links = manager.stylesheet_link_tag(:desktop_theme, 'all')
builder = Stylesheet::Manager::Builder.new(
target: :desktop_theme, theme: theme, manager: manager
)
builder.compile(force: true)
css = File.read(builder.stylesheet_fullpath)
_source_map = File.read(builder.source_map_fullpath)
expect(css).to match(/\.common/)
expect(css).to match(/\.desktop/)
# child theme CSS is no longer bundled with main theme
expect(css).not_to match(/child_common/)
expect(css).not_to match(/child_desktop/)
child_theme_builder = Stylesheet::Manager::Builder.new(
target: :desktop_theme, theme: child_theme, manager: manager
)
child_theme_builder.compile(force: true)
child_css = File.read(child_theme_builder.stylesheet_fullpath)
_child_source_map = File.read(child_theme_builder.source_map_fullpath)
expect(child_css).to match(/child_common/)
expect(child_css).to match(/child_desktop/)
child_theme.set_field(target: :desktop, name: :scss, value: ".nothing{color: green;}")
child_theme.save!
new_links = manager(theme.id).stylesheet_link_tag(:desktop_theme, 'all')
expect(new_links).not_to eq(old_links)
# our theme better have a name with the theme_id as part of it
expect(new_links).to include("/stylesheets/desktop_theme_#{theme.id}_")
expect(new_links).to include("/stylesheets/desktop_theme_#{child_theme.id}_")
end
it 'can correctly compile embedded theme css' do
manager = manager(theme.id)
builder = Stylesheet::Manager::Builder.new(
target: :embedded_theme, theme: theme, manager: manager
)
builder.compile(force: true)
css = File.read(builder.stylesheet_fullpath)
expect(css).to match(/\.embedded/)
expect(css).not_to match(/\.child_embedded/)
child_theme_builder = Stylesheet::Manager::Builder.new(
target: :embedded_theme,
theme: child_theme,
manager: manager
)
child_theme_builder.compile(force: true)
css = File.read(child_theme_builder.stylesheet_fullpath)
expect(css).to match(/\.child_embedded/)
end
it 'includes both parent and child theme assets' do
manager = manager(theme.id)
hrefs = manager.stylesheet_details(:desktop_theme, 'all')
expect(hrefs.count).to eq(2)
expect(hrefs.map { |href| href[:theme_id] }).to contain_exactly(
theme.id, child_theme.id
)
hrefs = manager.stylesheet_details(:embedded_theme, 'all')
expect(hrefs.count).to eq(2)
expect(hrefs.map { |href| href[:theme_id] }).to contain_exactly(
theme.id, child_theme.id
)
end
it "includes the escaped theme name" do
manager = manager(theme.id)
theme.update(name: "a strange name\"with a quote in it")
tag = manager.stylesheet_link_tag(:desktop_theme)
expect(tag).to have_tag("link", with: {
"data-theme-name" => theme.name.downcase
})
expect(tag).to have_tag("link", with: {
"data-theme-name" => child_theme.name.downcase
})
end
it "stylesheet_link_tag calls the preload callback when set" do
preload_list = []
preload_callback = ->(href, type) { preload_list << [href, type] }
manager = manager(theme.id)
expect {
manager.stylesheet_link_tag(:desktop_theme, 'all', preload_callback)
}.to change(preload_list, :size)
end
context "with stylesheet order" do
let(:z_child_theme) do
Fabricate(:theme, component: true, name: "ze component").tap do |z|
z.set_field(target: :desktop, name: "scss", value: ".child_desktop{.scss{color: red;}}")
z.save!
end
end
let(:remote) { RemoteTheme.create!(remote_url: "https://github.com/org/remote-theme1") }
let(:child_remote) do
Fabricate(:theme, remote_theme: remote, component: true).tap do |t|
t.set_field(target: :desktop, name: "scss", value: ".child_desktop{.scss{color: red;}}")
t.save!
end
end
it 'output remote child, then sort children alphabetically, then local parent' do
theme.add_relative_theme!(:child, z_child_theme)
theme.add_relative_theme!(:child, child_remote)
manager = manager(theme.id)
hrefs = manager.stylesheet_details(:desktop_theme, 'all')
parent = hrefs.select { |href| href[:theme_id] == theme.id }.first
child_a = hrefs.select { |href| href[:theme_id] == child_theme.id }.first
child_z = hrefs.select { |href| href[:theme_id] == z_child_theme.id }.first
child_r = hrefs.select { |href| href[:theme_id] == child_remote.id }.first
child_local_A = "<link href=\"#{child_a[:new_href]}\" data-theme-id=\"#{child_a[:theme_id]}\" data-theme-name=\"#{child_a[:theme_name]}\"/>"
child_local_Z = "<link href=\"#{child_z[:new_href]}\" data-theme-id=\"#{child_z[:theme_id]}\" data-theme-name=\"#{child_z[:theme_name]}\"/>"
child_remote_R = "<link href=\"#{child_r[:new_href]}\" data-theme-id=\"#{child_r[:theme_id]}\" data-theme-name=\"#{child_r[:theme_name]}\"/>"
parent_local = "<link href=\"#{parent[:new_href]}\" data-theme-id=\"#{parent[:theme_id]}\" data-theme-name=\"#{parent[:theme_name]}\"/>"
link_hrefs = manager.stylesheet_link_tag(:desktop_theme).gsub('media="all" rel="stylesheet" data-target="desktop_theme" ', '')
expect(link_hrefs).to eq([child_remote_R, child_local_A, child_local_Z, parent_local].join("\n").html_safe)
end
it "output remote child, remote parent, local child" do
remote2 = RemoteTheme.create!(remote_url: "https://github.com/org/remote-theme2")
remote_main_theme = Fabricate(:theme, remote_theme: remote2, name: "remote main").tap do |t|
t.set_field(target: :desktop, name: "scss", value: ".el{color: red;}")
t.save!
end
remote_main_theme.add_relative_theme!(:child, z_child_theme)
remote_main_theme.add_relative_theme!(:child, child_remote)
manager = manager(remote_main_theme.id)
hrefs = manager.stylesheet_details(:desktop_theme, 'all')
parent_r = hrefs.select { |href| href[:theme_id] == remote_main_theme.id }.first
child_z = hrefs.select { |href| href[:theme_id] == z_child_theme.id }.first
child_r = hrefs.select { |href| href[:theme_id] == child_remote.id }.first
parent_remote = "<link href=\"#{parent_r[:new_href]}\" data-theme-id=\"#{parent_r[:theme_id]}\" data-theme-name=\"#{parent_r[:theme_name]}\"/>"
child_local = "<link href=\"#{child_z[:new_href]}\" data-theme-id=\"#{child_z[:theme_id]}\" data-theme-name=\"#{child_z[:theme_name]}\"/>"
child_remote = "<link href=\"#{child_r[:new_href]}\" data-theme-id=\"#{child_r[:theme_id]}\" data-theme-name=\"#{child_r[:theme_name]}\"/>"
link_hrefs = manager.stylesheet_link_tag(:desktop_theme).gsub('media="all" rel="stylesheet" data-target="desktop_theme" ', '')
expect(link_hrefs).to eq([child_remote, parent_remote, child_local].join("\n").html_safe)
end
end
it 'outputs tags for non-theme targets for theme component' do
child_theme = Fabricate(:theme, component: true)
hrefs = manager(child_theme.id).stylesheet_details(:desktop, 'all')
expect(hrefs.count).to eq(1) # desktop
end
it 'does not output tags for component targets with no styles' do
embedded_scss_child = Fabricate(:theme, component: true)
embedded_scss_child.set_field(target: :common, name: "embedded_scss", value: ".scss{color: red;}")
embedded_scss_child.save!
theme.add_relative_theme!(:child, embedded_scss_child)
manager = manager(theme.id)
hrefs = manager.stylesheet_details(:desktop_theme, 'all')
expect(hrefs.count).to eq(2) # theme + child_theme
hrefs = manager.stylesheet_details(:embedded_theme, 'all')
expect(hrefs.count).to eq(3) # theme + child_theme + embedded_scss_child
end
it '.stylesheet_details can find components mobile SCSS when target is `:mobile_theme`' do
child_with_mobile_scss = Fabricate(:theme, component: true)
child_with_mobile_scss.set_field(target: :mobile, name: :scss, value: "body { color: red; }")
child_with_mobile_scss.save!
theme.add_relative_theme!(:child, child_with_mobile_scss)
manager = manager(theme.id)
hrefs = manager.stylesheet_details(:mobile_theme, 'all')
expect(hrefs.count).to eq(3)
expect(hrefs.find { |h| h[:theme_id] == child_with_mobile_scss.id }).to be_present
end
it 'does not output multiple assets for non-theme targets' do
manager = manager()
hrefs = manager.stylesheet_details(:admin, 'all')
expect(hrefs.count).to eq(1)
hrefs = manager.stylesheet_details(:mobile, 'all')
expect(hrefs.count).to eq(1)
end
end
describe 'digest' do
after do
DiscoursePluginRegistry.reset!
end
it 'can correctly account for plugins in default digest' do
builder = Stylesheet::Manager::Builder.new(target: :desktop, manager: manager)
digest1 = builder.digest
DiscoursePluginRegistry.stylesheets["fake"] = Set.new(["fake_file"])
builder = Stylesheet::Manager::Builder.new(target: :desktop, manager: manager)
digest2 = builder.digest
expect(digest1).not_to eq(digest2)
end
it "can correctly account for settings in theme's components" do
theme = Fabricate(:theme)
child = Fabricate(:theme, component: true)
theme.add_relative_theme!(:child, child)
child.set_field(target: :settings, name: :yaml, value: "childcolor: red")
child.set_field(target: :common, name: :scss, value: "body {background-color: $childcolor}")
child.save!
manager = manager(theme.id)
builder = Stylesheet::Manager::Builder.new(
target: :desktop_theme, theme: theme, manager: manager
)
digest1 = builder.digest
child.update_setting(:childcolor, "green")
manager = manager(theme.id)
builder = Stylesheet::Manager::Builder.new(
target: :desktop_theme, theme: theme, manager: manager
)
digest2 = builder.digest
expect(digest1).not_to eq(digest2)
end
let(:image) { file_from_fixtures("logo.png") }
let(:image2) { file_from_fixtures("logo-dev.png") }
it 'can correctly account for theme uploads in digest' do
theme = Fabricate(:theme)
upload = UploadCreator.new(image, "logo.png").create_for(-1)
field = ThemeField.create!(
theme_id: theme.id,
target_id: Theme.targets[:common],
name: "logo",
value: "",
upload_id: upload.id,
type_id: ThemeField.types[:theme_upload_var]
)
manager = manager(theme.id)
builder = Stylesheet::Manager::Builder.new(
target: :desktop_theme, theme: theme, manager: manager
)
digest1 = builder.digest
field.destroy!
upload = UploadCreator.new(image2, "logo.png").create_for(-1)
field = ThemeField.create!(
theme_id: theme.id,
target_id: Theme.targets[:common],
name: "logo",
value: "",
upload_id: upload.id,
type_id: ThemeField.types[:theme_upload_var]
)
builder = Stylesheet::Manager::Builder.new(
target: :desktop_theme, theme: theme.reload, manager: manager
)
digest2 = builder.digest
expect(digest1).not_to eq(digest2)
end
it 'can generate digest with a missing upload record' do
theme = Fabricate(:theme)
upload = UploadCreator.new(image, "logo.png").create_for(-1)
field = ThemeField.create!(
theme_id: theme.id,
target_id: Theme.targets[:common],
name: "logo",
value: "",
upload_id: upload.id,
type_id: ThemeField.types[:theme_upload_var]
)
manager = manager(theme.id)
builder = Stylesheet::Manager::Builder.new(
target: :desktop_theme, theme: theme, manager: manager
)
digest1 = builder.digest
upload.delete
builder = Stylesheet::Manager::Builder.new(
target: :desktop_theme, theme: theme.reload, manager: manager
)
digest2 = builder.digest
expect(digest1).not_to eq(digest2)
end
it 'returns different digest based on target' do
theme = Fabricate(:theme)
builder = Stylesheet::Manager::Builder.new(target: :desktop_theme, theme: theme, manager: manager)
expect(builder.digest).to eq(builder.theme_digest)
builder = Stylesheet::Manager::Builder.new(target: :color_definitions, manager: manager)
expect(builder.digest).to eq(builder.color_scheme_digest)
builder = Stylesheet::Manager::Builder.new(target: :admin, manager: manager)
expect(builder.digest).to eq(builder.default_digest)
builder = Stylesheet::Manager::Builder.new(target: :desktop, manager: manager)
expect(builder.digest).to eq(builder.default_digest)
end
it 'returns different digest based on hostname' do
theme = Fabricate(:theme)
SiteSetting.force_hostname = "host1.example.com"
initial_theme_digest = Stylesheet::Manager::Builder.new(target: :desktop_theme, theme: theme, manager: manager).digest
initial_color_scheme_digest = Stylesheet::Manager::Builder.new(target: :color_definitions, manager: manager).digest
initial_default_digest = Stylesheet::Manager::Builder.new(target: :desktop, manager: manager).digest
SiteSetting.force_hostname = "host2.example.com"
new_theme_digest = Stylesheet::Manager::Builder.new(target: :desktop_theme, theme: theme, manager: manager).digest
new_color_scheme_digest = Stylesheet::Manager::Builder.new(target: :color_definitions, manager: manager).digest
new_default_digest = Stylesheet::Manager::Builder.new(target: :desktop, manager: manager).digest
expect(initial_theme_digest).not_to eq(new_theme_digest)
expect(initial_color_scheme_digest).not_to eq(new_color_scheme_digest)
expect(initial_default_digest).not_to eq(new_default_digest)
end
end
describe 'color_scheme_digest' do
fab!(:theme) { Fabricate(:theme) }
it "changes with category background image" do
category1 = Fabricate(:category, uploaded_background_id: 123, updated_at: 1.week.ago)
category2 = Fabricate(:category, uploaded_background_id: 456, updated_at: 2.days.ago)
manager = manager(theme.id)
builder = Stylesheet::Manager::Builder.new(
target: :desktop_theme, theme: theme, manager: manager
)
digest1 = builder.color_scheme_digest
category2.update!(uploaded_background_id: 789, updated_at: 1.day.ago)
digest2 = builder.color_scheme_digest
expect(digest2).to_not eq(digest1)
category1.update!(uploaded_background_id: nil, updated_at: 5.minutes.ago)
digest3 = builder.color_scheme_digest
expect(digest3).to_not eq(digest2)
expect(digest3).to_not eq(digest1)
end
it "updates digest when updating a color scheme" do
scheme = ColorScheme.create_from_base(name: "Neutral", base_scheme_id: "Neutral")
manager = manager(theme.id)
builder = Stylesheet::Manager::Builder.new(
target: :color_definitions, theme: theme, color_scheme: scheme, manager: manager
)
digest1 = builder.color_scheme_digest
ColorSchemeRevisor.revise(scheme, colors: [{ name: "primary", hex: "CC0000" }])
digest2 = builder.color_scheme_digest
expect(digest1).to_not eq(digest2)
end
it "updates digest when updating a theme's color definitions" do
scheme = ColorScheme.base
manager = manager(theme.id)
builder = Stylesheet::Manager::Builder.new(
target: :color_definitions, theme: theme, color_scheme: scheme, manager: manager
)
digest1 = builder.color_scheme_digest
theme.set_field(target: :common, name: :color_definitions, value: 'body {color: brown}')
theme.save!
manager = manager(theme.id)
builder = Stylesheet::Manager::Builder.new(
target: :color_definitions, theme: theme, color_scheme: scheme, manager: manager
)
digest2 = builder.color_scheme_digest
expect(digest1).to_not eq(digest2)
end
it "updates digest when updating a theme component's color definitions" do
scheme = ColorScheme.base
manager = manager(theme.id)
builder = Stylesheet::Manager::Builder.new(
target: :color_definitions, theme: theme, color_scheme: scheme, manager: manager
)
digest1 = builder.color_scheme_digest
child_theme = Fabricate(:theme, component: true)
child_theme.set_field(target: :common, name: "color_definitions", value: 'body {color: fuchsia}')
child_theme.save!
theme.add_relative_theme!(:child, child_theme)
theme.save!
manager = manager(theme.id)
builder = Stylesheet::Manager::Builder.new(
target: :color_definitions, theme: theme, color_scheme: scheme, manager: manager
)
digest2 = builder.color_scheme_digest
expect(digest1).to_not eq(digest2)
child_theme.set_field(target: :common, name: "color_definitions", value: 'body {color: blue}')
child_theme.save!
manager = manager(theme.id)
builder = Stylesheet::Manager::Builder.new(
target: :color_definitions, theme: theme, color_scheme: scheme, manager: manager
)
digest3 = builder.color_scheme_digest
expect(digest2).to_not eq(digest3)
end
it "updates digest when setting fonts" do
manager = manager(theme.id)
builder = Stylesheet::Manager::Builder.new(
target: :desktop_theme, theme: theme, manager: manager
)
digest1 = builder.color_scheme_digest
SiteSetting.base_font = DiscourseFonts.fonts[2][:key]
digest2 = builder.color_scheme_digest
expect(digest1).to_not eq(digest2)
SiteSetting.heading_font = DiscourseFonts.fonts[4][:key]
digest3 = builder.color_scheme_digest
expect(digest3).to_not eq(digest2)
end
end
describe 'color_scheme_stylesheets' do
it "returns something by default" do
link = manager.color_scheme_stylesheet_link_tag
expect(link).to include("color_definitions_base")
end
it "does not crash when no default theme is set" do
SiteSetting.default_theme_id = -1
link = manager.color_scheme_stylesheet_link_tag
expect(link).to include("color_definitions_base")
end
it "loads base scheme when defined scheme id is missing" do
link = manager.color_scheme_stylesheet_link_tag(125)
expect(link).to include("color_definitions_base")
end
it "loads nothing when defined dark scheme id is missing" do
link = manager.color_scheme_stylesheet_link_tag(125, "(prefers-color-scheme: dark)")
expect(link).to eq("")
end
it "uses the correct color scheme from the default site theme" do
cs = Fabricate(:color_scheme, name: 'Funky')
theme = Fabricate(:theme, color_scheme_id: cs.id)
SiteSetting.default_theme_id = theme.id
link = manager.color_scheme_stylesheet_link_tag()
expect(link).to include("/stylesheets/color_definitions_funky_#{cs.id}_")
end
it "uses the correct color scheme when a non-default theme is selected and it uses the base 'Light' scheme" do
cs = Fabricate(:color_scheme, name: 'Not This')
ColorSchemeRevisor.revise(cs, colors: [{ name: "primary", hex: "CC0000" }])
default_theme = Fabricate(:theme, color_scheme_id: cs.id)
SiteSetting.default_theme_id = default_theme.id
user_theme = Fabricate(:theme, color_scheme_id: nil)
link = manager(user_theme.id).color_scheme_stylesheet_link_tag(nil, "all")
expect(link).to include("/stylesheets/color_definitions_base_")
stylesheet = Stylesheet::Manager::Builder.new(
target: :color_definitions, theme: user_theme, manager: manager
).compile(force: true)
expect(stylesheet).not_to include("--primary: #c00;")
expect(stylesheet).to include("--primary: #222;") # from base scheme
end
it "uses the correct scheme when a valid scheme id is used" do
link = manager.color_scheme_stylesheet_link_tag(ColorScheme.first.id)
slug = Slug.for(ColorScheme.first.name) + "_" + ColorScheme.first.id.to_s
expect(link).to include("/stylesheets/color_definitions_#{slug}_")
end
it "does not fail with a color scheme name containing spaces and special characters" do
cs = Fabricate(:color_scheme, name: 'Funky Bunch -_ @#$*(')
theme = Fabricate(:theme, color_scheme_id: cs.id)
SiteSetting.default_theme_id = theme.id
link = manager.color_scheme_stylesheet_link_tag
expect(link).to include("/stylesheets/color_definitions_funky-bunch_#{cs.id}_")
end
it "updates outputted colors when updating a color scheme" do
scheme = ColorScheme.create_from_base(name: "Neutral", base_scheme_id: "Neutral")
theme = Fabricate(:theme)
manager = manager(theme.id)
builder = Stylesheet::Manager::Builder.new(
target: :color_definitions, theme: theme, color_scheme: scheme, manager: manager
)
stylesheet = builder.compile
ColorSchemeRevisor.revise(scheme, colors: [{ name: "primary", hex: "CC0000" }])
builder2 = Stylesheet::Manager::Builder.new(
target: :color_definitions, theme: theme, color_scheme: scheme, manager: manager
)
stylesheet2 = builder2.compile
expect(stylesheet).not_to eq(stylesheet2)
expect(stylesheet2).to include("--primary: #c00;")
end
it "includes updated font definitions" do
details1 = manager.color_scheme_stylesheet_details(nil, "all")
SiteSetting.base_font = DiscourseFonts.fonts[2][:key]
details2 = manager.color_scheme_stylesheet_details(nil, "all")
expect(details1[:new_href]).not_to eq(details2[:new_href])
end
it "calls the preload callback when set" do
preload_list = []
cs = Fabricate(:color_scheme, name: 'Funky')
theme = Fabricate(:theme, color_scheme_id: cs.id)
preload_callback = ->(href, type) { preload_list << [href, type] }
expect {
manager.color_scheme_stylesheet_link_tag(theme.id, 'all', preload_callback)
}.to change(preload_list, :size).by(1)
end
context "with theme colors" do
let(:theme) { Fabricate(:theme).tap { |t|
t.set_field(target: :common, name: "color_definitions", value: ':root {--special: rebeccapurple;}')
t.save!
}}
let(:scss_child) { ':root {--child-definition: #{dark-light-choose(#c00, #fff)};}' }
let(:child) { Fabricate(:theme, component: true, name: "Child Theme").tap { |t|
t.set_field(target: :common, name: "color_definitions", value: scss_child)
t.save!
}}
let(:scheme) { ColorScheme.base }
let(:dark_scheme) { ColorScheme.create_from_base(name: 'Dark', base_scheme_id: 'Dark') }
it "includes theme color definitions in color scheme" do
manager = manager(theme.id)
stylesheet = Stylesheet::Manager::Builder.new(
target: :color_definitions, theme: theme, color_scheme: scheme, manager: manager
).compile(force: true)
expect(stylesheet).to include("--special: rebeccapurple")
end
it "includes child color definitions in color schemes" do
theme.add_relative_theme!(:child, child)
theme.save!
manager = manager(theme.id)
stylesheet = Stylesheet::Manager::Builder.new(
target: :color_definitions, theme: theme, color_scheme: scheme, manager: manager
).compile(force: true)
expect(stylesheet).to include("--special: rebeccapurple")
expect(stylesheet).to include("--child-definition: #c00")
end
it "respects selected color scheme in child color definitions" do
theme.add_relative_theme!(:child, child)
theme.save!
manager = manager(theme.id)
stylesheet = Stylesheet::Manager::Builder.new(
target: :color_definitions, theme: theme, color_scheme: dark_scheme, manager: manager
).compile(force: true)
expect(stylesheet).to include("--special: rebeccapurple")
expect(stylesheet).to include("--child-definition: #fff")
end
it "fails gracefully for broken SCSS" do
scss = "$test: $missing-var;"
theme.set_field(target: :common, name: "color_definitions", value: scss)
theme.save!
manager = manager(theme.id)
stylesheet = Stylesheet::Manager::Builder.new(
target: :color_definitions, theme: theme, color_scheme: scheme, manager: manager
)
expect { stylesheet.compile }.not_to raise_error
end
it "child theme SCSS includes the default theme's color scheme variables" do
SiteSetting.default_theme_id = theme.id
custom_scheme = ColorScheme.create_from_base(name: "Neutral", base_scheme_id: "Neutral")
ColorSchemeRevisor.revise(custom_scheme, colors: [{ name: "primary", hex: "CC0000" }])
theme.color_scheme_id = custom_scheme.id
theme.save!
scss = "body{ border: 2px solid $primary;}"
child.set_field(target: :common, name: "scss", value: scss)
child.save!
manager = manager(theme.id)
child_theme_manager = Stylesheet::Manager::Builder.new(
target: :desktop_theme, theme: child, manager: manager
)
child_theme_manager.compile(force: true)
child_css = File.read(child_theme_manager.stylesheet_fullpath)
expect(child_css).to include("body{border:2px solid #c00}")
end
end
context 'with encoded slugs' do
before { SiteSetting.slug_generation_method = 'encoded' }
after { SiteSetting.slug_generation_method = 'ascii' }
it "strips unicode in color scheme stylesheet filenames" do
cs = Fabricate(:color_scheme, name: 'Grün')
cs2 = Fabricate(:color_scheme, name: '어두운')
link = manager.color_scheme_stylesheet_link_tag(cs.id)
expect(link).to include("/stylesheets/color_definitions_grun_#{cs.id}_")
link2 = manager.color_scheme_stylesheet_link_tag(cs2.id)
expect(link2).to include("/stylesheets/color_definitions_scheme_#{cs2.id}_")
end
end
end
describe ".precompile css" do
before do
class << STDERR
alias_method :orig_write, :write
def write(x)
end
end
end
after do
class << STDERR
def write(x)
orig_write(x)
end
end
FileUtils.rm_rf("tmp/stylesheet-cache")
end
it "correctly generates precompiled CSS" do
scheme1 = ColorScheme.create!(name: "scheme1")
scheme2 = ColorScheme.create!(name: "scheme2")
core_targets = [:desktop, :mobile, :desktop_rtl, :mobile_rtl, :admin, :wizard]
theme_targets = [:desktop_theme, :mobile_theme]
Theme.update_all(user_selectable: false)
user_theme = Fabricate(:theme, user_selectable: true, color_scheme: scheme1)
default_theme = Fabricate(:theme, user_selectable: true, color_scheme: scheme2)
child_theme = Fabricate(:theme).tap do |t|
t.component = true
t.save!
user_theme.add_relative_theme!(:child, t)
end
child_theme_with_css = Fabricate(:theme).tap do |t|
t.component = true
t.set_field(
target: :common,
name: :scss,
value: "body { background: green }"
)
t.save!
user_theme.add_relative_theme!(:child, t)
default_theme.add_relative_theme!(:child, t)
end
default_theme.set_default!
StylesheetCache.destroy_all
# only core
output = capture_output(:stderr) do
Stylesheet::Manager.precompile_css
end
results = StylesheetCache.pluck(:target)
expect(results.size).to eq(core_targets.size)
StylesheetCache.destroy_all
# only themes
output = capture_output(:stderr) do
Stylesheet::Manager.precompile_theme_css
end
# Ensure we force compile each theme only once
expect(output.scan(/#{child_theme_with_css.name}/).length).to eq(2)
results = StylesheetCache.pluck(:target)
expect(results.size).to eq(22) # (3 themes * 2 targets) + 16 color schemes (2 themes * 8 color schemes (7 defaults + 1 theme scheme))
# themes + core
Stylesheet::Manager.precompile_css
results = StylesheetCache.pluck(:target)
expect(results.size).to eq(28) # 9 core targets + 9 theme + 10 color schemes
theme_targets.each do |tar|
expect(results.count { |target| target =~ /^#{tar}_(#{user_theme.id}|#{default_theme.id})$/ }).to eq(2)
end
Theme.clear_default!
StylesheetCache.destroy_all
# themes + core with no theme set as default
Stylesheet::Manager.precompile_css
Stylesheet::Manager.precompile_theme_css
results = StylesheetCache.pluck(:target)
expect(results.size).to eq(28) # 9 core targets + 9 theme + 10 color schemes
expect(results).to include("color_definitions_#{scheme1.name}_#{scheme1.id}_#{user_theme.id}")
expect(results).to include("color_definitions_#{scheme2.name}_#{scheme2.id}_#{default_theme.id}")
# Check that sourceMappingURL includes __ws parameter
content = StylesheetCache.last.content
expect(content).to match(/# sourceMappingURL=[^\/]+\.css\.map\?__ws=test\.localhost/)
end
end
end