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