mirror of
https://github.com/discourse/discourse.git
synced 2025-06-25 01:30:17 +08:00

Previously we were compiling core and theme CSS into two targets: Desktop and Mobile. The majority of both files was the 'common' css. This commit splits those common styles into their own targets so that there is less duplication. This should improve compilation times + cache reuse, as well as opening the door for experiments with media-query-based mobile-modes. The only functional change is that we can no longer use `@extend` to copy 'common' rules in core to mobile/desktop. This is probably for the best. Duplication and/or mixins are a more native-css pattern for this. Plugins already have a common / mobile / desktop pattern, so are unchanged by this commit.
1134 lines
38 KiB
Ruby
1134 lines
38 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(/\.desktop/)
|
|
|
|
# child theme CSS is no longer bundled with main theme
|
|
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_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)
|
|
|
|
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
|
|
href = manager.color_scheme_stylesheet_link_tag_href
|
|
expect(href).to include("color_definitions_base")
|
|
end
|
|
|
|
it "does not crash when no default theme is set" do
|
|
SiteSetting.default_theme_id = -1
|
|
href = manager.color_scheme_stylesheet_link_tag_href
|
|
|
|
expect(href).to include("color_definitions_base")
|
|
end
|
|
|
|
it "loads base scheme when defined scheme id is missing" do
|
|
href = manager.color_scheme_stylesheet_link_tag_href(125)
|
|
expect(href).to include("color_definitions_base")
|
|
end
|
|
|
|
it "loads nothing when fallback_to_base is false" do
|
|
href = manager.color_scheme_stylesheet_link_tag_href(125, fallback_to_base: false)
|
|
expect(href).to eq(nil)
|
|
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
|
|
|
|
href = manager.color_scheme_stylesheet_link_tag_href
|
|
expect(href).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)
|
|
|
|
href =
|
|
manager(user_theme.id).color_scheme_stylesheet_link_tag_href(nil, fallback_to_base: true)
|
|
expect(href).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: #CC0000;")
|
|
expect(stylesheet).to include("--primary: #222;") # from base scheme
|
|
end
|
|
|
|
it "uses the correct scheme when a valid scheme id is used" do
|
|
href = manager.color_scheme_stylesheet_link_tag_href(ColorScheme.first.id)
|
|
slug = Slug.for(ColorScheme.first.name) + "_" + ColorScheme.first.id.to_s
|
|
expect(href).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
|
|
|
|
href = manager.color_scheme_stylesheet_link_tag_href
|
|
expect(href).to include("/stylesheets/color_definitions_funky-bunch_#{cs.id}_")
|
|
end
|
|
|
|
it "generates the dark mode of a color scheme when the dark option is specified" do
|
|
scheme = ColorScheme.create_from_base(name: "Neutral", base_scheme_id: "Neutral")
|
|
ColorSchemeRevisor.revise(
|
|
scheme,
|
|
colors: [{ name: "primary", hex: "CABFAF", dark_hex: "FAFCAB" }],
|
|
)
|
|
theme = Fabricate(:theme)
|
|
manager = manager(theme.id)
|
|
|
|
dark_stylesheet =
|
|
Stylesheet::Manager::Builder.new(
|
|
target: :color_definitions,
|
|
theme: theme,
|
|
color_scheme: scheme,
|
|
manager: manager,
|
|
dark: true,
|
|
).compile
|
|
light_stylesheet =
|
|
Stylesheet::Manager::Builder.new(
|
|
target: :color_definitions,
|
|
theme: theme,
|
|
color_scheme: scheme,
|
|
manager: manager,
|
|
).compile
|
|
|
|
expect(light_stylesheet).to include("--primary: #CABFAF;")
|
|
expect(light_stylesheet).to include("color_definitions_neutral_#{scheme.id}_#{theme.id}")
|
|
expect(light_stylesheet).not_to include(
|
|
"color_definitions_neutral_#{scheme.id}_#{theme.id}_dark",
|
|
)
|
|
|
|
expect(dark_stylesheet).to include("--primary: #FAFCAB;")
|
|
expect(dark_stylesheet).to include("color_definitions_neutral_#{scheme.id}_#{theme.id}_dark")
|
|
end
|
|
|
|
it "uses the light colors as fallback if the dark scheme doesn't define them" do
|
|
scheme = ColorScheme.create_from_base(name: "Neutral", base_scheme_id: "Neutral")
|
|
ColorSchemeRevisor.revise(scheme, colors: [{ name: "primary", hex: "BACFAB", dark_hex: nil }])
|
|
theme = Fabricate(:theme)
|
|
manager = manager(theme.id)
|
|
|
|
dark_stylesheet =
|
|
Stylesheet::Manager::Builder.new(
|
|
target: :color_definitions,
|
|
theme: theme,
|
|
color_scheme: scheme,
|
|
manager: manager,
|
|
dark: true,
|
|
).compile
|
|
light_stylesheet =
|
|
Stylesheet::Manager::Builder.new(
|
|
target: :color_definitions,
|
|
theme: theme,
|
|
color_scheme: scheme,
|
|
manager: manager,
|
|
).compile
|
|
|
|
expect(light_stylesheet).to include("--primary: #BACFAB;")
|
|
expect(light_stylesheet).to include("color_definitions_neutral_#{scheme.id}_#{theme.id}")
|
|
expect(light_stylesheet).not_to include(
|
|
"color_definitions_neutral_#{scheme.id}_#{theme.id}_dark",
|
|
)
|
|
|
|
expect(dark_stylesheet).to include("--primary: #BACFAB;")
|
|
expect(dark_stylesheet).to include("color_definitions_neutral_#{scheme.id}_#{theme.id}_dark")
|
|
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: #CC0000;")
|
|
end
|
|
|
|
it "includes updated font definitions" do
|
|
details1 = manager.color_scheme_stylesheet_details(nil, fallback_to_base: true)
|
|
|
|
SiteSetting.base_font = DiscourseFonts.fonts[2][:key]
|
|
|
|
details2 = manager.color_scheme_stylesheet_details(nil, fallback_to_base: true)
|
|
expect(details1[:new_href]).not_to eq(details2[:new_href])
|
|
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 in production 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).to include("--primary:") # core vals preserved
|
|
expect(File.read(stylesheet.source_map_fullpath)).to include("/* SCSS compilation 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: :common_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: "어두운")
|
|
|
|
href = manager.color_scheme_stylesheet_link_tag_href(cs.id)
|
|
expect(href).to include("/stylesheets/color_definitions_grun_#{cs.id}_")
|
|
href2 = manager.color_scheme_stylesheet_link_tag_href(cs2.id)
|
|
expect(href2).to include("/stylesheets/color_definitions_scheme_#{cs2.id}_")
|
|
end
|
|
end
|
|
end
|
|
|
|
describe ".precompile_css" do
|
|
let(:core_targets) do
|
|
%w[common desktop mobile admin wizard common_rtl desktop_rtl mobile_rtl admin_rtl wizard_rtl]
|
|
end
|
|
|
|
let(:theme_targets) { %i[common_theme desktop_theme mobile_theme] }
|
|
|
|
before do
|
|
STDERR.stubs(:write)
|
|
StylesheetCache.destroy_all
|
|
default_theme.set_default!
|
|
end
|
|
|
|
after do
|
|
STDERR.unstub(:write)
|
|
Stylesheet::Manager.rm_cache_folder
|
|
end
|
|
|
|
fab!(:scheme1) { ColorScheme.create!(name: "scheme1") }
|
|
fab!(:scheme2) { ColorScheme.create!(name: "scheme2") }
|
|
|
|
fab!(:user_theme) { Fabricate(:theme, user_selectable: true, color_scheme: scheme1) }
|
|
fab!(:default_theme) { Fabricate(:theme, user_selectable: true, color_scheme: scheme2) }
|
|
fab!(:child_theme) do
|
|
Fabricate(:theme).tap do |t|
|
|
t.component = true
|
|
t.save!
|
|
user_theme.add_relative_theme!(:child, t)
|
|
end
|
|
end
|
|
fab!(:child_theme_with_css) do
|
|
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
|
|
end
|
|
|
|
it "generates precompiled CSS - only core" do
|
|
capture_output(:stderr) { Stylesheet::Manager.precompile_css }
|
|
|
|
expect(StylesheetCache.pluck(:target)).to contain_exactly(*core_targets)
|
|
end
|
|
|
|
it "generates precompiled CSS - only themes" do
|
|
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(1)
|
|
expect(StylesheetCache.count).to eq(33) # (1 theme with css) + 32 color schemes (2 themes * 8 color schemes (7 defaults + 1 theme scheme) * 2 (light and dark mode per scheme))
|
|
end
|
|
|
|
it "generates precompiled CSS - core and themes" do
|
|
Stylesheet::Manager.precompile_css
|
|
Stylesheet::Manager.precompile_theme_css
|
|
|
|
results = StylesheetCache.pluck(:target)
|
|
expect(results.size).to eq(43) # 10 core targets + 1 theme + 32 color schemes (light and dark mode per scheme)
|
|
|
|
expect(results.count { |target| target =~ /^common_theme_/ }).to eq(1)
|
|
end
|
|
|
|
it "correctly generates precompiled CSS - core and themes and no default theme" do
|
|
Theme.clear_default!
|
|
|
|
Stylesheet::Manager.precompile_css
|
|
Stylesheet::Manager.precompile_theme_css
|
|
|
|
results = StylesheetCache.pluck(:target)
|
|
expect(results.size).to eq(43) # 10 core targets + 1 theme + 32 color schemes (light and dark mode per scheme)
|
|
|
|
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)
|
|
|
|
ThemeField.create!(
|
|
theme_id: default_theme.id,
|
|
target_id: Theme.targets[:common],
|
|
name: "logo",
|
|
value: "",
|
|
upload_id: upload.id,
|
|
type_id: ThemeField.types[:theme_upload_var],
|
|
)
|
|
|
|
default_theme.set_field(
|
|
target: :common,
|
|
name: :scss,
|
|
value: "body { background: url($logo); border: 3px solid green; }",
|
|
)
|
|
|
|
default_theme.save!
|
|
|
|
upload.destroy!
|
|
|
|
Stylesheet::Manager.precompile_theme_css
|
|
|
|
manager = manager(default_theme.id)
|
|
theme_builder =
|
|
Stylesheet::Manager::Builder.new(
|
|
target: :common_theme,
|
|
theme: default_theme,
|
|
manager: manager,
|
|
)
|
|
css = File.read(theme_builder.stylesheet_fullpath)
|
|
expect(css).to include("border:3px solid green}")
|
|
end
|
|
|
|
context "when there are enabled plugins" do
|
|
let(:plugin1) do
|
|
plugin1 = plugin_from_fixtures("my_plugin")
|
|
plugin1.register_css "body { padding: 1px 2px 3px 4px; }"
|
|
plugin1
|
|
end
|
|
|
|
let(:plugin2) do
|
|
plugin2 = plugin_from_fixtures("scss_plugin")
|
|
plugin2
|
|
end
|
|
|
|
before do
|
|
Discourse.plugins << plugin1
|
|
Discourse.plugins << plugin2
|
|
plugin1.activate!
|
|
plugin2.activate!
|
|
Stylesheet::Importer.register_imports!
|
|
StylesheetCache.destroy_all
|
|
end
|
|
|
|
after do
|
|
Discourse.plugins.delete(plugin1)
|
|
Discourse.plugins.delete(plugin2)
|
|
Stylesheet::Importer.register_imports!
|
|
DiscoursePluginRegistry.reset!
|
|
end
|
|
|
|
it "generates LTR and RTL CSS for plugins" do
|
|
output = capture_output(:stderr) { Stylesheet::Manager.precompile_css }
|
|
|
|
results = StylesheetCache.pluck(:target)
|
|
expect(results).to contain_exactly(
|
|
*core_targets,
|
|
"my_plugin",
|
|
"my_plugin_rtl",
|
|
"scss_plugin",
|
|
"scss_plugin_rtl",
|
|
)
|
|
|
|
expect(output.scan(/my_plugin$/).length).to eq(1)
|
|
expect(output.scan(/my_plugin_rtl$/).length).to eq(1)
|
|
expect(output.scan(/scss_plugin$/).length).to eq(1)
|
|
expect(output.scan(/scss_plugin_rtl$/).length).to eq(1)
|
|
|
|
plugin1_ltr_css = StylesheetCache.where(target: "my_plugin").pluck(:content).first
|
|
plugin1_rtl_css = StylesheetCache.where(target: "my_plugin_rtl").pluck(:content).first
|
|
|
|
expect(plugin1_ltr_css).to include("body{padding:1px 2px 3px 4px}")
|
|
expect(plugin1_ltr_css).not_to include("body{padding:1px 4px 3px 2px}")
|
|
expect(plugin1_rtl_css).to include("body{padding:1px 4px 3px 2px}")
|
|
expect(plugin1_rtl_css).not_to include("body{padding:1px 2px 3px 4px}")
|
|
|
|
plugin2_ltr_css = StylesheetCache.where(target: "scss_plugin").pluck(:content).first
|
|
plugin2_rtl_css = StylesheetCache.where(target: "scss_plugin_rtl").pluck(:content).first
|
|
|
|
expect(plugin2_ltr_css).to include(".pull-left{float:left}")
|
|
expect(plugin2_ltr_css).not_to include(".pull-left{float:right}")
|
|
expect(plugin2_rtl_css).to include(".pull-left{float:right}")
|
|
expect(plugin2_rtl_css).not_to include(".pull-left{float:left}")
|
|
end
|
|
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]+\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-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
|