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.
This commit is contained in:
parent
5202c3f0c8
commit
9e14f437b6
|
@ -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 = $('<img />');
|
||||
image.addClass('appearance-light-logo-preview');
|
||||
image.attr('src', faviconWithOverlayUrl);
|
||||
|
||||
faviconContainer.append(image);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -9,19 +9,28 @@ 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
|
||||
def status_overlay(status_name)
|
||||
path = File.join(
|
||||
'ci_favicons',
|
||||
Rails.env.development? ? 'dev' : '',
|
||||
Gitlab::Utils.to_boolean(ENV['CANARY']) ? 'canary' : '',
|
||||
"#{status_name}.ico"
|
||||
'overlays',
|
||||
"#{status_name}.png"
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
private
|
||||
|
|
|
@ -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'))
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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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';
|
|
@ -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', () => {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue