mirror of
https://github.com/discourse/discourse.git
synced 2025-02-25 18:55:32 -06:00
FEATURE: Native theme support
This feature introduces the concept of themes. Themes are an evolution
of site customizations.
Themes introduce two very big conceptual changes:
- A theme may include other "child themes", children can include grand
children and so on.
- A theme may specify a color scheme
The change does away with the idea of "enabled" color schemes.
It also adds a bunch of big niceties like
- You can source a theme from a git repo
- History for themes is much improved
- You can only have a single enabled theme. Themes can be selected by
users, if you opt for it.
On a technical level this change comes with a whole bunch of goodies
- All CSS is now compiled using a custom pipeline that uses libsass
see /lib/stylesheet
- There is a single pipeline for css compilation (in the past we used
one for customizations and another one for the rest of the app
- The stylesheet pipeline is now divorced of sprockets, there is no
reliance on sprockets for CSS bundling
- CSS is generated with source maps everywhere (including themes) this
makes debugging much easier
- Our "live reloader" is smarter and avoid a flash of unstyled content
we run a file watcher in "puma" in dev so you no longer need to run
rake autospec to watch for CSS changes
This commit is contained in:
@@ -1,30 +0,0 @@
|
||||
require 'rails_helper'
|
||||
require_dependency 'sass/discourse_sass_compiler'
|
||||
|
||||
describe DiscourseSassCompiler do
|
||||
|
||||
let(:test_scss) { "body { p {color: blue;} }\n@import 'common/foundation/variables';\n@import 'plugins';" }
|
||||
|
||||
describe '#compile' do
|
||||
it "compiles scss" do
|
||||
DiscoursePluginRegistry.stubs(:stylesheets).returns(["#{Rails.root}/spec/fixtures/scss/my_plugin.scss"])
|
||||
css = described_class.compile(test_scss, "test")
|
||||
expect(css).to include("color")
|
||||
expect(css).to include('my-plugin-thing')
|
||||
end
|
||||
|
||||
it "raises error for invalid scss" do
|
||||
expect {
|
||||
described_class.compile("this isn't valid scss", "test")
|
||||
}.to raise_error(Sass::SyntaxError)
|
||||
end
|
||||
|
||||
it "doesn't load theme or plugins in safe mode" do
|
||||
ColorScheme.expects(:enabled).never
|
||||
DiscoursePluginRegistry.stubs(:stylesheets).returns(["#{Rails.root}/spec/fixtures/scss/my_plugin.scss"])
|
||||
css = described_class.compile(test_scss, "test", safe: true)
|
||||
expect(css).not_to include('my-plugin-thing')
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
@@ -1,46 +0,0 @@
|
||||
require 'rails_helper'
|
||||
require_dependency 'sass/discourse_stylesheets'
|
||||
|
||||
describe DiscourseStylesheets do
|
||||
|
||||
describe "compile" do
|
||||
it "can compile desktop bundle" do
|
||||
DiscoursePluginRegistry.stubs(:stylesheets).returns(["#{Rails.root}/spec/fixtures/scss/my_plugin.scss"])
|
||||
builder = described_class.new(:desktop)
|
||||
expect(builder.compile(force: true)).to include('my-plugin-thing')
|
||||
FileUtils.rm builder.stylesheet_fullpath
|
||||
end
|
||||
|
||||
it "can compile mobile bundle" do
|
||||
DiscoursePluginRegistry.stubs(:mobile_stylesheets).returns(["#{Rails.root}/spec/fixtures/scss/my_plugin.scss"])
|
||||
builder = described_class.new(:mobile)
|
||||
expect(builder.compile(force: true)).to include('my-plugin-thing')
|
||||
FileUtils.rm builder.stylesheet_fullpath
|
||||
end
|
||||
|
||||
it "can fallback when css is bad" do
|
||||
DiscoursePluginRegistry.stubs(:stylesheets).returns([
|
||||
"#{Rails.root}/spec/fixtures/scss/my_plugin.scss",
|
||||
"#{Rails.root}/spec/fixtures/scss/broken.scss"
|
||||
])
|
||||
builder = described_class.new(:desktop)
|
||||
expect(builder.compile(force: true)).not_to include('my-plugin-thing')
|
||||
FileUtils.rm builder.stylesheet_fullpath
|
||||
end
|
||||
end
|
||||
|
||||
describe "#digest" do
|
||||
before do
|
||||
described_class.expects(:max_file_mtime).returns(Time.new(2016, 06, 05, 12, 30, 0, 0))
|
||||
end
|
||||
|
||||
it "should return a digest" do
|
||||
expect(described_class.new.digest).to eq('0e6c2e957cfc92ed60661c90ec3345198ccef887')
|
||||
end
|
||||
|
||||
it "should include the cdn url when generating the digest" do
|
||||
GlobalSetting.expects(:cdn_url).returns('https://fastly.maxcdn.org')
|
||||
expect(described_class.new.digest).to eq('4995163b1232c54c8ed3b44200d803a90bc47613')
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -151,27 +151,31 @@ describe Wizard::StepUpdater do
|
||||
let!(:color_scheme) { Fabricate(:color_scheme, name: 'existing', via_wizard: true) }
|
||||
|
||||
it "updates the scheme" do
|
||||
updater = wizard.create_updater('colors', theme_id: 'dark')
|
||||
updater = wizard.create_updater('colors', base_scheme_id: 'dark')
|
||||
updater.update
|
||||
expect(updater.success?).to eq(true)
|
||||
expect(wizard.completed_steps?('colors')).to eq(true)
|
||||
|
||||
color_scheme.reload
|
||||
expect(color_scheme).to be_enabled
|
||||
|
||||
theme = Theme.find_by(key: SiteSetting.default_theme_key)
|
||||
expect(theme.color_scheme_id).to eq(color_scheme.id)
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
context "without an existing scheme" do
|
||||
it "creates the scheme" do
|
||||
updater = wizard.create_updater('colors', theme_id: 'dark')
|
||||
updater = wizard.create_updater('colors', base_scheme_id: 'dark')
|
||||
updater.update
|
||||
expect(updater.success?).to eq(true)
|
||||
expect(wizard.completed_steps?('colors')).to eq(true)
|
||||
|
||||
color_scheme = ColorScheme.where(via_wizard: true).first
|
||||
expect(color_scheme).to be_present
|
||||
expect(color_scheme).to be_enabled
|
||||
expect(color_scheme.colors).to be_present
|
||||
|
||||
theme = Theme.find_by(key: SiteSetting.default_theme_key)
|
||||
expect(theme.color_scheme_id).to eq(color_scheme.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
21
spec/components/stylesheet/compiler_spec.rb
Normal file
21
spec/components/stylesheet/compiler_spec.rb
Normal file
@@ -0,0 +1,21 @@
|
||||
require 'rails_helper'
|
||||
require 'stylesheet/compiler'
|
||||
|
||||
describe Stylesheet::Compiler do
|
||||
it "can compile desktop mobile and desktop css" do
|
||||
css,_map = Stylesheet::Compiler.compile_asset("desktop")
|
||||
expect(css.length).to be > 1000
|
||||
|
||||
css,_map = Stylesheet::Compiler.compile_asset("mobile")
|
||||
expect(css.length).to be > 1000
|
||||
end
|
||||
|
||||
it "supports asset-url" do
|
||||
css,_map = Stylesheet::Compiler.compile(".body{background-image: asset-url('foo.png');}","test.scss")
|
||||
|
||||
expect(css).to include("url('/foo.png')")
|
||||
expect(css).not_to include('asset-url')
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
57
spec/components/stylesheet/manager_spec.rb
Normal file
57
spec/components/stylesheet/manager_spec.rb
Normal file
@@ -0,0 +1,57 @@
|
||||
require 'rails_helper'
|
||||
require 'stylesheet/compiler'
|
||||
|
||||
describe Stylesheet::Manager do
|
||||
it 'can correctly compile theme css' do
|
||||
theme = Theme.new(
|
||||
name: 'parent',
|
||||
user_id: -1
|
||||
)
|
||||
|
||||
theme.set_field(:common, "scss", ".common{.scss{color: red;}}")
|
||||
theme.set_field(:desktop, "scss", ".desktop{.scss{color: red;}}")
|
||||
theme.set_field(:mobile, "scss", ".mobile{.scss{color: red;}}")
|
||||
theme.set_field(:common, "embedded_scss", ".embedded{.scss{color: red;}}")
|
||||
|
||||
theme.save!
|
||||
|
||||
|
||||
child_theme = Theme.new(
|
||||
name: 'parent',
|
||||
user_id: -1,
|
||||
)
|
||||
|
||||
child_theme.set_field(:common, "scss", ".child_common{.scss{color: red;}}")
|
||||
child_theme.set_field(:desktop, "scss", ".child_desktop{.scss{color: red;}}")
|
||||
child_theme.set_field(:mobile, "scss", ".child_mobile{.scss{color: red;}}")
|
||||
child_theme.set_field(:common, "embedded_scss", ".child_embedded{.scss{color: red;}}")
|
||||
child_theme.save!
|
||||
|
||||
theme.add_child_theme!(child_theme)
|
||||
|
||||
old_link = Stylesheet::Manager.stylesheet_link_tag(:desktop_theme, 'all', theme.key)
|
||||
|
||||
manager = Stylesheet::Manager.new(:desktop_theme, theme.key)
|
||||
manager.compile(force: true)
|
||||
|
||||
css = File.read(manager.stylesheet_fullpath)
|
||||
_source_map = File.read(manager.source_map_fullpath)
|
||||
|
||||
expect(css).to match(/child_common/)
|
||||
expect(css).to match(/child_desktop/)
|
||||
expect(css).to match(/\.common/)
|
||||
expect(css).to match(/\.desktop/)
|
||||
|
||||
|
||||
child_theme.set_field(:desktop, :scss, ".nothing{color: green;}")
|
||||
child_theme.save!
|
||||
|
||||
new_link = Stylesheet::Manager.stylesheet_link_tag(:desktop_theme, 'all', theme.key)
|
||||
|
||||
expect(new_link).not_to eq(old_link)
|
||||
|
||||
# our theme better have a name with the theme_id as part of it
|
||||
expect(new_link).to include("/stylesheets/desktop_theme_#{theme.id}_")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -9,7 +9,6 @@ describe Admin::ColorSchemesController do
|
||||
let!(:user) { log_in(:admin) }
|
||||
let(:valid_params) { { color_scheme: {
|
||||
name: 'Such Design',
|
||||
enabled: true,
|
||||
colors: [
|
||||
{name: 'primary', hex: 'FFBB00'},
|
||||
{name: 'secondary', hex: '888888'}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe Admin::SiteCustomizationsController do
|
||||
|
||||
it "is a subclass of AdminController" do
|
||||
expect(Admin::UsersController < Admin::AdminController).to eq(true)
|
||||
end
|
||||
|
||||
context 'while logged in as an admin' do
|
||||
before do
|
||||
@user = log_in(:admin)
|
||||
end
|
||||
|
||||
context ' .index' do
|
||||
it 'returns success' do
|
||||
SiteCustomization.create!(name: 'my name', user_id: Fabricate(:user).id, header: "my awesome header", stylesheet: "my awesome css")
|
||||
xhr :get, :index
|
||||
expect(response).to be_success
|
||||
end
|
||||
|
||||
it 'returns JSON' do
|
||||
xhr :get, :index
|
||||
expect(::JSON.parse(response.body)).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
context ' .create' do
|
||||
it 'returns success' do
|
||||
xhr :post, :create, site_customization: {name: 'my test name'}
|
||||
expect(response).to be_success
|
||||
end
|
||||
|
||||
it 'returns json' do
|
||||
xhr :post, :create, site_customization: {name: 'my test name'}
|
||||
expect(::JSON.parse(response.body)).to be_present
|
||||
end
|
||||
|
||||
it 'logs the change' do
|
||||
StaffActionLogger.any_instance.expects(:log_site_customization_change).once
|
||||
xhr :post, :create, site_customization: {name: 'my test name'}
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
|
||||
end
|
||||
@@ -8,15 +8,35 @@ describe Admin::StaffActionLogsController do
|
||||
let!(:user) { log_in(:admin) }
|
||||
|
||||
context '.index' do
|
||||
before do
|
||||
|
||||
it 'works' do
|
||||
xhr :get, :index
|
||||
expect(response).to be_success
|
||||
expect(::JSON.parse(response.body)).to be_a(Array)
|
||||
end
|
||||
end
|
||||
|
||||
subject { response }
|
||||
it { is_expected.to be_success }
|
||||
context '.diff' do
|
||||
it 'can generate diffs for theme changes' do
|
||||
theme = Theme.new(user_id: -1, name: 'bob')
|
||||
theme.set_field(:mobile, :scss, 'body {.up}')
|
||||
theme.set_field(:common, :scss, 'omit-dupe')
|
||||
|
||||
it 'returns JSON' do
|
||||
expect(::JSON.parse(subject.body)).to be_a(Array)
|
||||
original_json = ThemeSerializer.new(theme, root: false).to_json
|
||||
|
||||
theme.set_field(:mobile, :scss, 'body {.down}')
|
||||
|
||||
record = StaffActionLogger.new(Discourse.system_user)
|
||||
.log_theme_change(original_json, theme)
|
||||
|
||||
xhr :get, :diff, id: record.id
|
||||
expect(response).to be_success
|
||||
|
||||
parsed = JSON.parse(response.body)
|
||||
expect(parsed["side_by_side"]).to include("up")
|
||||
expect(parsed["side_by_side"]).to include("down")
|
||||
|
||||
expect(parsed["side_by_side"]).not_to include("omit-dupe")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
101
spec/controllers/admin/themes_controller_spec.rb
Normal file
101
spec/controllers/admin/themes_controller_spec.rb
Normal file
@@ -0,0 +1,101 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe Admin::ThemesController do
|
||||
|
||||
it "is a subclass of AdminController" do
|
||||
expect(Admin::UsersController < Admin::AdminController).to eq(true)
|
||||
end
|
||||
|
||||
context 'while logged in as an admin' do
|
||||
before do
|
||||
@user = log_in(:admin)
|
||||
end
|
||||
|
||||
context ' .index' do
|
||||
it 'returns success' do
|
||||
theme = Theme.new(name: 'my name', user_id: -1)
|
||||
theme.set_field(:common, :scss, '.body{color: black;}')
|
||||
theme.set_field(:desktop, :after_header, '<b>test</b>')
|
||||
|
||||
theme.remote_theme = RemoteTheme.new(
|
||||
remote_url: 'awesome.git',
|
||||
remote_version: '7',
|
||||
local_version: '8',
|
||||
remote_updated_at: Time.zone.now
|
||||
)
|
||||
|
||||
theme.save!
|
||||
|
||||
# this will get serialized as well
|
||||
ColorScheme.create_from_base(name: "test", colors: [])
|
||||
|
||||
xhr :get, :index
|
||||
|
||||
expect(response).to be_success
|
||||
|
||||
json = ::JSON.parse(response.body)
|
||||
|
||||
expect(json["extras"]["color_schemes"].length).to eq(2)
|
||||
theme_json = json["themes"].find{|t| t["id"] == theme.id}
|
||||
expect(theme_json["theme_fields"].length).to eq(2)
|
||||
expect(theme_json["remote_theme"]["remote_version"]).to eq("7")
|
||||
end
|
||||
end
|
||||
|
||||
context ' .create' do
|
||||
it 'creates a theme' do
|
||||
xhr :post, :create, theme: {name: 'my test name', theme_fields: [name: 'scss', target: 'common', value: 'body{color: red;}']}
|
||||
expect(response).to be_success
|
||||
|
||||
json = ::JSON.parse(response.body)
|
||||
|
||||
expect(json["theme"]["theme_fields"].length).to eq(1)
|
||||
expect(UserHistory.where(action: UserHistory.actions[:change_theme]).count).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
context ' .update' do
|
||||
it 'can change default theme' do
|
||||
theme = Theme.create(name: 'my name', user_id: -1)
|
||||
xhr :put, :update, id: theme.id, theme: { default: true }
|
||||
expect(SiteSetting.default_theme_key).to eq(theme.key)
|
||||
end
|
||||
|
||||
it 'can unset default theme' do
|
||||
theme = Theme.create(name: 'my name', user_id: -1)
|
||||
SiteSetting.default_theme_key = theme.key
|
||||
xhr :put, :update, id: theme.id, theme: { default: false}
|
||||
expect(SiteSetting.default_theme_key).to be_blank
|
||||
end
|
||||
|
||||
it 'updates a theme' do
|
||||
|
||||
theme = Theme.new(name: 'my name', user_id: -1)
|
||||
theme.set_field(:common, :scss, '.body{color: black;}')
|
||||
theme.save
|
||||
|
||||
child_theme = Theme.create(name: 'my name', user_id: -1)
|
||||
|
||||
xhr :put, :update, id: theme.id,
|
||||
theme: {
|
||||
child_theme_ids: [child_theme.id],
|
||||
name: 'my test name',
|
||||
theme_fields: [name: 'scss', target: 'common', value: 'body{color: red;}']
|
||||
}
|
||||
expect(response).to be_success
|
||||
|
||||
json = ::JSON.parse(response.body)
|
||||
|
||||
fields = json["theme"]["theme_fields"]
|
||||
|
||||
expect(fields.length).to eq(1)
|
||||
expect(fields.first["value"]).to eq('body{color: red;}')
|
||||
|
||||
expect(json["theme"]["child_themes"].length).to eq(1)
|
||||
|
||||
expect(UserHistory.where(action: UserHistory.actions[:change_theme]).count).to eq(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
@@ -1,45 +0,0 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe SiteCustomizationsController do
|
||||
|
||||
before do
|
||||
SiteCustomization.clear_cache!
|
||||
end
|
||||
|
||||
it 'can deliver enabled css' do
|
||||
SiteCustomization.create!(name: '1',
|
||||
user_id: -1,
|
||||
enabled: true,
|
||||
mobile_stylesheet: '.a1{margin: 1px;}',
|
||||
stylesheet: '.b1{margin: 1px;}'
|
||||
)
|
||||
|
||||
SiteCustomization.create!(name: '2',
|
||||
user_id: -1,
|
||||
enabled: true,
|
||||
mobile_stylesheet: '.a2{margin: 1px;}',
|
||||
stylesheet: '.b2{margin: 1px;}'
|
||||
)
|
||||
|
||||
get :show, key: SiteCustomization::ENABLED_KEY, format: :css, target: 'mobile'
|
||||
expect(response.body).to match(/\.a1.*\.a2/m)
|
||||
|
||||
get :show, key: SiteCustomization::ENABLED_KEY, format: :css
|
||||
expect(response.body).to match(/\.b1.*\.b2/m)
|
||||
end
|
||||
|
||||
it 'can deliver specific css' do
|
||||
c = SiteCustomization.create!(name: '1',
|
||||
user_id: -1,
|
||||
enabled: true,
|
||||
mobile_stylesheet: '.a1{margin: 1px;}',
|
||||
stylesheet: '.b1{margin: 1px;}'
|
||||
)
|
||||
|
||||
get :show, key: c.key, format: :css, target: 'mobile'
|
||||
expect(response.body).to match(/\.a1/)
|
||||
|
||||
get :show, key: c.key, format: :css
|
||||
expect(response.body).to match(/\.b1/)
|
||||
end
|
||||
end
|
||||
@@ -5,7 +5,7 @@ describe StylesheetsController do
|
||||
it 'can survive cache miss' do
|
||||
|
||||
StylesheetCache.destroy_all
|
||||
builder = DiscourseStylesheets.new('desktop_rtl')
|
||||
builder = Stylesheet::Manager.new('desktop_rtl', nil)
|
||||
builder.compile
|
||||
builder.ensure_digestless_file
|
||||
|
||||
@@ -26,7 +26,7 @@ describe StylesheetsController do
|
||||
expect(cached.digest).to eq digest
|
||||
|
||||
# tmp folder destruction and cached
|
||||
`rm #{DiscourseStylesheets.cache_fullpath}/*`
|
||||
`rm #{Stylesheet::Manager.cache_fullpath}/*`
|
||||
|
||||
get :show, name: 'desktop_rtl'
|
||||
expect(response).to be_success
|
||||
@@ -38,4 +38,31 @@ describe StylesheetsController do
|
||||
|
||||
end
|
||||
|
||||
it 'can lookup theme specific css' do
|
||||
scheme = ColorScheme.create_from_base({name: "testing", colors: []})
|
||||
theme = Theme.create!(name: "test", color_scheme_id: scheme.id, user_id: -1)
|
||||
|
||||
builder = Stylesheet::Manager.new(:desktop, theme.key)
|
||||
builder.compile
|
||||
|
||||
`rm #{Stylesheet::Manager.cache_fullpath}/*`
|
||||
|
||||
get :show, name: builder.stylesheet_filename.sub(".css", "")
|
||||
expect(response).to be_success
|
||||
|
||||
get :show, name: builder.stylesheet_filename_no_digest.sub(".css", "")
|
||||
expect(response).to be_success
|
||||
|
||||
builder = Stylesheet::Manager.new(:desktop_theme, theme.key)
|
||||
builder.compile
|
||||
|
||||
`rm #{Stylesheet::Manager.cache_fullpath}/*`
|
||||
|
||||
get :show, name: builder.stylesheet_filename.sub(".css", "")
|
||||
expect(response).to be_success
|
||||
|
||||
get :show, name: builder.stylesheet_filename_no_digest.sub(".css", "")
|
||||
expect(response).to be_success
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
Fabricator(:color_scheme) do
|
||||
name { sequence(:name) {|i| "Palette #{i}" } }
|
||||
enabled false
|
||||
color_scheme_colors(count: 2) { |attrs, i| Fabricate.build(:color_scheme_color, color_scheme: nil) }
|
||||
end
|
||||
|
||||
@@ -2,7 +2,7 @@ require 'rails_helper'
|
||||
|
||||
describe ColorScheme do
|
||||
|
||||
let(:valid_params) { {name: "Best Colors Evar", enabled: true, colors: valid_colors} }
|
||||
let(:valid_params) { {name: "Best Colors Evar", colors: valid_colors} }
|
||||
let(:valid_colors) { [
|
||||
{name: '$primary_background_color', hex: 'FFBB00'},
|
||||
{name: '$secondary_background_color', hex: '888888'}
|
||||
@@ -10,7 +10,7 @@ describe ColorScheme do
|
||||
|
||||
describe "new" do
|
||||
it "can take colors" do
|
||||
c = described_class.new(valid_params)
|
||||
c = ColorScheme.new(valid_params)
|
||||
expect(c.colors.size).to eq valid_colors.size
|
||||
expect(c.colors.first).to be_a(ColorSchemeColor)
|
||||
expect {
|
||||
@@ -55,29 +55,4 @@ describe ColorScheme do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "destroy" do
|
||||
it "also destroys old versions" do
|
||||
c1 = described_class.create(valid_params.merge(version: 2))
|
||||
_c2 = described_class.create(valid_params.merge(versioned_id: c1.id, version: 1))
|
||||
_other = described_class.create(valid_params)
|
||||
expect {
|
||||
c1.destroy
|
||||
}.to change { described_class.count }.by(-2)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#enabled" do
|
||||
it "returns nil when there is no enabled record" do
|
||||
expect(described_class.enabled).to eq nil
|
||||
end
|
||||
|
||||
it "returns the enabled color scheme" do
|
||||
ColorScheme.hex_cache.clear
|
||||
expect(described_class.hex_for_name('$primary_background_color')).to eq nil
|
||||
c = described_class.create(valid_params.merge(enabled: true))
|
||||
expect(described_class.enabled.id).to eq c.id
|
||||
expect(described_class.hex_for_name('$primary_background_color')).to eq "FFBB00"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
84
spec/models/remote_theme_spec.rb
Normal file
84
spec/models/remote_theme_spec.rb
Normal file
@@ -0,0 +1,84 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe RemoteTheme do
|
||||
context '#import_remote' do
|
||||
def setup_git_repo(files)
|
||||
dir = Dir.tmpdir
|
||||
repo_dir = "#{dir}/#{SecureRandom.hex}"
|
||||
`mkdir #{repo_dir}`
|
||||
`cd #{repo_dir} && git init .`
|
||||
`cd #{repo_dir} && mkdir desktop mobile common`
|
||||
files.each do |name, data|
|
||||
File.write("#{repo_dir}/#{name}", data)
|
||||
`cd #{repo_dir} && git add #{name}`
|
||||
end
|
||||
`cd #{repo_dir} && git commit -am 'first commit'`
|
||||
repo_dir
|
||||
end
|
||||
|
||||
let :initial_repo do
|
||||
setup_git_repo(
|
||||
"about.json" => '{
|
||||
"name": "awesome theme",
|
||||
"about_url": "https://www.site.com/about",
|
||||
"license_url": "https://www.site.com/license"
|
||||
}',
|
||||
"desktop/desktop.scss" => "body {color: red;}",
|
||||
"common/header.html" => "I AM HEADER",
|
||||
"common/random.html" => "I AM SILLY",
|
||||
)
|
||||
end
|
||||
|
||||
after do
|
||||
`rm -fr #{initial_repo}`
|
||||
end
|
||||
|
||||
it 'can correctly import a remote theme' do
|
||||
|
||||
time = Time.new('2000')
|
||||
freeze_time time
|
||||
|
||||
@theme = RemoteTheme.import_theme(initial_repo)
|
||||
remote = @theme.remote_theme
|
||||
|
||||
expect(@theme.name).to eq('awesome theme')
|
||||
expect(remote.remote_url).to eq(initial_repo)
|
||||
expect(remote.remote_version).to eq(`cd #{initial_repo} && git rev-parse HEAD`.strip)
|
||||
expect(remote.local_version).to eq(`cd #{initial_repo} && git rev-parse HEAD`.strip)
|
||||
|
||||
expect(remote.about_url).to eq("https://www.site.com/about")
|
||||
expect(remote.license_url).to eq("https://www.site.com/license")
|
||||
|
||||
expect(@theme.theme_fields.length).to eq(2)
|
||||
|
||||
mapped = Hash[*@theme.theme_fields.map{|f| ["#{f.target}-#{f.name}", f.value]}.flatten]
|
||||
|
||||
expect(mapped["0-header"]).to eq("I AM HEADER")
|
||||
expect(mapped["1-scss"]).to eq("body {color: red;}")
|
||||
|
||||
expect(remote.remote_updated_at).to eq(time)
|
||||
|
||||
File.write("#{initial_repo}/common/header.html", "I AM UPDATED")
|
||||
`cd #{initial_repo} && git commit -am "update"`
|
||||
|
||||
time = Time.new('2001')
|
||||
freeze_time time
|
||||
|
||||
remote.update_remote_version
|
||||
expect(remote.commits_behind).to eq(1)
|
||||
expect(remote.remote_version).to eq(`cd #{initial_repo} && git rev-parse HEAD`.strip)
|
||||
|
||||
|
||||
remote.update_from_remote
|
||||
@theme.save
|
||||
@theme.reload
|
||||
|
||||
mapped = Hash[*@theme.theme_fields.map{|f| ["#{f.target}-#{f.name}", f.value]}.flatten]
|
||||
|
||||
expect(mapped["0-header"]).to eq("I AM UPDATED")
|
||||
expect(mapped["1-scss"]).to eq("body {color: red;}")
|
||||
expect(remote.remote_updated_at).to eq(time)
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,155 +0,0 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe SiteCustomization do
|
||||
|
||||
before do
|
||||
SiteCustomization.clear_cache!
|
||||
end
|
||||
|
||||
let :user do
|
||||
Fabricate(:user)
|
||||
end
|
||||
|
||||
let :customization_params do
|
||||
{name: 'my name', user_id: user.id, header: "my awesome header", stylesheet: "my awesome css", mobile_stylesheet: nil, mobile_header: nil}
|
||||
end
|
||||
|
||||
let :customization do
|
||||
SiteCustomization.create!(customization_params)
|
||||
end
|
||||
|
||||
let :customization_with_mobile do
|
||||
SiteCustomization.create!(customization_params.merge(mobile_stylesheet: ".mobile {better: true;}", mobile_header: "fancy mobile stuff"))
|
||||
end
|
||||
|
||||
it 'should set default key when creating a new customization' do
|
||||
s = SiteCustomization.create!(name: 'my name', user_id: user.id)
|
||||
expect(s.key).not_to eq(nil)
|
||||
end
|
||||
|
||||
it 'can enable more than one style at once' do
|
||||
c1 = SiteCustomization.create!(name: '2', user_id: user.id, header: 'World',
|
||||
enabled: true, mobile_header: 'hi', footer: 'footer',
|
||||
stylesheet: '.hello{.world {color: blue;}}')
|
||||
|
||||
SiteCustomization.create!(name: '1', user_id: user.id, header: 'Hello',
|
||||
enabled: true, mobile_footer: 'mfooter',
|
||||
mobile_stylesheet: '.hello{margin: 1px;}',
|
||||
stylesheet: 'p{width: 1px;}'
|
||||
)
|
||||
|
||||
expect(SiteCustomization.custom_header).to eq("Hello\nWorld")
|
||||
expect(SiteCustomization.custom_header(nil, :mobile)).to eq("hi")
|
||||
expect(SiteCustomization.custom_footer(nil, :mobile)).to eq("mfooter")
|
||||
expect(SiteCustomization.custom_footer).to eq("footer")
|
||||
|
||||
desktop_css = SiteCustomization.custom_stylesheet
|
||||
expect(desktop_css).to match(Regexp.new("#{SiteCustomization::ENABLED_KEY}.css\\?target=desktop"))
|
||||
|
||||
mobile_css = SiteCustomization.custom_stylesheet(nil, :mobile)
|
||||
expect(mobile_css).to match(Regexp.new("#{SiteCustomization::ENABLED_KEY}.css\\?target=mobile"))
|
||||
|
||||
expect(SiteCustomization.enabled_stylesheet_contents).to match(/\.hello \.world/)
|
||||
|
||||
# cache expiry
|
||||
c1.enabled = false
|
||||
c1.save
|
||||
|
||||
expect(SiteCustomization.custom_stylesheet).not_to eq(desktop_css)
|
||||
expect(SiteCustomization.enabled_stylesheet_contents).not_to match(/\.hello \.world/)
|
||||
end
|
||||
|
||||
it 'should be able to look up stylesheets by key' do
|
||||
c = SiteCustomization.create!(name: '2', user_id: user.id,
|
||||
enabled: true,
|
||||
stylesheet: '.hello{.world {color: blue;}}',
|
||||
mobile_stylesheet: '.world{.hello{color: black;}}')
|
||||
|
||||
expect(SiteCustomization.custom_stylesheet(c.key, :mobile)).to match(Regexp.new("#{c.key}.css\\?target=mobile"))
|
||||
expect(SiteCustomization.custom_stylesheet(c.key)).to match(Regexp.new("#{c.key}.css\\?target=desktop"))
|
||||
|
||||
end
|
||||
|
||||
|
||||
it 'should allow including discourse styles' do
|
||||
c = SiteCustomization.create!(user_id: user.id, name: "test", stylesheet: '@import "desktop";', mobile_stylesheet: '@import "mobile";')
|
||||
expect(c.stylesheet_baked).not_to match(/Syntax error/)
|
||||
expect(c.stylesheet_baked.length).to be > 1000
|
||||
expect(c.mobile_stylesheet_baked).not_to match(/Syntax error/)
|
||||
expect(c.mobile_stylesheet_baked.length).to be > 1000
|
||||
end
|
||||
|
||||
it 'should provide an awesome error on failure' do
|
||||
c = SiteCustomization.create!(user_id: user.id, name: "test", stylesheet: "$black: #000; #a { color: $black; }\n\n\nboom", header: '')
|
||||
expect(c.stylesheet_baked).to match(/Syntax error/)
|
||||
expect(c.mobile_stylesheet_baked).not_to be_present
|
||||
end
|
||||
|
||||
it 'should provide an awesome error on failure for mobile too' do
|
||||
c = SiteCustomization.create!(user_id: user.id, name: "test", stylesheet: '', header: '', mobile_stylesheet: "$black: #000; #a { color: $black; }\n\n\nboom", mobile_header: '')
|
||||
expect(c.mobile_stylesheet_baked).to match(/Syntax error/)
|
||||
expect(c.stylesheet_baked).not_to be_present
|
||||
end
|
||||
|
||||
it 'should correct bad html in body_tag_baked and head_tag_baked' do
|
||||
c = SiteCustomization.create!(user_id: -1, name: "test", head_tag: "<b>I am bold", body_tag: "<b>I am bold")
|
||||
expect(c.head_tag_baked).to eq("<b>I am bold</b>")
|
||||
expect(c.body_tag_baked).to eq("<b>I am bold</b>")
|
||||
end
|
||||
|
||||
it 'should precompile fragments in body and head tags' do
|
||||
with_template = <<HTML
|
||||
<script type='text/x-handlebars' name='template'>
|
||||
{{hello}}
|
||||
</script>
|
||||
<script type='text/x-handlebars' data-template-name='raw_template.raw'>
|
||||
{{hello}}
|
||||
</script>
|
||||
HTML
|
||||
c = SiteCustomization.create!(user_id: -1, name: "test", head_tag: with_template, body_tag: with_template)
|
||||
expect(c.head_tag_baked).to match(/HTMLBars/)
|
||||
expect(c.body_tag_baked).to match(/HTMLBars/)
|
||||
expect(c.body_tag_baked).to match(/raw-handlebars/)
|
||||
expect(c.head_tag_baked).to match(/raw-handlebars/)
|
||||
end
|
||||
|
||||
it 'should create body_tag_baked on demand if needed' do
|
||||
c = SiteCustomization.create!(user_id: -1, name: "test", head_tag: "<b>test", enabled: true)
|
||||
c.update_columns(head_tag_baked: nil)
|
||||
expect(SiteCustomization.custom_head_tag).to match(/<b>test<\/b>/)
|
||||
end
|
||||
|
||||
context "plugin api" do
|
||||
def transpile(html)
|
||||
c = SiteCustomization.create!(user_id: -1, name: "test", head_tag: html, body_tag: html)
|
||||
c.head_tag_baked
|
||||
end
|
||||
|
||||
it "transpiles ES6 code" do
|
||||
html = <<HTML
|
||||
<script type='text/discourse-plugin' version='0.1'>
|
||||
const x = 1;
|
||||
</script>
|
||||
HTML
|
||||
|
||||
transpiled = transpile(html)
|
||||
expect(transpiled).to match(/\<script\>/)
|
||||
expect(transpiled).to match(/var x = 1;/)
|
||||
expect(transpiled).to match(/_registerPluginCode\('0.1'/)
|
||||
end
|
||||
|
||||
it "converts errors to a script type that is not evaluated" do
|
||||
html = <<HTML
|
||||
<script type='text/discourse-plugin' version='0.1'>
|
||||
const x = 1;
|
||||
x = 2;
|
||||
</script>
|
||||
HTML
|
||||
|
||||
transpiled = transpile(html)
|
||||
expect(transpiled).to match(/text\/discourse-js-error/)
|
||||
expect(transpiled).to match(/read-only/)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
@@ -2,6 +2,44 @@ require 'rails_helper'
|
||||
require_dependency 'site'
|
||||
|
||||
describe Site do
|
||||
|
||||
def expect_correct_themes(guardian)
|
||||
json = Site.json_for(guardian)
|
||||
parsed = JSON.parse(json)
|
||||
|
||||
expected = Theme.where('key = :default OR user_selectable',
|
||||
default: SiteSetting.default_theme_key)
|
||||
.order(:name)
|
||||
.pluck(:key, :name)
|
||||
.map{|k,n| {"theme_key" => k, "name" => n, "default" => k == SiteSetting.default_theme_key}}
|
||||
|
||||
expect(parsed["user_themes"]).to eq(expected)
|
||||
end
|
||||
|
||||
it "includes user themes and expires them as needed" do
|
||||
default_theme = Theme.create!(user_id: -1, name: 'default')
|
||||
SiteSetting.default_theme_key = default_theme.key
|
||||
user_theme = Theme.create!(user_id: -1, name: 'user theme', user_selectable: true)
|
||||
|
||||
anon_guardian = Guardian.new
|
||||
user_guardian = Guardian.new(Fabricate(:user))
|
||||
|
||||
expect_correct_themes(anon_guardian)
|
||||
expect_correct_themes(user_guardian)
|
||||
|
||||
Theme.clear_default!
|
||||
|
||||
expect_correct_themes(anon_guardian)
|
||||
expect_correct_themes(user_guardian)
|
||||
|
||||
user_theme.user_selectable = false
|
||||
user_theme.save!
|
||||
|
||||
expect_correct_themes(anon_guardian)
|
||||
expect_correct_themes(user_guardian)
|
||||
|
||||
end
|
||||
|
||||
it "omits categories users can not write to from the category list" do
|
||||
category = Fabricate(:category)
|
||||
user = Fabricate(:user)
|
||||
|
||||
@@ -5,7 +5,7 @@ describe StylesheetCache do
|
||||
describe "add" do
|
||||
it "correctly cycles once MAX_TO_KEEP is hit" do
|
||||
(StylesheetCache::MAX_TO_KEEP + 1).times do |i|
|
||||
StylesheetCache.add("a", "d" + i.to_s, "c" + i.to_s)
|
||||
StylesheetCache.add("a", "d" + i.to_s, "c" + i.to_s, "map")
|
||||
end
|
||||
|
||||
expect(StylesheetCache.count).to eq StylesheetCache::MAX_TO_KEEP
|
||||
@@ -13,8 +13,8 @@ describe StylesheetCache do
|
||||
end
|
||||
|
||||
it "does nothing if digest is set and already exists" do
|
||||
StylesheetCache.add("a", "b", "c")
|
||||
StylesheetCache.add("a", "b", "cc")
|
||||
StylesheetCache.add("a", "b", "c", "map")
|
||||
StylesheetCache.add("a", "b", "cc", "map")
|
||||
|
||||
expect(StylesheetCache.count).to eq 1
|
||||
expect(StylesheetCache.first.content).to eq "c"
|
||||
|
||||
141
spec/models/theme_spec.rb
Normal file
141
spec/models/theme_spec.rb
Normal file
@@ -0,0 +1,141 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe Theme do
|
||||
|
||||
before do
|
||||
Theme.clear_cache!
|
||||
end
|
||||
|
||||
let :user do
|
||||
Fabricate(:user)
|
||||
end
|
||||
|
||||
let :customization_params do
|
||||
{name: 'my name', user_id: user.id, header: "my awesome header"}
|
||||
end
|
||||
|
||||
let :customization do
|
||||
Theme.create!(customization_params)
|
||||
end
|
||||
|
||||
it 'should set default key when creating a new customization' do
|
||||
s = Theme.create!(name: 'my name', user_id: user.id)
|
||||
expect(s.key).not_to eq(nil)
|
||||
end
|
||||
|
||||
it 'can support child themes' do
|
||||
child = Theme.new(name: '2', user_id: user.id)
|
||||
|
||||
child.set_field(:common, "header", "World")
|
||||
child.set_field(:desktop, "header", "Desktop")
|
||||
child.set_field(:mobile, "header", "Mobile")
|
||||
|
||||
child.save!
|
||||
|
||||
expect(Theme.lookup_field(child.key, :desktop, "header")).to eq("World\nDesktop")
|
||||
expect(Theme.lookup_field(child.key, "mobile", :header)).to eq("World\nMobile")
|
||||
|
||||
|
||||
child.set_field(:common, "header", "Worldie")
|
||||
child.save!
|
||||
|
||||
expect(Theme.lookup_field(child.key, :mobile, :header)).to eq("Worldie\nMobile")
|
||||
|
||||
parent = Theme.new(name: '1', user_id: user.id)
|
||||
|
||||
parent.set_field(:common, "header", "Common Parent")
|
||||
parent.set_field(:mobile, "header", "Mobile Parent")
|
||||
|
||||
parent.save!
|
||||
|
||||
parent.add_child_theme!(child)
|
||||
|
||||
expect(Theme.lookup_field(parent.key, :mobile, "header")).to eq("Common Parent\nMobile Parent\nWorldie\nMobile")
|
||||
|
||||
end
|
||||
|
||||
it 'can correctly find parent themes' do
|
||||
grandchild = Theme.create!(name: 'grandchild', user_id: user.id)
|
||||
child = Theme.create!(name: 'child', user_id: user.id)
|
||||
theme = Theme.create!(name: 'theme', user_id: user.id)
|
||||
|
||||
theme.add_child_theme!(child)
|
||||
child.add_child_theme!(grandchild)
|
||||
|
||||
expect(grandchild.dependant_themes.length).to eq(2)
|
||||
end
|
||||
|
||||
|
||||
it 'should correct bad html in body_tag_baked and head_tag_baked' do
|
||||
theme = Theme.new(user_id: -1, name: "test")
|
||||
theme.set_field(:common, "head_tag", "<b>I am bold")
|
||||
theme.save!
|
||||
|
||||
expect(Theme.lookup_field(theme.key, :desktop, "head_tag")).to eq("<b>I am bold</b>")
|
||||
end
|
||||
|
||||
it 'should precompile fragments in body and head tags' do
|
||||
with_template = <<HTML
|
||||
<script type='text/x-handlebars' name='template'>
|
||||
{{hello}}
|
||||
</script>
|
||||
<script type='text/x-handlebars' data-template-name='raw_template.raw'>
|
||||
{{hello}}
|
||||
</script>
|
||||
HTML
|
||||
theme = Theme.new(user_id: -1, name: "test")
|
||||
theme.set_field(:common, "header", with_template)
|
||||
theme.save!
|
||||
|
||||
baked = Theme.lookup_field(theme.key, :mobile, "header")
|
||||
|
||||
expect(baked).to match(/HTMLBars/)
|
||||
expect(baked).to match(/raw-handlebars/)
|
||||
end
|
||||
|
||||
it 'should create body_tag_baked on demand if needed' do
|
||||
|
||||
theme = Theme.new(user_id: -1, name: "test")
|
||||
theme.set_field(:common, :body_tag, "<b>test")
|
||||
theme.save
|
||||
|
||||
ThemeField.update_all(value_baked: nil)
|
||||
|
||||
expect(Theme.lookup_field(theme.key, :desktop, :body_tag)).to match(/<b>test<\/b>/)
|
||||
end
|
||||
|
||||
context "plugin api" do
|
||||
def transpile(html)
|
||||
f = ThemeField.create!(target: Theme.targets[:mobile], theme_id: -1, name: "after_header", value: html)
|
||||
f.value_baked
|
||||
end
|
||||
|
||||
it "transpiles ES6 code" do
|
||||
html = <<HTML
|
||||
<script type='text/discourse-plugin' version='0.1'>
|
||||
const x = 1;
|
||||
</script>
|
||||
HTML
|
||||
|
||||
transpiled = transpile(html)
|
||||
expect(transpiled).to match(/\<script\>/)
|
||||
expect(transpiled).to match(/var x = 1;/)
|
||||
expect(transpiled).to match(/_registerPluginCode\('0.1'/)
|
||||
end
|
||||
|
||||
it "converts errors to a script type that is not evaluated" do
|
||||
html = <<HTML
|
||||
<script type='text/discourse-plugin' version='0.1'>
|
||||
const x = 1;
|
||||
x = 2;
|
||||
</script>
|
||||
HTML
|
||||
|
||||
transpiled = transpile(html)
|
||||
expect(transpiled).to match(/text\/discourse-js-error/)
|
||||
expect(transpiled).to match(/read-only/)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
end
|
||||
@@ -3,62 +3,42 @@ require 'rails_helper'
|
||||
describe ColorSchemeRevisor do
|
||||
|
||||
let(:color) { Fabricate.build(:color_scheme_color, hex: 'FFFFFF', color_scheme: nil) }
|
||||
let(:color_scheme) { Fabricate(:color_scheme, enabled: false, created_at: 1.day.ago, updated_at: 1.day.ago, color_scheme_colors: [color]) }
|
||||
let(:valid_params) { { name: color_scheme.name, enabled: color_scheme.enabled, colors: nil } }
|
||||
let(:color_scheme) { Fabricate(:color_scheme, created_at: 1.day.ago, updated_at: 1.day.ago, color_scheme_colors: [color]) }
|
||||
let(:valid_params) { { name: color_scheme.name, colors: nil } }
|
||||
|
||||
describe "revise" do
|
||||
it "does nothing if there are no changes" do
|
||||
expect {
|
||||
described_class.revise(color_scheme, valid_params.merge(colors: nil))
|
||||
ColorSchemeRevisor.revise(color_scheme, valid_params.merge(colors: nil))
|
||||
}.to_not change { color_scheme.reload.updated_at }
|
||||
end
|
||||
|
||||
it "can change the name" do
|
||||
described_class.revise(color_scheme, valid_params.merge(name: "Changed Name"))
|
||||
ColorSchemeRevisor.revise(color_scheme, valid_params.merge(name: "Changed Name"))
|
||||
expect(color_scheme.reload.name).to eq("Changed Name")
|
||||
end
|
||||
|
||||
it "can update the theme_id" do
|
||||
described_class.revise(color_scheme, valid_params.merge(theme_id: 'test'))
|
||||
expect(color_scheme.reload.theme_id).to eq('test')
|
||||
it "can update the base_scheme_id" do
|
||||
ColorSchemeRevisor.revise(color_scheme, valid_params.merge(base_scheme_id: 'test'))
|
||||
expect(color_scheme.reload.base_scheme_id).to eq('test')
|
||||
end
|
||||
|
||||
it "can enable and disable" do
|
||||
described_class.revise(color_scheme, valid_params.merge(enabled: true))
|
||||
expect(color_scheme.reload).to be_enabled
|
||||
described_class.revise(color_scheme, valid_params.merge(enabled: false))
|
||||
expect(color_scheme.reload).not_to be_enabled
|
||||
end
|
||||
|
||||
def test_color_change(color_scheme_arg, expected_enabled)
|
||||
described_class.revise(color_scheme_arg, valid_params.merge(colors: [
|
||||
{name: color.name, hex: 'BEEF99'}
|
||||
it 'can change colors' do
|
||||
ColorSchemeRevisor.revise(color_scheme, valid_params.merge(colors: [
|
||||
{name: color.name, hex: 'BEEF99'},
|
||||
{name: 'bob', hex: 'AAAAAA'}
|
||||
]))
|
||||
color_scheme_arg.reload
|
||||
expect(color_scheme_arg.enabled).to eq(expected_enabled)
|
||||
expect(color_scheme_arg.colors.size).to eq(1)
|
||||
expect(color_scheme_arg.colors.first.hex).to eq('BEEF99')
|
||||
end
|
||||
color_scheme.reload
|
||||
|
||||
it "can change colors of a color scheme that's not enabled" do
|
||||
test_color_change(color_scheme, false)
|
||||
end
|
||||
|
||||
it "can change colors of the enabled color scheme" do
|
||||
color_scheme.update_attribute(:enabled, true)
|
||||
test_color_change(color_scheme, true)
|
||||
end
|
||||
|
||||
it "disables other color scheme before enabling" do
|
||||
prev_enabled = Fabricate(:color_scheme, enabled: true)
|
||||
described_class.revise(color_scheme, valid_params.merge(enabled: true))
|
||||
expect(prev_enabled.reload.enabled).to eq(false)
|
||||
expect(color_scheme.reload.enabled).to eq(true)
|
||||
expect(color_scheme.version).to eq(2)
|
||||
expect(color_scheme.colors.size).to eq(2)
|
||||
expect(color_scheme.colors.find_by(name: color.name).hex).to eq('BEEF99')
|
||||
expect(color_scheme.colors.find_by(name: 'bob').hex).to eq('AAAAAA')
|
||||
end
|
||||
|
||||
it "doesn't make changes when a color is invalid" do
|
||||
expect {
|
||||
cs = described_class.revise(color_scheme, valid_params.merge(colors: [
|
||||
cs = ColorSchemeRevisor.revise(color_scheme, valid_params.merge(colors: [
|
||||
{name: color.name, hex: 'OOPS'}
|
||||
]))
|
||||
expect(cs).not_to be_valid
|
||||
@@ -66,72 +46,6 @@ describe ColorSchemeRevisor do
|
||||
}.to_not change { color_scheme.reload.version }
|
||||
expect(color_scheme.colors.first.hex).to eq(color.hex)
|
||||
end
|
||||
|
||||
describe "versions" do
|
||||
it "doesn't create a new version if colors is not given" do
|
||||
expect {
|
||||
described_class.revise(color_scheme, valid_params.merge(name: "Changed Name"))
|
||||
}.to_not change { color_scheme.reload.version }
|
||||
end
|
||||
|
||||
it "creates a new version if colors have changed" do
|
||||
old_hex = color.hex
|
||||
expect {
|
||||
described_class.revise(color_scheme, valid_params.merge(colors: [
|
||||
{name: color.name, hex: 'BEEF99'}
|
||||
]))
|
||||
}.to change { color_scheme.reload.version }.by(1)
|
||||
old_version = ColorScheme.find_by(versioned_id: color_scheme.id, version: (color_scheme.version - 1))
|
||||
expect(old_version).not_to eq(nil)
|
||||
expect(old_version.colors.count).to eq(color_scheme.colors.count)
|
||||
expect(old_version.colors_by_name[color.name].hex).to eq(old_hex)
|
||||
expect(color_scheme.colors_by_name[color.name].hex).to eq('BEEF99')
|
||||
end
|
||||
|
||||
it "doesn't create a new version if colors have not changed" do
|
||||
expect {
|
||||
described_class.revise(color_scheme, valid_params.merge(colors: [
|
||||
{name: color.name, hex: color.hex}
|
||||
]))
|
||||
}.to_not change { color_scheme.reload.version }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "revert" do
|
||||
context "when there are no previous versions" do
|
||||
it "does nothing" do
|
||||
expect {
|
||||
expect(described_class.revert(color_scheme)).to eq(color_scheme)
|
||||
}.to_not change { color_scheme.reload.version }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are previous versions' do
|
||||
let(:new_color_params) { {name: color.name, hex: 'BEEF99'} }
|
||||
|
||||
before do
|
||||
@prev_hex = color.hex
|
||||
described_class.revise(color_scheme, valid_params.merge(colors: [ new_color_params ]))
|
||||
end
|
||||
|
||||
it "reverts the colors to the previous version" do
|
||||
expect(color_scheme.colors_by_name[new_color_params[:name]].hex).to eq(new_color_params[:hex])
|
||||
expect {
|
||||
described_class.revert(color_scheme)
|
||||
}.to change { color_scheme.reload.version }.by(-1)
|
||||
expect(color_scheme.colors.size).to eq(1)
|
||||
expect(color_scheme.colors.first.hex).to eq(@prev_hex)
|
||||
expect(color_scheme.colors_by_name[new_color_params[:name]].hex).to eq(@prev_hex)
|
||||
end
|
||||
|
||||
it "destroys the old version's record" do
|
||||
expect {
|
||||
described_class.revert(color_scheme)
|
||||
}.to change { ColorScheme.count }.by(-1)
|
||||
expect(color_scheme.reload.previous_version).to eq(nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -129,46 +129,56 @@ describe StaffActionLogger do
|
||||
end
|
||||
end
|
||||
|
||||
describe "log_site_customization_change" do
|
||||
let(:valid_params) { {name: 'Cool Theme', stylesheet: "body {\n background-color: blue;\n}\n", header: "h1 {color: white;}"} }
|
||||
describe "log_theme_change" do
|
||||
|
||||
it "raises an error when params are invalid" do
|
||||
expect { logger.log_site_customization_change(nil, nil) }.to raise_error(Discourse::InvalidParameters)
|
||||
expect { logger.log_theme_change(nil, nil) }.to raise_error(Discourse::InvalidParameters)
|
||||
end
|
||||
|
||||
let :theme do
|
||||
Theme.new(name: 'bob', user_id: -1)
|
||||
end
|
||||
|
||||
it "logs new site customizations" do
|
||||
log_record = logger.log_site_customization_change(nil, valid_params)
|
||||
expect(log_record.subject).to eq(valid_params[:name])
|
||||
|
||||
log_record = logger.log_theme_change(nil, theme)
|
||||
expect(log_record.subject).to eq(theme.name)
|
||||
expect(log_record.previous_value).to eq(nil)
|
||||
expect(log_record.new_value).to be_present
|
||||
|
||||
json = ::JSON.parse(log_record.new_value)
|
||||
expect(json['stylesheet']).to be_present
|
||||
expect(json['header']).to be_present
|
||||
expect(json['name']).to eq(theme.name)
|
||||
end
|
||||
|
||||
it "logs updated site customizations" do
|
||||
existing = SiteCustomization.new(name: 'Banana', stylesheet: "body {color: yellow;}", header: "h1 {color: brown;}")
|
||||
log_record = logger.log_site_customization_change(existing, valid_params)
|
||||
old_json = ThemeSerializer.new(theme, root:false).to_json
|
||||
|
||||
theme.set_field(:common, :scss, "body{margin: 10px;}")
|
||||
|
||||
log_record = logger.log_theme_change(old_json, theme)
|
||||
|
||||
expect(log_record.previous_value).to be_present
|
||||
json = ::JSON.parse(log_record.previous_value)
|
||||
expect(json['stylesheet']).to eq(existing.stylesheet)
|
||||
expect(json['header']).to eq(existing.header)
|
||||
|
||||
json = ::JSON.parse(log_record.new_value)
|
||||
expect(json['theme_fields']).to eq([{"name" => "scss", "target" => "common", "value" => "body{margin: 10px;}"}])
|
||||
end
|
||||
end
|
||||
|
||||
describe "log_site_customization_destroy" do
|
||||
describe "log_theme_destroy" do
|
||||
it "raises an error when params are invalid" do
|
||||
expect { logger.log_site_customization_destroy(nil) }.to raise_error(Discourse::InvalidParameters)
|
||||
expect { logger.log_theme_destroy(nil) }.to raise_error(Discourse::InvalidParameters)
|
||||
end
|
||||
|
||||
it "creates a new UserHistory record" do
|
||||
site_customization = SiteCustomization.new(name: 'Banana', stylesheet: "body {color: yellow;}", header: "h1 {color: brown;}")
|
||||
log_record = logger.log_site_customization_destroy(site_customization)
|
||||
theme = Theme.new(name: 'Banana')
|
||||
theme.set_field(:common, :scss, "body{margin: 10px;}")
|
||||
|
||||
log_record = logger.log_theme_destroy(theme)
|
||||
expect(log_record.previous_value).to be_present
|
||||
expect(log_record.new_value).to eq(nil)
|
||||
json = ::JSON.parse(log_record.previous_value)
|
||||
expect(json['stylesheet']).to eq(site_customization.stylesheet)
|
||||
expect(json['header']).to eq(site_customization.header)
|
||||
|
||||
expect(json['theme_fields']).to eq([{"name" => "scss", "target" => "common", "value" => "body{margin: 10px;}"}])
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user