From 9e14f437b6ed205744d916f5566ee2c11e52b734 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Wed, 6 Dec 2017 21:04:53 +0100 Subject: [PATCH] create favicon overlay on the client the initial reason for this change was that graphicsmagick does not support writing to ico files. this fact lead to a chain of changes: 1. use png instead of ico (browser support is good enough) 2. render the overlays on the client using the canvas API. this way we only need to store the original favion and generate the overlay versions dynamically. this change also enables (next step) to simplify the handling of the stock favicons as well, as we don't need to generate all the versions upfront. --- app/assets/javascripts/favicon_admin.js | 19 +++++++ .../javascripts/lib/utils/common_utils.js | 50 +++++++++++++++++-- .../mr_widget_options.vue | 5 +- app/serializers/status_entity.rb | 2 +- app/uploaders/favicon_uploader.rb | 37 +------------- app/views/admin/appearances/_form.html.haml | 7 ++- app/views/layouts/_head.html.haml | 2 +- lib/gitlab/favicon.rb | 31 ++++++++---- spec/features/admin/admin_appearance_spec.rb | 27 ++-------- .../lib/utils/common_utils_spec.js | 36 +++++++++++-- spec/javascripts/lib/utils/mock_data.js | 5 ++ .../vue_mr_widget/mr_widget_options_spec.js | 13 +++-- spec/lib/gitlab/favicon_spec.rb | 30 ++++++++--- spec/uploaders/favicon_uploader_spec.rb | 11 +--- 14 files changed, 168 insertions(+), 107 deletions(-) create mode 100644 app/assets/javascripts/favicon_admin.js create mode 100644 spec/javascripts/lib/utils/mock_data.js diff --git a/app/assets/javascripts/favicon_admin.js b/app/assets/javascripts/favicon_admin.js new file mode 100644 index 00000000000..6b2dcf4502e --- /dev/null +++ b/app/assets/javascripts/favicon_admin.js @@ -0,0 +1,19 @@ +import {createOverlayIcon} from '~/lib/utils/common_utils'; + +export default class FaviconAdmin { + constructor() { + const faviconContainer = $('.js-favicons'); + const faviconUrl = faviconContainer.data('favicon'); + const overlayUrls = faviconContainer.data('status-overlays'); + + overlayUrls.forEach((statusOverlay) => { + createOverlayIcon(faviconUrl, statusOverlay).then((faviconWithOverlayUrl) => { + const image = $(''); + image.addClass('appearance-light-logo-preview'); + image.attr('src', faviconWithOverlayUrl); + + faviconContainer.append(image); + }); + }); + } +} diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 310b731c6d8..d55d0585031 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -384,6 +384,49 @@ export const backOff = (fn, timeout = 60000) => { }); }; +export const createOverlayIcon = (iconPath, overlayPath) => { + const faviconImage = document.createElement('img'); + + return new Promise((resolve) => { + faviconImage.onload = () => { + const size = 32; + + const canvas = document.createElement('canvas'); + canvas.width = size; + canvas.height = size; + + const context = canvas.getContext('2d'); + context.clearRect(0, 0, size, size); + context.drawImage( + faviconImage, 0, 0, faviconImage.width, faviconImage.height, 0, 0, size, size, + ); + + const overlayImage = document.createElement('img'); + overlayImage.onload = () => { + context.drawImage( + overlayImage, 0, 0, overlayImage.width, overlayImage.height, 0, 0, size, size, + ); + + const faviconWithOverlayUrl = canvas.toDataURL(); + + resolve(faviconWithOverlayUrl); + }; + overlayImage.src = overlayPath; + }; + faviconImage.src = iconPath; + }); +}; + +export const setFaviconOverlay = (overlayPath) => { + const faviconEl = document.getElementById('favicon'); + + if (!faviconEl) { return null; } + + const iconPath = faviconEl.getAttribute('data-original-href'); + + return createOverlayIcon(iconPath, overlayPath).then(faviconWithOverlayUrl => faviconEl.setAttribute('href', faviconWithOverlayUrl)); +}; + export const setFavicon = (faviconPath) => { const faviconEl = document.getElementById('favicon'); if (faviconEl && faviconPath) { @@ -395,7 +438,7 @@ export const resetFavicon = () => { const faviconEl = document.getElementById('favicon'); if (faviconEl) { - const originalFavicon = faviconEl.getAttribute('data-default-href'); + const originalFavicon = faviconEl.getAttribute('data-original-href'); faviconEl.setAttribute('href', originalFavicon); } }; @@ -404,10 +447,9 @@ export const setCiStatusFavicon = pageUrl => axios.get(pageUrl) .then(({ data }) => { if (data && data.favicon) { - setFavicon(data.favicon); - } else { - resetFavicon(); + return setFaviconOverlay(data.favicon); } + return resetFavicon(); }) .catch(resetFavicon); diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue index f69fe03fcb3..d6cba878b28 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue @@ -36,7 +36,7 @@ import { notify, SourceBranchRemovalStatus, } from './dependencies'; -import { setFavicon } from '../lib/utils/common_utils'; +import { setFaviconOverlay } from '../lib/utils/common_utils'; export default { el: '#js-vue-mr-widget', @@ -159,8 +159,9 @@ export default { }, setFaviconHelper() { if (this.mr.ciStatusFaviconPath) { - setFavicon(this.mr.ciStatusFaviconPath); + return setFaviconOverlay(this.mr.ciStatusFaviconPath); } + return Promise.resolve(); }, fetchDeployments() { return this.service.fetchDeployments() diff --git a/app/serializers/status_entity.rb b/app/serializers/status_entity.rb index 2f3d4b80565..47df7f9dcf9 100644 --- a/app/serializers/status_entity.rb +++ b/app/serializers/status_entity.rb @@ -7,7 +7,7 @@ class StatusEntity < Grape::Entity expose :details_path expose :favicon do |status| - Gitlab::Favicon.status(status.favicon) + Gitlab::Favicon.status_overlay(status.favicon) end expose :action, if: -> (status, _) { status.has_action? } do diff --git a/app/uploaders/favicon_uploader.rb b/app/uploaders/favicon_uploader.rb index 7697f5fe885..d7be77477b2 100644 --- a/app/uploaders/favicon_uploader.rb +++ b/app/uploaders/favicon_uploader.rb @@ -1,35 +1,12 @@ class FaviconUploader < AttachmentUploader include CarrierWave::MiniMagick - STATUS_ICON_NAMES = [ - :favicon_status_canceled, - :favicon_status_created, - :favicon_status_failed, - :favicon_status_manual, - :favicon_status_not_found, - :favicon_status_pending, - :favicon_status_running, - :favicon_status_skipped, - :favicon_status_success, - :favicon_status_warning - ].freeze - version :favicon_main do process resize_to_fill: [32, 32] - process convert: 'ico' + process convert: 'png' def full_filename(filename) - filename_for_different_format(super(filename), 'ico') - end - end - - STATUS_ICON_NAMES.each do |status_name| - version status_name, from_version: :favicon_main do - process status_favicon: status_name - - def full_filename(filename) - filename_for_different_format(super(filename), 'ico') - end + filename_for_different_format(super(filename), 'png') end end @@ -39,16 +16,6 @@ class FaviconUploader < AttachmentUploader private - def status_favicon(status_name) - manipulate! do |img| - overlay_path = Rails.root.join("app/assets/images/ci_favicons/overlays/#{status_name}.png") - overlay = MiniMagick::Image.open(overlay_path) - img.composite(overlay) do |c| - c.compose 'over' - end - end - end - def filename_for_different_format(filename, format) filename.chomp(File.extname(filename)) + ".#{format}" end diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml index 1119c75637c..f77e22bcc45 100644 --- a/app/views/admin/appearances/_form.html.haml +++ b/app/views/admin/appearances/_form.html.haml @@ -62,13 +62,12 @@ = f.label :favicon, 'Favicon', class: 'control-label' .col-sm-10 - if @appearance.favicon? - = image_tag @appearance.favicon.favicon_main.url, class: 'appearance-light-logo-preview' + = image_tag @appearance.favicon.favicon_main.url, class: 'appearance-light-logo-preview js-main-favicon' - if @appearance.favicon? - = f.label :favicon, 'Generated status icons', class: 'control-label' + = f.label :favicon, 'Status icons preview', class: 'control-label' .col-sm-10 - if @appearance.favicon? - - FaviconUploader::STATUS_ICON_NAMES.each do |status_name| - = image_tag @appearance.favicon.public_send(status_name).url, class: 'appearance-light-logo-preview' + .js-favicons{ data: { favicon: @appearance.favicon.favicon_main.url, status_overlays: Gitlab::Favicon.available_status_overlays } } - if @appearance.persisted? %br = link_to 'Remove favicon', favicon_admin_appearances_path, data: { confirm: "Favicon will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-logo" diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index 024f80e9935..9253a0652da 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -25,7 +25,7 @@ %title= page_title(site_name) %meta{ name: "description", content: page_description } - = favicon_link_tag favicon, id: 'favicon', :'data-default-href': favicon + = favicon_link_tag favicon, id: 'favicon', data: { original_href: favicon }, type: 'image/png' = stylesheet_link_tag "application", media: "all" = stylesheet_link_tag "print", media: "print" diff --git a/lib/gitlab/favicon.rb b/lib/gitlab/favicon.rb index 51a25b408ee..e28d4c67661 100644 --- a/lib/gitlab/favicon.rb +++ b/lib/gitlab/favicon.rb @@ -9,18 +9,27 @@ module Gitlab 'favicon.ico' end - def status(status_name) - if appearance_favicon.exists? - custom_favicon_url(appearance_favicon.public_send("#{status_name}").url) # rubocop:disable GitlabSecurity/PublicSend - else - path = File.join( - 'ci_favicons', - Rails.env.development? ? 'dev' : '', - Gitlab::Utils.to_boolean(ENV['CANARY']) ? 'canary' : '', - "#{status_name}.ico" - ) + def status_overlay(status_name) + path = File.join( + 'ci_favicons', + 'overlays', + "#{status_name}.png" + ) - ActionController::Base.helpers.image_path(path) + ActionController::Base.helpers.image_path(path) + end + + def available_status_overlays + available_status_names.map do |status_name| + status_overlay(status_name) + end + end + + def available_status_names + @available_status_names ||= begin + Dir.glob(Rails.root.join('app', 'assets', 'images', 'ci_favicons', 'overlays', "*.png")) + .map { |file| File.basename(file, '.png') } + .sort end end diff --git a/spec/features/admin/admin_appearance_spec.rb b/spec/features/admin/admin_appearance_spec.rb index ffffd14752e..0ac4f111c52 100644 --- a/spec/features/admin/admin_appearance_spec.rb +++ b/spec/features/admin/admin_appearance_spec.rb @@ -76,38 +76,19 @@ feature 'Admin Appearance' do expect(page).not_to have_css(header_logo_selector) end - scenario 'Favicon' do + scenario 'Favicon', :js do sign_in(create(:admin)) visit admin_appearances_path attach_file(:appearance_favicon, logo_fixture) click_button 'Save' - expect(page).to have_css('//img[data-src$="/default_dk.ico"]') - expect(page).to have_css('//img[data-src$="/status_canceled_dk.ico"]') - expect(page).to have_css('//img[data-src$="/status_created_dk.ico"]') - expect(page).to have_css('//img[data-src$="/status_failed_dk.ico"]') - expect(page).to have_css('//img[data-src$="/status_manual_dk.ico"]') - expect(page).to have_css('//img[data-src$="/status_not_found_dk.ico"]') - expect(page).to have_css('//img[data-src$="/status_pending_dk.ico"]') - expect(page).to have_css('//img[data-src$="/status_running_dk.ico"]') - expect(page).to have_css('//img[data-src$="/status_skipped_dk.ico"]') - expect(page).to have_css('//img[data-src$="/status_success_dk.ico"]') - expect(page).to have_css('//img[data-src$="/status_warning_dk.ico"]') + # 11 = 1 original + 10 overlay variations + expect(page).to have_css('.appearance-light-logo-preview', count: 11) click_link 'Remove favicon' - expect(page).not_to have_css('//img[data-src$="/default_dk.ico"]') - expect(page).not_to have_css('//img[data-src$="/status_canceled_dk.ico"]') - expect(page).not_to have_css('//img[data-src$="/status_created_dk.ico"]') - expect(page).not_to have_css('//img[data-src$="/status_failed_dk.ico"]') - expect(page).not_to have_css('//img[data-src$="/status_manual_dk.ico"]') - expect(page).not_to have_css('//img[data-src$="/status_not_found_dk.ico"]') - expect(page).not_to have_css('//img[data-src$="/status_pending_dk.ico"]') - expect(page).not_to have_css('//img[data-src$="/status_running_dk.ico"]') - expect(page).not_to have_css('//img[data-src$="/status_skipped_dk.ico"]') - expect(page).not_to have_css('//img[data-src$="/status_success_dk.ico"]') - expect(page).not_to have_css('//img[data-src$="/status_warning_dk.ico"]') + expect(page).not_to have_css('.appearance-light-logo-preview') # allowed file types attach_file(:appearance_favicon, Rails.root.join('spec', 'fixtures', 'sanitized.svg')) diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index 64d13275a59..2d7cc3443cf 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -2,6 +2,7 @@ import axios from '~/lib/utils/axios_utils'; import * as commonUtils from '~/lib/utils/common_utils'; import MockAdapter from 'axios-mock-adapter'; +import { faviconDataUrl, overlayDataUrl, faviconWithOverlayDataUrl } from './mock_data'; describe('common_utils', () => { describe('parseUrl', () => { @@ -430,6 +431,35 @@ describe('common_utils', () => { }); }); + describe('createOverlayIcon', () => { + it('should return the favicon with the overlay', (done) => { + commonUtils.createOverlayIcon(faviconDataUrl, overlayDataUrl).then((url) => { + expect(url).toEqual(faviconWithOverlayDataUrl); + done(); + }); + }); + }); + + describe('setFaviconOverlay', () => { + beforeEach(() => { + const favicon = document.createElement('link'); + favicon.setAttribute('id', 'favicon'); + favicon.setAttribute('data-original-href', faviconDataUrl); + document.body.appendChild(favicon); + }); + + afterEach(() => { + document.body.removeChild(document.getElementById('favicon')); + }); + + it('should set page favicon to provided favicon overlay', (done) => { + commonUtils.setFaviconOverlay(overlayDataUrl).then(() => { + expect(document.getElementById('favicon').getAttribute('href')).toEqual(faviconWithOverlayDataUrl); + done(); + }); + }); + }); + describe('setCiStatusFavicon', () => { const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1/status.json`; let mock; @@ -463,16 +493,14 @@ describe('common_utils', () => { }); it('should set page favicon to CI status favicon based on provided status', (done) => { - const FAVICON_PATH = '//icon_status_success'; - mock.onGet(BUILD_URL).reply(200, { - favicon: FAVICON_PATH, + favicon: overlayDataUrl, }); commonUtils.setCiStatusFavicon(BUILD_URL) .then(() => { const favicon = document.getElementById('favicon'); - expect(favicon.getAttribute('href')).toEqual(FAVICON_PATH); + expect(favicon.getAttribute('href')).toEqual(faviconWithOverlayDataUrl); done(); }) .catch(done.fail); diff --git a/spec/javascripts/lib/utils/mock_data.js b/spec/javascripts/lib/utils/mock_data.js new file mode 100644 index 00000000000..fd0d62b751f --- /dev/null +++ b/spec/javascripts/lib/utils/mock_data.js @@ -0,0 +1,5 @@ +export const faviconDataUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAACcFBMVEX////iQyniQyniQyniQyniQyniQyniQyniQynhRiriQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniRCniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQynhQiniQiniQiniQinhQinpUSjqUSjqTyjqTyjqTyjlSCniRCniQynjRCjqTyjsZSjrWyj8oib9kSb8pyb9pib8oyb8fyb3ZSb4Zib8fCb8oyb8oyb8oyb8pCb8cSbiQyn7bCb8cib8oyb8oSb8bSbtVSjpTij8nyb8oyb8oyb8lCb2Yyf3ZCf8mCb8oyb8oyb8oyb8iib8bSbiRCn8gyb8oyb8eCbpTinrUSj8oyb8oyb8oyb8pSb8bib4Zif0YCf8byb8oyb8oyb8oyb7oib8oyb8nCbjRSn9bib8ayb8nib8oyb8oyb8oyb8kSbpTyjpTyj8jib8oyb8oyb8oyb8fib0Xyf2ZSb8gCb8oyb6pSb8oyb8dib+cCbgQCnjRSn8cCb8oib8oyb8oyb8oybqUCjnSyn8bCb8oyb8oyb8oyb8myb2YyfyXyf8oyb8oyb8hibhQSn+bib8iSb8oyb8qCb+fSbmSSnqTyj8oib9pCb1YifxXyf7pSb8oCb8pCb+mCb0fCf8pSb7hSXvcSjiQyniQinqTyj9kCb9bib9byb+cCbqUSjiRCnsVCj+cSb8pib8bCb8bSbgQCn7bCb8bibjRSn8oyb8ayb8oib8aib8pCbjRCn8pybhQinhQSn8pSb7ayb7aSb6aib8eib///8IbM+7AAAAr3RSTlMBA3NtX2vT698HGQcRLwWLiXnv++3V+eEd/R8HE2V/Y5HjyefdFw99YWfJ+/3nwQP78/HvX1VTQ/kdA2HzbQXj9fX79/3DGf379/33T/v99/f7ba33+/f1+9/18/v59V339flzF/H9+fX3/fMhBwOh9/v5/fmvBV/z+fP3Awnp9/f38+UFgff7+/37+4c77/f7/flFz/f59dFr7/v98Wnr+/f3I5/197EDBU1ZAwUD8/kLUwAAAAFiS0dEAIgFHUgAAAAHdElNRQfhBQoLHiBV6/1lAAACHUlEQVQ4y41TZXsTQRCe4FAIUigN7m7FXY+iLRQKBG2x4g7BjhZ3Le7uMoEkFJprwyQk0CC/iZnNhUZaHt4vt6/szO7cHcD/wFKjZrJWq3YMq1M3eVc9rFzXR2yQkuA3RGxkjZLGiEk9miA2tURJs1RsnhhokYYtzaU13WZDbBVnW1sjo43J2vI6tZ0lLtFeAh1M0lECneI7dGYtrUtk3RUVIKaEJR25qw27yT0s3W0qEHuPlB4RradivXo7GX36xnbo51SQ+fWHARmCgYMGDxkaxbD3SssYPmIkwKgPLrfA87EETTg/fVaSa/SYsQDjSsd7DcGEsr+BieVKmaRNBsjUtClTfUI900y/5Mt05c8oJQKYSURZ2UqYFa0w283M588JEM2BuRwI5EqT8nmmXzZf4l8XsGNfCIv4QcHFklhiBpaqAsuC4tghj+ySyOdjeJYrP7RCCuR/E5tWAqxaLcmCNSyujdxjHZdbn8UHoA0bN/GoNm8hjQJb/ZzYpo6w3TB27JRduxxqrA7YzbWCezixN8RD2Oc2/Ptlfx7o5uT1A4XMiwzj4HfEikNe7+Ew0ZGjeuW70eEYaeHjxomTiKd++E4XnKGz8d+HDufOB3Ky3RcwdNF1qZiKLyf/B44r2tWf15wV143cwI2qfi8dbtKtX6Hbd+6G74EDqkTm/QcPH/0ufFyNLXjy9NnzF9Xb8BJevYY38C+8fZcg/AF3QTYemVkCwwAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxNy0wNS0xMFQxMTozMDozMiswMjowMMzup8UAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTctMDUtMTBUMTE6MzA6MzIrMDI6MDC9sx95AAAAAElFTkSuQmCC'; + +export const overlayDataUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAA85JREFUWAntVllIVGEUPv/9b46O41KplYN7PeRkti8TjQlhCUGh3MmeQugpIsGKAi2soIcIooiohxYKK2daqDAlIpIiWwxtQaJcaHE0d5tMrbn37z9XRqfR0TvVW56Hudf//uec72zfEWBCJjIwkYGJDPzvGSD/KgExN3Oi2Q+2DJgSDYQEMwItVGH1iZGmJw/Si1y+/PwVAMYYib22MYc/8hVQFgKDEfYoId0KYzagAQebsos/ewMZoeB9wdffcTYpQSaCTWHKoqSQaDk7zkIt0+aCUR8BelEHrf3dUNv9AcqbnsHtT5UKB/hTASh0SLYjnjb/CIDRJi0XiFAaJOpCD8zLpdb4NB66b1OfelthX815dtdRRfiti2aAXLvVLiMQ6olGyztGDkSo4JGGXk8/QFdGpYzpHG2GBQTDhtgVhPEaVbbVpvI6GJz22rv4TcAfrYI1x7Rj5MWWAppomKFVVb2302SFzUkZHAbkG+0b1+Gh77yNYjrmqnWTrLBLRxdvBWv8qlFujH/kYjJYyvLkj71t78zAUvzMAMnHhpN4zf9UREJhd8omyssxu1IgazQDwDnHUcNuH6vhPIE1fmuBzHt74Hn7W89jWGtcAjoaIDOFrdcMYJBkgOCoaRF0Lj0oglddDbCj6tRvKjphEpgjkzEQs2YAKsNxMzjn3nKurhzK+Ly7xe28ua8TwgMMcHJZnvvT0BPtEEKM4tDJ+C8GvIIk4ylINIXVZ0EUKJxYuh3mhCeokbudl6TtVc88dfBdLwbyaWB6zQCYQJpBYSrDGQxBQ/ZWRM2B+VNmQnVnHWx7elyNuL2/R336co7KyJR8CL9oLgEuFlREevWUkEl6uGwpVEG4FBm0OEf9N10NMgPlvWYAuNVwsWDKvcUNYsHUWTCZ13ysyFEXe6TO6aC8CUr9IiK+A05TQrc8yjwmxARHeeMAPlfQJw+AQRwu0YhL/GDXi9NwufG+S8dYkuYMqIb4SsWthotlNMOUCOM6r+G9cqXxPmd1dqrBav/o1zJy2l5/NUjJA/VORwYuFnOUaTQcPs9wMqwV++Xv8oADxKAcZ8nLPr8AoGW+xR6HSqYk3GodAz2QNj0V+Gr26dT9ASNH5239Pf0gktVNWZca8ZvfAFBprWS6hSu1pqt++Y0PD+WIwDAhIWQGtzvSHDbcodfFUFB9hg1Gjs5LXqIdFL+acFBl+FddqYwdxsWC3I70OvgfUaA65zhq2O2c8VxYcyIGFTVlXegYtvCXANCQZJMobjVcLMjtSK/IcEgyOOe8Ve5w7ryKDefp2P3+C/5ohv8HZmVLAAAAAElFTkSuQmCC'; + +export const faviconWithOverlayDataUrl = 'data:image/png;base64,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'; diff --git a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js index 30918428da2..6342ea00436 100644 --- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js +++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js @@ -5,6 +5,7 @@ import notify from '~/lib/utils/notify'; import { stateKey } from '~/vue_merge_request_widget/stores/state_maps'; import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mockData from './mock_data'; +import { faviconDataUrl, overlayDataUrl, faviconWithOverlayDataUrl } from '../lib/utils/mock_data'; const returnPromise = data => new Promise((resolve) => { resolve({ @@ -273,6 +274,7 @@ describe('mrWidgetOptions', () => { beforeEach(() => { const favicon = document.createElement('link'); favicon.setAttribute('id', 'favicon'); + favicon.setAttribute('data-original-href', faviconDataUrl); document.body.appendChild(favicon); faviconElement = document.getElementById('favicon'); @@ -282,10 +284,13 @@ describe('mrWidgetOptions', () => { document.body.removeChild(document.getElementById('favicon')); }); - it('should call setFavicon method', () => { - vm.setFaviconHelper(); - - expect(faviconElement.getAttribute('href')).toEqual(vm.mr.ciStatusFaviconPath); + it('should call setFavicon method', (done) => { + vm.mr.ciStatusFaviconPath = overlayDataUrl; + vm.setFaviconHelper().then(() => { + expect(faviconElement.getAttribute('href')).toEqual(faviconWithOverlayDataUrl); + done(); + }) + .catch(done.fail); }); it('should not call setFavicon when there is no ciStatusFaviconPath', () => { diff --git a/spec/lib/gitlab/favicon_spec.rb b/spec/lib/gitlab/favicon_spec.rb index 51b8fda81d1..22b9c631ed8 100644 --- a/spec/lib/gitlab/favicon_spec.rb +++ b/spec/lib/gitlab/favicon_spec.rb @@ -19,20 +19,34 @@ RSpec.describe Gitlab::Favicon, :request_store do it 'uses the custom favicon if a favicon appearance is present' do create :appearance, favicon: fixture_file_upload(Rails.root.join('spec/fixtures/dk.png')) - expect(described_class.main).to match %r{/uploads/-/system/appearance/favicon/\d+/favicon_main_dk.ico} + expect(described_class.main).to match %r{/uploads/-/system/appearance/favicon/\d+/favicon_main_dk.png} end end - describe '.status' do - subject { described_class.status('favicon_status_created') } + describe '.status_overlay' do + subject { described_class.status_overlay('favicon_status_created') } - it 'defaults to the stock icon' do - expect(subject).to eq '/assets/ci_favicons/favicon_status_created.ico' + it 'returns the overlay for the status' do + expect(subject).to eq '/assets/ci_favicons/overlays/favicon_status_created.png' end + end - it 'uses the custom favicon if a favicon appearance is present' do - create :appearance, favicon: fixture_file_upload(Rails.root.join('spec/fixtures/dk.png')) - expect(subject).to match(%r{/uploads/-/system/appearance/favicon/\d+/favicon_status_created_dk.ico}) + describe '.available_status_names' do + subject { described_class.available_status_names } + + it 'returns the available status names' do + expect(subject).to eq %w( + favicon_status_canceled + favicon_status_created + favicon_status_failed + favicon_status_manual + favicon_status_not_found + favicon_status_pending + favicon_status_running + favicon_status_skipped + favicon_status_success + favicon_status_warning + ) end end end diff --git a/spec/uploaders/favicon_uploader_spec.rb b/spec/uploaders/favicon_uploader_spec.rb index b521670addb..db8a3207f4d 100644 --- a/spec/uploaders/favicon_uploader_spec.rb +++ b/spec/uploaders/favicon_uploader_spec.rb @@ -19,20 +19,11 @@ RSpec.describe FaviconUploader do end it 'has the correct format' do - expect(uploader.favicon_main).to be_format('ico') + expect(uploader.favicon_main).to be_format('png') end it 'has the correct dimensions' do expect(uploader.favicon_main).to have_dimensions(32, 32) end - - it 'generates all the status icons' do - # make sure that the following each statement actually loops - expect(FaviconUploader::STATUS_ICON_NAMES.count).to eq 10 - - FaviconUploader::STATUS_ICON_NAMES.each do |status_name| - expect(File.exist?(uploader.favicon_status_not_found.file.file)).to be true - end - end end end