mirror of
https://github.com/discourse/discourse.git
synced 2025-05-25 09:57:25 +08:00

On 1st April 2025, we start showing no icons if the icon name used is a deprecated one and therefore no longer part of the svg set. We'll continue showing the messages with the correct icon name to aid correction of these names. Console logging will now be done at an error level for such icons. We retain the behaviour of raising an error for such icons in plugins from svg_sprite.rb in test environments, but removed this from icon-library.js as it's harder to test the actual expected behaviour of returning the original icon now that it's not part of the deprecation workflow. (sinon.stub doesn't work well here for `isTesting` - the alternative would be to override the environment.js module with `proxyquire`) In any case, once we remove the mapping logic, we won't be raising errors in test environment either for this scenario.
379 lines
13 KiB
Ruby
379 lines
13 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
RSpec.describe SvgSprite do
|
|
fab!(:theme)
|
|
|
|
before do
|
|
SvgSprite.clear_plugin_svg_sprite_cache!
|
|
SvgSprite.expire_cache
|
|
allow(Rails.env).to receive(:test?).and_return(false)
|
|
end
|
|
|
|
it "can generate a bundle" do
|
|
bundle = SvgSprite.bundle
|
|
expect(bundle).to match(/heart/)
|
|
expect(bundle).to match(/angles-down/)
|
|
end
|
|
|
|
it "can generate paths" do
|
|
version = SvgSprite.version # Icons won't change for this test
|
|
expect(SvgSprite.path).to eq("/svg-sprite/#{Discourse.current_hostname}/svg--#{version}.js")
|
|
expect(SvgSprite.path(1)).to eq("/svg-sprite/#{Discourse.current_hostname}/svg-1-#{version}.js")
|
|
|
|
# Safe mode
|
|
expect(SvgSprite.path(nil)).to eq(
|
|
"/svg-sprite/#{Discourse.current_hostname}/svg--#{version}.js",
|
|
)
|
|
end
|
|
|
|
it "can search for a specific FA icon" do
|
|
expect(SvgSprite.search("poo-storm")).to match(/poo-storm/)
|
|
expect(SvgSprite.search("this-is-not-an-icon")).to eq(false)
|
|
end
|
|
|
|
it "can get a raw SVG for an icon" do
|
|
expect(SvgSprite.raw_svg("heart")).to match(/svg.*svg/) # SVG inside SVG
|
|
expect(SvgSprite.raw_svg("this-is-not-an-icon")).to eq("")
|
|
end
|
|
|
|
it "can get a consistent version string" do
|
|
version1 = SvgSprite.version
|
|
version2 = SvgSprite.version
|
|
|
|
expect(version1).to eq(version2)
|
|
end
|
|
|
|
it "version string changes" do
|
|
version1 = SvgSprite.version
|
|
Fabricate(:badge, name: "Custom Icon Badge", icon: "gamepad")
|
|
version2 = SvgSprite.version
|
|
|
|
expect(version1).not_to eq(version2)
|
|
end
|
|
|
|
it "version should be based on bundled output, not requested icons" do
|
|
fname = "custom-theme-icon-sprite.svg"
|
|
upload = UploadCreator.new(file_from_fixtures(fname), fname, for_theme: true).create_for(-1)
|
|
|
|
version1 = SvgSprite.version(theme.id)
|
|
bundle1 = SvgSprite.bundle(theme.id)
|
|
|
|
SiteSetting.svg_icon_subset = "my-custom-theme-icon"
|
|
|
|
version2 = SvgSprite.version(theme.id)
|
|
bundle2 = SvgSprite.bundle(theme.id)
|
|
|
|
# The contents of the bundle should not change, because the icon does not actually exist
|
|
expect(bundle1).to eq(bundle2)
|
|
# Therefore the version hash should not change
|
|
expect(version1).to eq(version2)
|
|
|
|
# Now add the icon to the theme
|
|
theme.set_field(
|
|
target: :common,
|
|
name: SvgSprite.theme_sprite_variable_name,
|
|
upload_id: upload.id,
|
|
type: :theme_upload_var,
|
|
)
|
|
theme.save!
|
|
|
|
version3 = SvgSprite.version(theme.id)
|
|
bundle3 = SvgSprite.bundle(theme.id)
|
|
|
|
# The version/bundle should be updated
|
|
expect(bundle3).not_to match(bundle2)
|
|
expect(version3).not_to match(version2)
|
|
expect(bundle3).to match(/my-custom-theme-icon/)
|
|
end
|
|
|
|
it "strips whitespace when processing icons" do
|
|
Fabricate(:badge, name: "Custom Icon Badge", icon: " fab fa-facebook-messenger ")
|
|
expect(SvgSprite.all_icons).to include("fab fa-facebook-messenger")
|
|
expect(SvgSprite.all_icons).not_to include(" fab fa-facebook-messenger ")
|
|
end
|
|
|
|
it "includes icons from badges" do
|
|
Fabricate(:badge, name: "Custom Icon Badge", icon: "far fa-building")
|
|
expect(SvgSprite.all_icons).to include("far fa-building")
|
|
end
|
|
|
|
it "raises an error in test for deprecated icons" do
|
|
allow(Rails.env).to receive(:test?).and_return(true)
|
|
expect { SvgSprite.search("fa-gamepad") }.to raise_error(Discourse::MissingIconError)
|
|
end
|
|
|
|
it "includes icons defined in theme settings" do
|
|
# Works for default settings:
|
|
theme.set_field(target: :settings, name: :yaml, value: "custom_icon: dragon")
|
|
theme.save!
|
|
expect(SvgSprite.all_icons(theme.id)).to include("dragon")
|
|
|
|
# Automatically purges cache when default changes:
|
|
theme.set_field(target: :settings, name: :yaml, value: "custom_icon: gamepad")
|
|
theme.save!
|
|
expect(SvgSprite.all_icons(theme.id)).to include("gamepad")
|
|
|
|
# Works when applying override
|
|
theme.update_setting(:custom_icon, "gas-pump")
|
|
theme.save!
|
|
expect(SvgSprite.all_icons(theme.id)).to include("gas-pump")
|
|
|
|
# Works when changing override
|
|
theme.update_setting(:custom_icon, "gamepad")
|
|
theme.save!
|
|
expect(SvgSprite.all_icons(theme.id)).to include("gamepad")
|
|
expect(SvgSprite.all_icons(theme.id)).not_to include("gas-pump")
|
|
|
|
# FA5 syntax
|
|
theme.update_setting(:custom_icon, "fab fa-bandcamp")
|
|
theme.save!
|
|
expect(SvgSprite.all_icons(theme.id)).to include("fab fa-bandcamp")
|
|
|
|
# Internal Discourse syntax + multiple icons
|
|
theme.update_setting(:custom_icon, "fab-android|dragon")
|
|
theme.save!
|
|
expect(SvgSprite.all_icons(theme.id)).to include("fab-android")
|
|
expect(SvgSprite.all_icons(theme.id)).to include("dragon")
|
|
|
|
# Check themes don't leak into non-theme sprite sheet
|
|
expect(SvgSprite.all_icons).not_to include("dragon")
|
|
|
|
# Check components are included
|
|
theme.update(component: true)
|
|
theme.save!
|
|
parent_theme = Fabricate(:theme)
|
|
parent_theme.add_relative_theme!(:child, theme)
|
|
expect(SvgSprite.all_icons(parent_theme.id)).to include("dragon")
|
|
end
|
|
|
|
it "includes icons defined in theme modifiers" do
|
|
child_theme = Fabricate(:theme, component: true)
|
|
theme.add_relative_theme!(:child, child_theme)
|
|
|
|
expect(SvgSprite.all_icons(theme.id)).not_to include("dragon")
|
|
|
|
theme.theme_modifier_set.svg_icons = ["dragon"]
|
|
theme.save!
|
|
|
|
child_theme.theme_modifier_set.svg_icons = ["fly"]
|
|
child_theme.save!
|
|
|
|
icons = SvgSprite.all_icons(theme.id)
|
|
|
|
expect(icons).to include("dragon")
|
|
expect(icons).to include("fly")
|
|
end
|
|
|
|
describe "s3" do
|
|
let(:upload_s3) { Fabricate(:upload_s3) }
|
|
|
|
before do
|
|
setup_s3
|
|
body = <<~XML
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
|
|
<symbol id="my-custom-theme-icon" viewBox="0 0 496 512">
|
|
<path d="M248 8C111.03 8 0 119.03 0 256s111.03 248 248 248 248-111.03 248-248S384.97 8 248 8zm0 376c-17.67 0-32-14.33-32-32s14.33-32 32-32 32 14.33 32 32-14.33 32-32 32zm0-128c-53.02 0-96 42.98-96 96s42.98 96 96 96c-106.04 0-192-85.96-192-192S141.96 64 248 64c53.02 0 96 42.98 96 96s-42.98 96-96 96zm0-128c-17.67 0-32 14.33-32 32s14.33 32 32 32 32-14.33 32-32-14.33-32-32-32z"></path>
|
|
</symbol>
|
|
</svg>
|
|
XML
|
|
stub_request(:get, upload_s3.url).to_return(status: 200, body: body)
|
|
end
|
|
|
|
it "includes svg sprites in themes stored in s3" do
|
|
theme.set_field(
|
|
target: :common,
|
|
name: SvgSprite.theme_sprite_variable_name,
|
|
upload_id: upload_s3.id,
|
|
type: :theme_upload_var,
|
|
)
|
|
theme.save!
|
|
|
|
sprite_files = SvgSprite.custom_svgs(theme.id).values.join("|")
|
|
expect(sprite_files).to match(/my-custom-theme-icon/)
|
|
|
|
SvgSprite.bundle(theme.id)
|
|
expect(SvgSprite.cache.hash.keys).to include("theme_svg_sprites_#{theme.id}")
|
|
|
|
external_copy = Discourse.store.download(upload_s3)
|
|
File.delete external_copy.try(:path)
|
|
|
|
SvgSprite.bundle(theme.id)
|
|
# after a temp file is missing, bundling still works
|
|
expect(SvgSprite.cache.hash.keys).to include("theme_svg_sprites_#{theme.id}")
|
|
end
|
|
end
|
|
|
|
it "includes icons from SiteSettings" do
|
|
SiteSetting.svg_icon_subset = "blender|compass-drafting|fab-bandcamp"
|
|
|
|
all_icons = SvgSprite.all_icons
|
|
expect(all_icons).to include("blender")
|
|
expect(all_icons).to include("compass-drafting")
|
|
expect(all_icons).to include("fab-bandcamp")
|
|
|
|
SiteSetting.svg_icon_subset = nil
|
|
SvgSprite.expire_cache
|
|
expect(SvgSprite.all_icons).not_to include("compass-drafting")
|
|
|
|
# does not fail on non-string setting
|
|
SiteSetting.svg_icon_subset = false
|
|
SvgSprite.expire_cache
|
|
expect(SvgSprite.all_icons).to be_truthy
|
|
end
|
|
|
|
it "includes icons from plugin registry" do
|
|
DiscoursePluginRegistry.register_svg_icon "blender"
|
|
DiscoursePluginRegistry.register_svg_icon "fab fa-bandcamp"
|
|
|
|
expect(SvgSprite.all_icons).to include("blender")
|
|
expect(SvgSprite.all_icons).to include("fab fa-bandcamp")
|
|
end
|
|
|
|
it "includes Font Awesome icon from groups" do
|
|
_group = Fabricate(:group, flair_icon: "far-building")
|
|
expect(SvgSprite.bundle).to match(/far-building/)
|
|
end
|
|
|
|
describe "#custom_svgs" do
|
|
it "is empty by default" do
|
|
expect(SvgSprite.custom_svgs(nil)).to be_empty
|
|
expect(SvgSprite.bundle).not_to be_empty
|
|
end
|
|
|
|
context "with a plugin" do
|
|
let :plugin1 do
|
|
plugin1 = plugin_from_fixtures("my_plugin")
|
|
plugin1
|
|
end
|
|
|
|
before do
|
|
Discourse.plugins << plugin1
|
|
plugin1.activate!
|
|
end
|
|
|
|
after do
|
|
Discourse.plugins.delete plugin1
|
|
DiscoursePluginRegistry.reset!
|
|
end
|
|
|
|
it "includes custom icons from plugins" do
|
|
expect(SvgSprite.custom_svgs(nil).size).to eq(1)
|
|
expect(SvgSprite.bundle).to match(/custom-icon/)
|
|
end
|
|
end
|
|
|
|
it "includes custom icons in a theme" do
|
|
fname = "custom-theme-icon-sprite.svg"
|
|
|
|
upload = UploadCreator.new(file_from_fixtures(fname), fname, for_theme: true).create_for(-1)
|
|
|
|
theme.set_field(
|
|
target: :common,
|
|
name: SvgSprite.theme_sprite_variable_name,
|
|
upload_id: upload.id,
|
|
type: :theme_upload_var,
|
|
)
|
|
theme.save!
|
|
|
|
expect(Upload.exists?(id: upload.id)).to eq(true)
|
|
expect(SvgSprite.bundle(theme.id)).to match(/my-custom-theme-icon/)
|
|
end
|
|
|
|
it "includes custom icons in a theme and an attached theme component" do
|
|
theme_component = Fabricate(:theme, component: true)
|
|
theme.add_relative_theme!(:child, theme_component)
|
|
|
|
fname1 = "custom-theme-icon-sprite.svg"
|
|
fname2 = "custom-theme-component-icon-sprite.svg"
|
|
|
|
[[theme, fname1], [theme_component, fname2]].each do |t, fname|
|
|
upload = UploadCreator.new(file_from_fixtures(fname), fname, for_theme: true).create_for(-1)
|
|
expect(Upload.exists?(id: upload.id)).to eq(true)
|
|
|
|
t.set_field(
|
|
target: :common,
|
|
name: SvgSprite.theme_sprite_variable_name,
|
|
upload_id: upload.id,
|
|
type: :theme_upload_var,
|
|
)
|
|
t.save!
|
|
end
|
|
|
|
expect(SvgSprite.bundle(theme.id)).to match(/my-custom-theme-icon/)
|
|
expect(SvgSprite.bundle(theme.id)).to match(/my-other-custom-theme-icon/)
|
|
end
|
|
|
|
it "does not fail on bad XML in custom icon sprite" do
|
|
fname = "bad-xml-icon-sprite.svg"
|
|
|
|
upload = UploadCreator.new(file_from_fixtures(fname), fname, for_theme: true).create_for(-1)
|
|
|
|
theme.set_field(
|
|
target: :common,
|
|
name: SvgSprite.theme_sprite_variable_name,
|
|
upload_id: upload.id,
|
|
type: :theme_upload_var,
|
|
)
|
|
theme.save!
|
|
|
|
expect(Upload.exists?(id: upload.id)).to eq(true)
|
|
expect(SvgSprite.bundle(theme.id)).to match(/arrow-down/)
|
|
end
|
|
|
|
it "includes custom icons in a child theme" do
|
|
fname = "custom-theme-icon-sprite.svg"
|
|
child_theme = Fabricate(:theme, component: true)
|
|
theme.add_relative_theme!(:child, child_theme)
|
|
|
|
upload = UploadCreator.new(file_from_fixtures(fname), fname, for_theme: true).create_for(-1)
|
|
|
|
child_theme.set_field(
|
|
target: :common,
|
|
name: SvgSprite.theme_sprite_variable_name,
|
|
upload_id: upload.id,
|
|
type: :theme_upload_var,
|
|
)
|
|
child_theme.save!
|
|
|
|
expect(Upload.exists?(id: upload.id)).to eq(true)
|
|
expect(SvgSprite.bundle(theme.id)).to match(/my-custom-theme-icon/)
|
|
end
|
|
|
|
it "does not include theme icons if custom icon sprite is too large" do
|
|
fname = "theme-icon-sprite.svg"
|
|
symbols = ""
|
|
|
|
# should exceed MAX_THEME_SPRITE_SIZE
|
|
3500.times do |i|
|
|
id = "icon-id-#{i}"
|
|
path =
|
|
"M#{rand(1..100)} 2.18-2.087.277-4.216.42-6.378.42s-4.291-.143-6.378-.42c-1.085-.144-1.872-1.086-1.872-2.18v-4.25m16.5 0a2.18 2.18 0 00.75-1.661V8.706c0-1.081-.768-2.015-1.837-2.175a48.114 48.114 0 00-3.413-.387m4.5 8.006c-.194.165-.42.295-.673.38A23.978 23.978 0 0112 15.75c-2.648 0-5.195-.429-7.577-1.22a2.016 2.016 0 01-.673-.38m0 0A2.18 2.18 0 013 12.489V8.706c0-1.081.768-2.015 1.837-2.175a48.111 48.111 0 013.413-.387m7.5 0V5.25A2.25 2.25 0 0013.5 3h-3a2.25 .008z"
|
|
symbols += "<symbol id='#{id}' viewBox='0 0 100 100'><path d='#{path}'/></symbol>\n"
|
|
end
|
|
|
|
contents =
|
|
"<?xml version='1.0' encoding='UTF-8'?><svg><symbol id='customthemeicon' viewBox='0 0 100 100'><path d='M0 0h1ssss00v100H0z'/></symbol>#{symbols}</svg>"
|
|
|
|
child_theme = Fabricate(:theme, component: true)
|
|
theme.add_relative_theme!(:child, child_theme)
|
|
|
|
upload =
|
|
UploadCreator.new(file_from_contents(contents, fname), fname, for_theme: true).create_for(
|
|
-1,
|
|
)
|
|
|
|
child_theme.set_field(
|
|
target: :common,
|
|
name: SvgSprite.theme_sprite_variable_name,
|
|
upload_id: upload.id,
|
|
type: :theme_upload_var,
|
|
)
|
|
child_theme.save!
|
|
|
|
expect(Upload.exists?(id: upload.id)).to eq(true)
|
|
expect(SvgSprite.bundle(theme.id)).not_to match(/customthemeicon/)
|
|
end
|
|
end
|
|
end
|