Commit 9e14f437 authored by Alexis Reigel's avatar Alexis Reigel Committed by Alexis Reigel

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.
parent 5202c3f0
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 = $('<img />');
image.addClass('appearance-light-logo-preview');
image.attr('src', faviconWithOverlayUrl);
faviconContainer.append(image);
});
});
}
}
...@@ -384,6 +384,49 @@ export const backOff = (fn, timeout = 60000) => { ...@@ -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) => { export const setFavicon = (faviconPath) => {
const faviconEl = document.getElementById('favicon'); const faviconEl = document.getElementById('favicon');
if (faviconEl && faviconPath) { if (faviconEl && faviconPath) {
...@@ -395,7 +438,7 @@ export const resetFavicon = () => { ...@@ -395,7 +438,7 @@ export const resetFavicon = () => {
const faviconEl = document.getElementById('favicon'); const faviconEl = document.getElementById('favicon');
if (faviconEl) { if (faviconEl) {
const originalFavicon = faviconEl.getAttribute('data-default-href'); const originalFavicon = faviconEl.getAttribute('data-original-href');
faviconEl.setAttribute('href', originalFavicon); faviconEl.setAttribute('href', originalFavicon);
} }
}; };
...@@ -404,10 +447,9 @@ export const setCiStatusFavicon = pageUrl => ...@@ -404,10 +447,9 @@ export const setCiStatusFavicon = pageUrl =>
axios.get(pageUrl) axios.get(pageUrl)
.then(({ data }) => { .then(({ data }) => {
if (data && data.favicon) { if (data && data.favicon) {
setFavicon(data.favicon); return setFaviconOverlay(data.favicon);
} else {
resetFavicon();
} }
return resetFavicon();
}) })
.catch(resetFavicon); .catch(resetFavicon);
......
...@@ -36,7 +36,7 @@ import { ...@@ -36,7 +36,7 @@ import {
notify, notify,
SourceBranchRemovalStatus, SourceBranchRemovalStatus,
} from './dependencies'; } from './dependencies';
import { setFavicon } from '../lib/utils/common_utils'; import { setFaviconOverlay } from '../lib/utils/common_utils';
export default { export default {
el: '#js-vue-mr-widget', el: '#js-vue-mr-widget',
...@@ -159,8 +159,9 @@ export default { ...@@ -159,8 +159,9 @@ export default {
}, },
setFaviconHelper() { setFaviconHelper() {
if (this.mr.ciStatusFaviconPath) { if (this.mr.ciStatusFaviconPath) {
setFavicon(this.mr.ciStatusFaviconPath); return setFaviconOverlay(this.mr.ciStatusFaviconPath);
} }
return Promise.resolve();
}, },
fetchDeployments() { fetchDeployments() {
return this.service.fetchDeployments() return this.service.fetchDeployments()
......
...@@ -7,7 +7,7 @@ class StatusEntity < Grape::Entity ...@@ -7,7 +7,7 @@ class StatusEntity < Grape::Entity
expose :details_path expose :details_path
expose :favicon do |status| expose :favicon do |status|
Gitlab::Favicon.status(status.favicon) Gitlab::Favicon.status_overlay(status.favicon)
end end
expose :action, if: -> (status, _) { status.has_action? } do expose :action, if: -> (status, _) { status.has_action? } do
......
class FaviconUploader < AttachmentUploader class FaviconUploader < AttachmentUploader
include CarrierWave::MiniMagick 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 version :favicon_main do
process resize_to_fill: [32, 32] process resize_to_fill: [32, 32]
process convert: 'ico' process convert: 'png'
def full_filename(filename) def full_filename(filename)
filename_for_different_format(super(filename), 'ico') filename_for_different_format(super(filename), 'png')
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
end end
end end
...@@ -39,16 +16,6 @@ class FaviconUploader < AttachmentUploader ...@@ -39,16 +16,6 @@ class FaviconUploader < AttachmentUploader
private 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) def filename_for_different_format(filename, format)
filename.chomp(File.extname(filename)) + ".#{format}" filename.chomp(File.extname(filename)) + ".#{format}"
end end
......
...@@ -62,13 +62,12 @@ ...@@ -62,13 +62,12 @@
= f.label :favicon, 'Favicon', class: 'control-label' = f.label :favicon, 'Favicon', class: 'control-label'
.col-sm-10 .col-sm-10
- if @appearance.favicon? - 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? - if @appearance.favicon?
= f.label :favicon, 'Generated status icons', class: 'control-label' = f.label :favicon, 'Status icons preview', class: 'control-label'
.col-sm-10 .col-sm-10
- if @appearance.favicon? - if @appearance.favicon?
- FaviconUploader::STATUS_ICON_NAMES.each do |status_name| .js-favicons{ data: { favicon: @appearance.favicon.favicon_main.url, status_overlays: Gitlab::Favicon.available_status_overlays } }
= image_tag @appearance.favicon.public_send(status_name).url, class: 'appearance-light-logo-preview'
- if @appearance.persisted? - if @appearance.persisted?
%br %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" = 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"
......
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
%title= page_title(site_name) %title= page_title(site_name)
%meta{ name: "description", content: page_description } %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 "application", media: "all"
= stylesheet_link_tag "print", media: "print" = stylesheet_link_tag "print", media: "print"
......
...@@ -9,18 +9,27 @@ module Gitlab ...@@ -9,18 +9,27 @@ module Gitlab
'favicon.ico' 'favicon.ico'
end end
def status(status_name) def status_overlay(status_name)
if appearance_favicon.exists? path = File.join(
custom_favicon_url(appearance_favicon.public_send("#{status_name}").url) # rubocop:disable GitlabSecurity/PublicSend 'ci_favicons',
else 'overlays',
path = File.join( "#{status_name}.png"
'ci_favicons', )
Rails.env.development? ? 'dev' : '',
Gitlab::Utils.to_boolean(ENV['CANARY']) ? 'canary' : '', ActionController::Base.helpers.image_path(path)
"#{status_name}.ico" end
)
def available_status_overlays
ActionController::Base.helpers.image_path(path) 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
end end
......
...@@ -76,38 +76,19 @@ feature 'Admin Appearance' do ...@@ -76,38 +76,19 @@ feature 'Admin Appearance' do
expect(page).not_to have_css(header_logo_selector) expect(page).not_to have_css(header_logo_selector)
end end
scenario 'Favicon' do scenario 'Favicon', :js do
sign_in(create(:admin)) sign_in(create(:admin))
visit admin_appearances_path visit admin_appearances_path
attach_file(:appearance_favicon, logo_fixture) attach_file(:appearance_favicon, logo_fixture)
click_button 'Save' click_button 'Save'
expect(page).to have_css('//img[data-src$="/default_dk.ico"]') # 11 = 1 original + 10 overlay variations
expect(page).to have_css('//img[data-src$="/status_canceled_dk.ico"]') expect(page).to have_css('.appearance-light-logo-preview', count: 11)
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"]')
click_link 'Remove favicon' click_link 'Remove favicon'
expect(page).not_to have_css('//img[data-src$="/default_dk.ico"]') expect(page).not_to have_css('.appearance-light-logo-preview')
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"]')
# allowed file types # allowed file types
attach_file(:appearance_favicon, Rails.root.join('spec', 'fixtures', 'sanitized.svg')) attach_file(:appearance_favicon, Rails.root.join('spec', 'fixtures', 'sanitized.svg'))
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import * as commonUtils from '~/lib/utils/common_utils'; import * as commonUtils from '~/lib/utils/common_utils';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import { faviconDataUrl, overlayDataUrl, faviconWithOverlayDataUrl } from './mock_data';
describe('common_utils', () => { describe('common_utils', () => {
describe('parseUrl', () => { describe('parseUrl', () => {
...@@ -430,6 +431,35 @@ describe('common_utils', () => { ...@@ -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', () => { describe('setCiStatusFavicon', () => {
const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1/status.json`; const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1/status.json`;
let mock; let mock;
...@@ -463,16 +493,14 @@ describe('common_utils', () => { ...@@ -463,16 +493,14 @@ describe('common_utils', () => {
}); });
it('should set page favicon to CI status favicon based on provided status', (done) => { 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, { mock.onGet(BUILD_URL).reply(200, {
favicon: FAVICON_PATH, favicon: overlayDataUrl,
}); });
commonUtils.setCiStatusFavicon(BUILD_URL) commonUtils.setCiStatusFavicon(BUILD_URL)
.then(() => { .then(() => {
const favicon = document.getElementById('favicon'); const favicon = document.getElementById('favicon');
expect(favicon.getAttribute('href')).toEqual(FAVICON_PATH); expect(favicon.getAttribute('href')).toEqual(faviconWithOverlayDataUrl);
done(); done();
}) })
.catch(done.fail); .catch(done.fail);
......
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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';
...@@ -5,6 +5,7 @@ import notify from '~/lib/utils/notify'; ...@@ -5,6 +5,7 @@ import notify from '~/lib/utils/notify';
import { stateKey } from '~/vue_merge_request_widget/stores/state_maps'; import { stateKey } from '~/vue_merge_request_widget/stores/state_maps';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mountComponent from 'spec/helpers/vue_mount_component_helper';
import mockData from './mock_data'; import mockData from './mock_data';
import { faviconDataUrl, overlayDataUrl, faviconWithOverlayDataUrl } from '../lib/utils/mock_data';
const returnPromise = data => new Promise((resolve) => { const returnPromise = data => new Promise((resolve) => {
resolve({ resolve({
...@@ -273,6 +274,7 @@ describe('mrWidgetOptions', () => { ...@@ -273,6 +274,7 @@ describe('mrWidgetOptions', () => {
beforeEach(() => { beforeEach(() => {
const favicon = document.createElement('link'); const favicon = document.createElement('link');
favicon.setAttribute('id', 'favicon'); favicon.setAttribute('id', 'favicon');
favicon.setAttribute('data-original-href', faviconDataUrl);
document.body.appendChild(favicon); document.body.appendChild(favicon);
faviconElement = document.getElementById('favicon'); faviconElement = document.getElementById('favicon');
...@@ -282,10 +284,13 @@ describe('mrWidgetOptions', () => { ...@@ -282,10 +284,13 @@ describe('mrWidgetOptions', () => {
document.body.removeChild(document.getElementById('favicon')); document.body.removeChild(document.getElementById('favicon'));
}); });
it('should call setFavicon method', () => { it('should call setFavicon method', (done) => {
vm.setFaviconHelper(); vm.mr.ciStatusFaviconPath = overlayDataUrl;
vm.setFaviconHelper().then(() => {
expect(faviconElement.getAttribute('href')).toEqual(vm.mr.ciStatusFaviconPath); expect(faviconElement.getAttribute('href')).toEqual(faviconWithOverlayDataUrl);
done();
})
.catch(done.fail);
}); });
it('should not call setFavicon when there is no ciStatusFaviconPath', () => { it('should not call setFavicon when there is no ciStatusFaviconPath', () => {
......
...@@ -19,20 +19,34 @@ RSpec.describe Gitlab::Favicon, :request_store do ...@@ -19,20 +19,34 @@ RSpec.describe Gitlab::Favicon, :request_store do
it 'uses the custom favicon if a favicon appearance is present' 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')) 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
end end
describe '.status' do describe '.status_overlay' do
subject { described_class.status('favicon_status_created') } subject { described_class.status_overlay('favicon_status_created') }
it 'defaults to the stock icon' do it 'returns the overlay for the status' do
expect(subject).to eq '/assets/ci_favicons/favicon_status_created.ico' expect(subject).to eq '/assets/ci_favicons/overlays/favicon_status_created.png'
end end
end
it 'uses the custom favicon if a favicon appearance is present' do describe '.available_status_names' do
create :appearance, favicon: fixture_file_upload(Rails.root.join('spec/fixtures/dk.png')) subject { described_class.available_status_names }
expect(subject).to match(%r{/uploads/-/system/appearance/favicon/\d+/favicon_status_created_dk.ico})
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 end
end end
...@@ -19,20 +19,11 @@ RSpec.describe FaviconUploader do ...@@ -19,20 +19,11 @@ RSpec.describe FaviconUploader do
end end
it 'has the correct format' do it 'has the correct format' do
expect(uploader.favicon_main).to be_format('ico') expect(uploader.favicon_main).to be_format('png')
end end
it 'has the correct dimensions' do it 'has the correct dimensions' do
expect(uploader.favicon_main).to have_dimensions(32, 32) expect(uploader.favicon_main).to have_dimensions(32, 32)
end 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
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment