discourse/spec/lib/stylesheet/manager_spec.rb
Ted Johansson aaec964547
DEV: Add both safe and unsafe Discourse.store.download methods (stable) (#21499)
### Background

Several call sites use `FileStore#download` (through `Discourse.store.download`). In some cases the author seems aware that the method can raise an error if the download fails, and in some cases not. Because of this we're seeing some of these exceptions bubble all the way up and getting logged in production. Although they are not really actionable at that point. Rather each call site needs to be considered to figure out how to handle them.

### What is this change?

This change accomplishes primarily two things.

Firstly it separates the method into a safe version which will handle errors by returning `nil`, and an unsafe version which will re-package upstream errors in a new `FileStore::DownloadError` class.

Secondly it updates the call sites which have been doing error handling downstream to use the new safe version.

For backwards compatibility, there's an interim situation and a desired end state.

**Interim:**

```
FileStore#download      → Old unsafe version. Will raise any error and show a deprecation warning.
FileStore#download!     → New unsafe version. Will raise FileStore::DownloadError.
FileStore#download_safe → New safe version.   Will return nil.
```

**Desired end-state:**

```
FileStore#download  → New safe version.   Will return nil.
FileStore#download! → New unsafe version. Will raise FileStore::DownloadError.
```

### What's next?

We need to do a quick audit of the call sites that are using the old unsafe version without any error handling, as well as check for call sites in plugins other repos. Follow-up PRs incoming.
2023-05-12 11:38:08 +08:00

1053 lines
35 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) do
Fabricate(:theme, component: true, name: "a component").tap do |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!
end
end
let(:theme) do
Fabricate(:theme).tap do |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)
end
end
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 { DiscoursePluginRegistry.reset! }
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],
)
upload2 = UploadCreator.new(image2, "icon.png").create_for(-1)
field =
ThemeField.create!(
theme_id: theme.id,
target_id: Theme.targets[:common],
name: "icon",
value: "",
upload_id: upload2.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) do
Fabricate(:theme).tap do |t|
t.set_field(
target: :common,
name: "color_definitions",
value: ":root {--special: rebeccapurple;}",
)
t.save!
end
end
let(:scss_child) { ':root {--child-definition: #{dark-light-choose(#c00, #fff)};}' }
let(:child) do
Fabricate(:theme, component: true, name: "Child Theme").tap do |t|
t.set_field(target: :common, name: "color_definitions", value: scss_child)
t.save!
end
end
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 { STDERR.stubs(:write) }
after do
STDERR.unstub(:write)
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 = %i[desktop mobile desktop_rtl mobile_rtl admin wizard]
theme_targets = %i[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) { Stylesheet::Manager.precompile_css }
results = StylesheetCache.pluck(:target)
expect(results.size).to eq(core_targets.size)
StylesheetCache.destroy_all
# only themes
output = capture_output(:stderr) { Stylesheet::Manager.precompile_theme_css }
# 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(%r{# sourceMappingURL=[^/]+\.css\.map\?__ws=test\.localhost})
end
it "generates precompiled CSS with a missing upload" do
image = file_from_fixtures("logo.png")
upload = UploadCreator.new(image, "logo.png").create_for(-1)
scheme = ColorScheme.create!(name: "scheme")
core_targets = %i[desktop mobile desktop_rtl mobile_rtl admin wizard]
theme_targets = %i[desktop_theme mobile_theme]
default_theme =
Fabricate(:theme, color_scheme: scheme).tap do |t|
field =
ThemeField.create!(
theme_id: t.id,
target_id: Theme.targets[:common],
name: "logo",
value: "",
upload_id: upload.id,
type_id: ThemeField.types[:theme_upload_var],
)
t.set_field(
target: :common,
name: :scss,
value: "body { background: url($logo); border: 3px solid green; }",
)
t.save!
end
default_theme.set_default!
upload.destroy!
StylesheetCache.destroy_all
Stylesheet::Manager.precompile_theme_css
manager = manager(default_theme.id)
theme_builder =
Stylesheet::Manager::Builder.new(
target: :desktop_theme,
theme: default_theme,
manager: manager,
)
css = File.read(theme_builder.stylesheet_fullpath)
expect(css).to include("border:3px solid green}")
end
end
describe ".fs_asset_cachebuster" do
it "returns a number in test/development mode" do
expect(Stylesheet::Manager.fs_asset_cachebuster).to match(/\A[0-9]+:[0-9]+\z/)
end
context "with production mode enabled" do
before { Stylesheet::Manager.stubs(:use_file_hash_for_cachebuster?).returns(true) }
after do
path = Stylesheet::Manager.send(:manifest_full_path)
File.delete(path) if File.exist?(path)
end
it "returns a hash" do
cachebuster = Stylesheet::Manager.fs_asset_cachebuster
expect(cachebuster).to match(/\A[0-9]+:[0-9a-f]{40}\z/)
end
it "caches the value on the filesystem" do
initial_cachebuster = Stylesheet::Manager.recalculate_fs_asset_cachebuster!
Stylesheet::Manager.stubs(:list_files).never
expect(Stylesheet::Manager.fs_asset_cachebuster).to eq(initial_cachebuster)
expect(File.read(Stylesheet::Manager.send(:manifest_full_path))).to eq(initial_cachebuster)
end
it "updates the hash when a file changes" do
original_files = Stylesheet::Manager.send(:list_files)
initial_cachebuster = Stylesheet::Manager.recalculate_fs_asset_cachebuster!
additional_file_path =
"#{Rails.root}/spec/fixtures/plugins/scss_plugin/assets/stylesheets/colors.scss"
Stylesheet::Manager.stubs(:list_files).returns(original_files + [additional_file_path])
new_cachebuster = Stylesheet::Manager.recalculate_fs_asset_cachebuster!
expect(new_cachebuster).not_to eq(initial_cachebuster)
end
end
end
end