mirror of
https://github.com/discourse/discourse.git
synced 2024-11-26 06:43:44 +08:00
8700c5ee6b
Previously the stylesheet cachebusting hash was based on the maximum mtime of files. This works well in development and during in-container updates (e.g. via docker_manager). However, when a fresh docker image is created for each deploy, the file mtimes will change even if the contents has not. This commit changes the production logic to calculate the cachebuster from the filenames and contents of the relevant assets. This should be consistent across deploys, thereby improving cache hits and improving page load times.
917 lines
33 KiB
Ruby
917 lines
33 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'stylesheet/compiler'
|
|
|
|
RSpec.describe Stylesheet::Manager do
|
|
def manager(theme_id = nil)
|
|
Stylesheet::Manager.new(theme_id: theme_id)
|
|
end
|
|
|
|
it 'does not crash for missing theme' do
|
|
Theme.clear_default!
|
|
link = manager.stylesheet_link_tag(:embedded_theme)
|
|
expect(link).to eq("")
|
|
end
|
|
|
|
it "still returns something for no themes" do
|
|
link = manager.stylesheet_link_tag(:desktop, 'all')
|
|
expect(link).not_to eq("")
|
|
end
|
|
|
|
describe "themes with components" do
|
|
let(:child_theme) { Fabricate(:theme, component: true, name: "a component").tap { |c|
|
|
c.set_field(target: :common, name: "scss", value: ".child_common{.scss{color: red;}}")
|
|
c.set_field(target: :desktop, name: "scss", value: ".child_desktop{.scss{color: red;}}")
|
|
c.set_field(target: :mobile, name: "scss", value: ".child_mobile{.scss{color: red;}}")
|
|
c.set_field(target: :common, name: "embedded_scss", value: ".child_embedded{.scss{color: red;}}")
|
|
c.save!
|
|
}}
|
|
|
|
let(:theme) { Fabricate(:theme).tap { |t|
|
|
t.set_field(target: :common, name: "scss", value: ".common{.scss{color: red;}}")
|
|
t.set_field(target: :desktop, name: "scss", value: ".desktop{.scss{color: red;}}")
|
|
t.set_field(target: :mobile, name: "scss", value: ".mobile{.scss{color: red;}}")
|
|
t.set_field(target: :common, name: "embedded_scss", value: ".embedded{.scss{color: red;}}")
|
|
t.save!
|
|
|
|
t.add_relative_theme!(:child, child_theme)
|
|
}}
|
|
|
|
it "generates the right links for non-theme targets" do
|
|
manager = manager(nil)
|
|
|
|
hrefs = manager.stylesheet_details(:desktop, 'all')
|
|
|
|
expect(hrefs.length).to eq(1)
|
|
end
|
|
|
|
it 'can correctly compile theme css' do
|
|
manager = manager(theme.id)
|
|
old_links = manager.stylesheet_link_tag(:desktop_theme, 'all')
|
|
|
|
builder = Stylesheet::Manager::Builder.new(
|
|
target: :desktop_theme, theme: theme, manager: manager
|
|
)
|
|
|
|
builder.compile(force: true)
|
|
|
|
css = File.read(builder.stylesheet_fullpath)
|
|
_source_map = File.read(builder.source_map_fullpath)
|
|
|
|
expect(css).to match(/\.common/)
|
|
expect(css).to match(/\.desktop/)
|
|
|
|
# child theme CSS is no longer bundled with main theme
|
|
expect(css).not_to match(/child_common/)
|
|
expect(css).not_to match(/child_desktop/)
|
|
|
|
child_theme_builder = Stylesheet::Manager::Builder.new(
|
|
target: :desktop_theme, theme: child_theme, manager: manager
|
|
)
|
|
|
|
child_theme_builder.compile(force: true)
|
|
|
|
child_css = File.read(child_theme_builder.stylesheet_fullpath)
|
|
_child_source_map = File.read(child_theme_builder.source_map_fullpath)
|
|
|
|
expect(child_css).to match(/child_common/)
|
|
expect(child_css).to match(/child_desktop/)
|
|
|
|
child_theme.set_field(target: :desktop, name: :scss, value: ".nothing{color: green;}")
|
|
child_theme.save!
|
|
|
|
new_links = manager(theme.id).stylesheet_link_tag(:desktop_theme, 'all')
|
|
|
|
expect(new_links).not_to eq(old_links)
|
|
|
|
# our theme better have a name with the theme_id as part of it
|
|
expect(new_links).to include("/stylesheets/desktop_theme_#{theme.id}_")
|
|
expect(new_links).to include("/stylesheets/desktop_theme_#{child_theme.id}_")
|
|
end
|
|
|
|
it 'can correctly compile embedded theme css' do
|
|
manager = manager(theme.id)
|
|
|
|
builder = Stylesheet::Manager::Builder.new(
|
|
target: :embedded_theme, theme: theme, manager: manager
|
|
)
|
|
|
|
builder.compile(force: true)
|
|
|
|
css = File.read(builder.stylesheet_fullpath)
|
|
expect(css).to match(/\.embedded/)
|
|
expect(css).not_to match(/\.child_embedded/)
|
|
|
|
child_theme_builder = Stylesheet::Manager::Builder.new(
|
|
target: :embedded_theme,
|
|
theme: child_theme,
|
|
manager: manager
|
|
)
|
|
|
|
child_theme_builder.compile(force: true)
|
|
|
|
css = File.read(child_theme_builder.stylesheet_fullpath)
|
|
expect(css).to match(/\.child_embedded/)
|
|
end
|
|
|
|
it 'includes both parent and child theme assets' do
|
|
manager = manager(theme.id)
|
|
|
|
hrefs = manager.stylesheet_details(:desktop_theme, 'all')
|
|
|
|
expect(hrefs.count).to eq(2)
|
|
|
|
expect(hrefs.map { |href| href[:theme_id] }).to contain_exactly(
|
|
theme.id, child_theme.id
|
|
)
|
|
|
|
hrefs = manager.stylesheet_details(:embedded_theme, 'all')
|
|
|
|
expect(hrefs.count).to eq(2)
|
|
|
|
expect(hrefs.map { |href| href[:theme_id] }).to contain_exactly(
|
|
theme.id, child_theme.id
|
|
)
|
|
end
|
|
|
|
it "includes the escaped theme name" do
|
|
manager = manager(theme.id)
|
|
|
|
theme.update(name: "a strange name\"with a quote in it")
|
|
|
|
tag = manager.stylesheet_link_tag(:desktop_theme)
|
|
expect(tag).to have_tag("link", with: {
|
|
"data-theme-name" => theme.name.downcase
|
|
})
|
|
expect(tag).to have_tag("link", with: {
|
|
"data-theme-name" => child_theme.name.downcase
|
|
})
|
|
end
|
|
|
|
it "stylesheet_link_tag calls the preload callback when set" do
|
|
preload_list = []
|
|
preload_callback = ->(href, type) { preload_list << [href, type] }
|
|
|
|
manager = manager(theme.id)
|
|
expect {
|
|
manager.stylesheet_link_tag(:desktop_theme, 'all', preload_callback)
|
|
}.to change(preload_list, :size)
|
|
end
|
|
|
|
context "with stylesheet order" do
|
|
let(:z_child_theme) do
|
|
Fabricate(:theme, component: true, name: "ze component").tap do |z|
|
|
z.set_field(target: :desktop, name: "scss", value: ".child_desktop{.scss{color: red;}}")
|
|
z.save!
|
|
end
|
|
end
|
|
|
|
let(:remote) { RemoteTheme.create!(remote_url: "https://github.com/org/remote-theme1") }
|
|
|
|
let(:child_remote) do
|
|
Fabricate(:theme, remote_theme: remote, component: true).tap do |t|
|
|
t.set_field(target: :desktop, name: "scss", value: ".child_desktop{.scss{color: red;}}")
|
|
t.save!
|
|
end
|
|
end
|
|
|
|
it 'output remote child, then sort children alphabetically, then local parent' do
|
|
theme.add_relative_theme!(:child, z_child_theme)
|
|
theme.add_relative_theme!(:child, child_remote)
|
|
|
|
manager = manager(theme.id)
|
|
hrefs = manager.stylesheet_details(:desktop_theme, 'all')
|
|
|
|
parent = hrefs.select { |href| href[:theme_id] == theme.id }.first
|
|
child_a = hrefs.select { |href| href[:theme_id] == child_theme.id }.first
|
|
child_z = hrefs.select { |href| href[:theme_id] == z_child_theme.id }.first
|
|
child_r = hrefs.select { |href| href[:theme_id] == child_remote.id }.first
|
|
|
|
child_local_A = "<link href=\"#{child_a[:new_href]}\" data-theme-id=\"#{child_a[:theme_id]}\" data-theme-name=\"#{child_a[:theme_name]}\"/>"
|
|
child_local_Z = "<link href=\"#{child_z[:new_href]}\" data-theme-id=\"#{child_z[:theme_id]}\" data-theme-name=\"#{child_z[:theme_name]}\"/>"
|
|
child_remote_R = "<link href=\"#{child_r[:new_href]}\" data-theme-id=\"#{child_r[:theme_id]}\" data-theme-name=\"#{child_r[:theme_name]}\"/>"
|
|
parent_local = "<link href=\"#{parent[:new_href]}\" data-theme-id=\"#{parent[:theme_id]}\" data-theme-name=\"#{parent[:theme_name]}\"/>"
|
|
|
|
link_hrefs = manager.stylesheet_link_tag(:desktop_theme).gsub('media="all" rel="stylesheet" data-target="desktop_theme" ', '')
|
|
|
|
expect(link_hrefs).to eq([child_remote_R, child_local_A, child_local_Z, parent_local].join("\n").html_safe)
|
|
end
|
|
|
|
it "output remote child, remote parent, local child" do
|
|
remote2 = RemoteTheme.create!(remote_url: "https://github.com/org/remote-theme2")
|
|
remote_main_theme = Fabricate(:theme, remote_theme: remote2, name: "remote main").tap do |t|
|
|
t.set_field(target: :desktop, name: "scss", value: ".el{color: red;}")
|
|
t.save!
|
|
end
|
|
|
|
remote_main_theme.add_relative_theme!(:child, z_child_theme)
|
|
remote_main_theme.add_relative_theme!(:child, child_remote)
|
|
|
|
manager = manager(remote_main_theme.id)
|
|
hrefs = manager.stylesheet_details(:desktop_theme, 'all')
|
|
|
|
parent_r = hrefs.select { |href| href[:theme_id] == remote_main_theme.id }.first
|
|
child_z = hrefs.select { |href| href[:theme_id] == z_child_theme.id }.first
|
|
child_r = hrefs.select { |href| href[:theme_id] == child_remote.id }.first
|
|
|
|
parent_remote = "<link href=\"#{parent_r[:new_href]}\" data-theme-id=\"#{parent_r[:theme_id]}\" data-theme-name=\"#{parent_r[:theme_name]}\"/>"
|
|
child_local = "<link href=\"#{child_z[:new_href]}\" data-theme-id=\"#{child_z[:theme_id]}\" data-theme-name=\"#{child_z[:theme_name]}\"/>"
|
|
child_remote = "<link href=\"#{child_r[:new_href]}\" data-theme-id=\"#{child_r[:theme_id]}\" data-theme-name=\"#{child_r[:theme_name]}\"/>"
|
|
|
|
link_hrefs = manager.stylesheet_link_tag(:desktop_theme).gsub('media="all" rel="stylesheet" data-target="desktop_theme" ', '')
|
|
expect(link_hrefs).to eq([child_remote, parent_remote, child_local].join("\n").html_safe)
|
|
end
|
|
end
|
|
|
|
it 'outputs tags for non-theme targets for theme component' do
|
|
child_theme = Fabricate(:theme, component: true)
|
|
|
|
hrefs = manager(child_theme.id).stylesheet_details(:desktop, 'all')
|
|
|
|
expect(hrefs.count).to eq(1) # desktop
|
|
end
|
|
|
|
it 'does not output tags for component targets with no styles' do
|
|
embedded_scss_child = Fabricate(:theme, component: true)
|
|
embedded_scss_child.set_field(target: :common, name: "embedded_scss", value: ".scss{color: red;}")
|
|
embedded_scss_child.save!
|
|
|
|
theme.add_relative_theme!(:child, embedded_scss_child)
|
|
|
|
manager = manager(theme.id)
|
|
|
|
hrefs = manager.stylesheet_details(:desktop_theme, 'all')
|
|
expect(hrefs.count).to eq(2) # theme + child_theme
|
|
|
|
hrefs = manager.stylesheet_details(:embedded_theme, 'all')
|
|
expect(hrefs.count).to eq(3) # theme + child_theme + embedded_scss_child
|
|
end
|
|
|
|
it '.stylesheet_details can find components mobile SCSS when target is `:mobile_theme`' do
|
|
child_with_mobile_scss = Fabricate(:theme, component: true)
|
|
child_with_mobile_scss.set_field(target: :mobile, name: :scss, value: "body { color: red; }")
|
|
child_with_mobile_scss.save!
|
|
theme.add_relative_theme!(:child, child_with_mobile_scss)
|
|
|
|
manager = manager(theme.id)
|
|
hrefs = manager.stylesheet_details(:mobile_theme, 'all')
|
|
|
|
expect(hrefs.count).to eq(3)
|
|
expect(hrefs.find { |h| h[:theme_id] == child_with_mobile_scss.id }).to be_present
|
|
end
|
|
|
|
it 'does not output multiple assets for non-theme targets' do
|
|
manager = manager()
|
|
|
|
hrefs = manager.stylesheet_details(:admin, 'all')
|
|
expect(hrefs.count).to eq(1)
|
|
|
|
hrefs = manager.stylesheet_details(:mobile, 'all')
|
|
expect(hrefs.count).to eq(1)
|
|
end
|
|
end
|
|
|
|
describe 'digest' do
|
|
after do
|
|
DiscoursePluginRegistry.reset!
|
|
end
|
|
|
|
it 'can correctly account for plugins in default digest' do
|
|
builder = Stylesheet::Manager::Builder.new(target: :desktop, manager: manager)
|
|
digest1 = builder.digest
|
|
|
|
DiscoursePluginRegistry.stylesheets["fake"] = Set.new(["fake_file"])
|
|
builder = Stylesheet::Manager::Builder.new(target: :desktop, manager: manager)
|
|
digest2 = builder.digest
|
|
|
|
expect(digest1).not_to eq(digest2)
|
|
end
|
|
|
|
it "can correctly account for settings in theme's components" do
|
|
theme = Fabricate(:theme)
|
|
child = Fabricate(:theme, component: true)
|
|
theme.add_relative_theme!(:child, child)
|
|
|
|
child.set_field(target: :settings, name: :yaml, value: "childcolor: red")
|
|
child.set_field(target: :common, name: :scss, value: "body {background-color: $childcolor}")
|
|
child.save!
|
|
|
|
manager = manager(theme.id)
|
|
|
|
builder = Stylesheet::Manager::Builder.new(
|
|
target: :desktop_theme, theme: theme, manager: manager
|
|
)
|
|
|
|
digest1 = builder.digest
|
|
|
|
child.update_setting(:childcolor, "green")
|
|
|
|
manager = manager(theme.id)
|
|
|
|
builder = Stylesheet::Manager::Builder.new(
|
|
target: :desktop_theme, theme: theme, manager: manager
|
|
)
|
|
|
|
digest2 = builder.digest
|
|
|
|
expect(digest1).not_to eq(digest2)
|
|
end
|
|
|
|
let(:image) { file_from_fixtures("logo.png") }
|
|
let(:image2) { file_from_fixtures("logo-dev.png") }
|
|
|
|
it 'can correctly account for theme uploads in digest' do
|
|
theme = Fabricate(:theme)
|
|
|
|
upload = UploadCreator.new(image, "logo.png").create_for(-1)
|
|
field = ThemeField.create!(
|
|
theme_id: theme.id,
|
|
target_id: Theme.targets[:common],
|
|
name: "logo",
|
|
value: "",
|
|
upload_id: upload.id,
|
|
type_id: ThemeField.types[:theme_upload_var]
|
|
)
|
|
|
|
manager = manager(theme.id)
|
|
|
|
builder = Stylesheet::Manager::Builder.new(
|
|
target: :desktop_theme, theme: theme, manager: manager
|
|
)
|
|
|
|
digest1 = builder.digest
|
|
field.destroy!
|
|
|
|
upload = UploadCreator.new(image2, "logo.png").create_for(-1)
|
|
field = ThemeField.create!(
|
|
theme_id: theme.id,
|
|
target_id: Theme.targets[:common],
|
|
name: "logo",
|
|
value: "",
|
|
upload_id: upload.id,
|
|
type_id: ThemeField.types[:theme_upload_var]
|
|
)
|
|
|
|
builder = Stylesheet::Manager::Builder.new(
|
|
target: :desktop_theme, theme: theme.reload, manager: manager
|
|
)
|
|
|
|
digest2 = builder.digest
|
|
|
|
expect(digest1).not_to eq(digest2)
|
|
end
|
|
|
|
it 'can generate digest with a missing upload record' do
|
|
theme = Fabricate(:theme)
|
|
|
|
upload = UploadCreator.new(image, "logo.png").create_for(-1)
|
|
field = ThemeField.create!(
|
|
theme_id: theme.id,
|
|
target_id: Theme.targets[:common],
|
|
name: "logo",
|
|
value: "",
|
|
upload_id: upload.id,
|
|
type_id: ThemeField.types[:theme_upload_var]
|
|
)
|
|
|
|
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) { Fabricate(:theme).tap { |t|
|
|
t.set_field(target: :common, name: "color_definitions", value: ':root {--special: rebeccapurple;}')
|
|
t.save!
|
|
}}
|
|
let(:scss_child) { ':root {--child-definition: #{dark-light-choose(#c00, #fff)};}' }
|
|
let(:child) { Fabricate(:theme, component: true, name: "Child Theme").tap { |t|
|
|
t.set_field(target: :common, name: "color_definitions", value: scss_child)
|
|
t.save!
|
|
}}
|
|
|
|
let(:scheme) { ColorScheme.base }
|
|
let(:dark_scheme) { ColorScheme.create_from_base(name: 'Dark', base_scheme_id: 'Dark') }
|
|
|
|
it "includes theme color definitions in color scheme" do
|
|
manager = manager(theme.id)
|
|
|
|
stylesheet = Stylesheet::Manager::Builder.new(
|
|
target: :color_definitions, theme: theme, color_scheme: scheme, manager: manager
|
|
).compile(force: true)
|
|
|
|
expect(stylesheet).to include("--special: rebeccapurple")
|
|
end
|
|
|
|
it "includes child color definitions in color schemes" do
|
|
theme.add_relative_theme!(:child, child)
|
|
theme.save!
|
|
manager = manager(theme.id)
|
|
|
|
stylesheet = Stylesheet::Manager::Builder.new(
|
|
target: :color_definitions, theme: theme, color_scheme: scheme, manager: manager
|
|
).compile(force: true)
|
|
|
|
expect(stylesheet).to include("--special: rebeccapurple")
|
|
expect(stylesheet).to include("--child-definition: #c00")
|
|
end
|
|
|
|
it "respects selected color scheme in child color definitions" do
|
|
theme.add_relative_theme!(:child, child)
|
|
theme.save!
|
|
|
|
manager = manager(theme.id)
|
|
|
|
stylesheet = Stylesheet::Manager::Builder.new(
|
|
target: :color_definitions, theme: theme, color_scheme: dark_scheme, manager: manager
|
|
).compile(force: true)
|
|
|
|
expect(stylesheet).to include("--special: rebeccapurple")
|
|
expect(stylesheet).to include("--child-definition: #fff")
|
|
end
|
|
|
|
it "fails gracefully for broken SCSS" do
|
|
scss = "$test: $missing-var;"
|
|
theme.set_field(target: :common, name: "color_definitions", value: scss)
|
|
theme.save!
|
|
|
|
manager = manager(theme.id)
|
|
|
|
stylesheet = Stylesheet::Manager::Builder.new(
|
|
target: :color_definitions, theme: theme, color_scheme: scheme, manager: manager
|
|
)
|
|
|
|
expect { stylesheet.compile }.not_to raise_error
|
|
end
|
|
|
|
it "child theme SCSS includes the default theme's color scheme variables" do
|
|
SiteSetting.default_theme_id = theme.id
|
|
custom_scheme = ColorScheme.create_from_base(name: "Neutral", base_scheme_id: "Neutral")
|
|
ColorSchemeRevisor.revise(custom_scheme, colors: [{ name: "primary", hex: "CC0000" }])
|
|
theme.color_scheme_id = custom_scheme.id
|
|
theme.save!
|
|
|
|
scss = "body{ border: 2px solid $primary;}"
|
|
child.set_field(target: :common, name: "scss", value: scss)
|
|
child.save!
|
|
|
|
manager = manager(theme.id)
|
|
|
|
child_theme_manager = Stylesheet::Manager::Builder.new(
|
|
target: :desktop_theme, theme: child, manager: manager
|
|
)
|
|
|
|
child_theme_manager.compile(force: true)
|
|
|
|
child_css = File.read(child_theme_manager.stylesheet_fullpath)
|
|
expect(child_css).to include("body{border:2px solid #c00}")
|
|
end
|
|
end
|
|
|
|
context 'with encoded slugs' do
|
|
before { SiteSetting.slug_generation_method = 'encoded' }
|
|
after { SiteSetting.slug_generation_method = 'ascii' }
|
|
|
|
it "strips unicode in color scheme stylesheet filenames" do
|
|
cs = Fabricate(:color_scheme, name: 'Grün')
|
|
cs2 = Fabricate(:color_scheme, name: '어두운')
|
|
|
|
link = manager.color_scheme_stylesheet_link_tag(cs.id)
|
|
expect(link).to include("/stylesheets/color_definitions_grun_#{cs.id}_")
|
|
link2 = manager.color_scheme_stylesheet_link_tag(cs2.id)
|
|
expect(link2).to include("/stylesheets/color_definitions_scheme_#{cs2.id}_")
|
|
end
|
|
end
|
|
end
|
|
|
|
describe ".precompile css" do
|
|
before do
|
|
class << STDERR
|
|
alias_method :orig_write, :write
|
|
def write(x)
|
|
end
|
|
end
|
|
end
|
|
|
|
after do
|
|
class << STDERR
|
|
def write(x)
|
|
orig_write(x)
|
|
end
|
|
end
|
|
FileUtils.rm_rf("tmp/stylesheet-cache")
|
|
end
|
|
|
|
it "correctly generates precompiled CSS" do
|
|
scheme1 = ColorScheme.create!(name: "scheme1")
|
|
scheme2 = ColorScheme.create!(name: "scheme2")
|
|
core_targets = [:desktop, :mobile, :desktop_rtl, :mobile_rtl, :admin, :wizard]
|
|
theme_targets = [:desktop_theme, :mobile_theme]
|
|
|
|
Theme.update_all(user_selectable: false)
|
|
user_theme = Fabricate(:theme, user_selectable: true, color_scheme: scheme1)
|
|
default_theme = Fabricate(:theme, user_selectable: true, color_scheme: scheme2)
|
|
|
|
child_theme = Fabricate(:theme).tap do |t|
|
|
t.component = true
|
|
t.save!
|
|
user_theme.add_relative_theme!(:child, t)
|
|
end
|
|
|
|
child_theme_with_css = Fabricate(:theme).tap do |t|
|
|
t.component = true
|
|
|
|
t.set_field(
|
|
target: :common,
|
|
name: :scss,
|
|
value: "body { background: green }"
|
|
)
|
|
|
|
t.save!
|
|
|
|
user_theme.add_relative_theme!(:child, t)
|
|
default_theme.add_relative_theme!(:child, t)
|
|
end
|
|
|
|
default_theme.set_default!
|
|
|
|
StylesheetCache.destroy_all
|
|
|
|
# only core
|
|
output = capture_output(:stderr) do
|
|
Stylesheet::Manager.precompile_css
|
|
end
|
|
|
|
results = StylesheetCache.pluck(:target)
|
|
expect(results.size).to eq(core_targets.size)
|
|
|
|
StylesheetCache.destroy_all
|
|
|
|
# only themes
|
|
output = capture_output(:stderr) do
|
|
Stylesheet::Manager.precompile_theme_css
|
|
end
|
|
|
|
# Ensure we force compile each theme only once
|
|
expect(output.scan(/#{child_theme_with_css.name}/).length).to eq(2)
|
|
results = StylesheetCache.pluck(:target)
|
|
expect(results.size).to eq(22) # (3 themes * 2 targets) + 16 color schemes (2 themes * 8 color schemes (7 defaults + 1 theme scheme))
|
|
|
|
# themes + core
|
|
Stylesheet::Manager.precompile_css
|
|
results = StylesheetCache.pluck(:target)
|
|
expect(results.size).to eq(28) # 9 core targets + 9 theme + 10 color schemes
|
|
|
|
theme_targets.each do |tar|
|
|
expect(results.count { |target| target =~ /^#{tar}_(#{user_theme.id}|#{default_theme.id})$/ }).to eq(2)
|
|
end
|
|
|
|
Theme.clear_default!
|
|
StylesheetCache.destroy_all
|
|
|
|
# themes + core with no theme set as default
|
|
Stylesheet::Manager.precompile_css
|
|
Stylesheet::Manager.precompile_theme_css
|
|
results = StylesheetCache.pluck(:target)
|
|
expect(results.size).to eq(28) # 9 core targets + 9 theme + 10 color schemes
|
|
|
|
expect(results).to include("color_definitions_#{scheme1.name}_#{scheme1.id}_#{user_theme.id}")
|
|
expect(results).to include("color_definitions_#{scheme2.name}_#{scheme2.id}_#{default_theme.id}")
|
|
|
|
# Check that sourceMappingURL includes __ws parameter
|
|
content = StylesheetCache.last.content
|
|
expect(content).to match(/# sourceMappingURL=[^\/]+\.css\.map\?__ws=test\.localhost/)
|
|
end
|
|
end
|
|
|
|
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 do
|
|
Stylesheet::Manager.stubs(:use_file_hash_for_cachebuster?).returns(true)
|
|
end
|
|
|
|
after do
|
|
path = Stylesheet::Manager.send(:manifest_full_path)
|
|
File.delete(path) if File.exists?(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
|