mirror of
https://github.com/discourse/discourse.git
synced 2024-12-05 21:33:55 +08:00
c2fcd55a80
Prior to this commit, we didn't have RTL versions of our admin and plugins CSS bundles and we always served LTR versions of those bundles even when users used an RTL locale, causing admin and plugins UI elements to never look as good as when an LTR locale was used. Example of UI issues prior to this commit were: missing margins, borders on the wrong side and buttons too close to each other etc. This commit creates an RTL version for the admin CSS bundle as well as RTL bundles for all the installed plugins and serves those RTL bundles to users/sites who use RTL locales.
1120 lines
37 KiB
Ruby
1120 lines
37 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: #CC0000;")
|
|
expect(stylesheet).to include("--primary: #222222;") # 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: #CC0000;")
|
|
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
|
|
let(:core_targets) do
|
|
%w[desktop mobile admin wizard desktop_rtl mobile_rtl admin_rtl wizard_rtl]
|
|
end
|
|
|
|
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")
|
|
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).to contain_exactly(*core_targets)
|
|
|
|
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(30) # 11 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(30) # 11 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")
|
|
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
|
|
|
|
context "when there are enabled plugins" do
|
|
let(:plugin1) do
|
|
plugin1 = Plugin::Instance.new
|
|
plugin1.path = "#{Rails.root}/spec/fixtures/plugins/my_plugin/plugin.rb"
|
|
plugin1.register_css "body { padding: 1px 2px 3px 4px; }"
|
|
plugin1
|
|
end
|
|
|
|
let(:plugin2) do
|
|
plugin2 = Plugin::Instance.new
|
|
plugin2.path = "#{Rails.root}/spec/fixtures/plugins/scss_plugin/plugin.rb"
|
|
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]+:[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
|