# frozen_string_literal: true RSpec.describe SvgSprite do fab!(:theme) before do SvgSprite.clear_plugin_svg_sprite_cache! SvgSprite.expire_cache 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: "fa-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-facebook-messenger") expect(SvgSprite.all_icons).not_to include(" fab-facebook-messenger ") end it "includes Font Awesome 5 icons from badges" do Fabricate(:badge, name: "Custom Icon Badge", icon: "far fa-building") expect(SvgSprite.all_icons).to include("far-building") 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-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 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-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 += "\n" end contents = "#{symbols}" 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