Commit 460e0e70 authored by Mike Greiling's avatar Mike Greiling

Merge branch 'tz-load-emojis-async' into 'master'

Dynamic loading of emojis from json file

See merge request gitlab-org/gitlab!33588
parents bf8fdd82 da48ebdb
...@@ -9,6 +9,7 @@ import { updateTooltipTitle } from './lib/utils/common_utils'; ...@@ -9,6 +9,7 @@ import { updateTooltipTitle } from './lib/utils/common_utils';
import { isInVueNoteablePage } from './lib/utils/dom_utils'; import { isInVueNoteablePage } from './lib/utils/dom_utils';
import flash from './flash'; import flash from './flash';
import axios from './lib/utils/axios_utils'; import axios from './lib/utils/axios_utils';
import * as Emoji from '~/emoji';
const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd'; const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd';
const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd'; const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd';
...@@ -619,7 +620,7 @@ export class AwardsHandler { ...@@ -619,7 +620,7 @@ export class AwardsHandler {
let awardsHandlerPromise = null; let awardsHandlerPromise = null;
export default function loadAwardsHandler(reload = false) { export default function loadAwardsHandler(reload = false) {
if (!awardsHandlerPromise || reload) { if (!awardsHandlerPromise || reload) {
awardsHandlerPromise = import(/* webpackChunkName: 'emoji' */ './emoji').then(Emoji => { awardsHandlerPromise = Emoji.initEmojiMap().then(() => {
const awardsHandler = new AwardsHandler(Emoji); const awardsHandler = new AwardsHandler(Emoji);
awardsHandler.bindEvents(); awardsHandler.bindEvents();
return awardsHandler; return awardsHandler;
......
import 'document-register-element'; import 'document-register-element';
import isEmojiUnicodeSupported from '../emoji/support'; import isEmojiUnicodeSupported from '../emoji/support';
import { initEmojiMap, getEmojiInfo, emojiFallbackImageSrc, emojiImageTag } from '../emoji';
class GlEmoji extends HTMLElement { class GlEmoji extends HTMLElement {
constructor() { constructor() {
super(); super();
const emojiUnicode = this.textContent.trim(); this.initialize();
const { name, unicodeVersion, fallbackSrc, fallbackSpriteClass } = this.dataset; }
initialize() {
const isEmojiUnicode = let emojiUnicode = this.textContent.trim();
this.childNodes && const { fallbackSpriteClass, fallbackSrc } = this.dataset;
Array.prototype.every.call(this.childNodes, childNode => childNode.nodeType === 3); let { name, unicodeVersion } = this.dataset;
const hasImageFallback = fallbackSrc && fallbackSrc.length > 0;
const hasCssSpriteFalback = fallbackSpriteClass && fallbackSpriteClass.length > 0; return initEmojiMap().then(() => {
if (!unicodeVersion) {
if (emojiUnicode && isEmojiUnicode && !isEmojiUnicodeSupported(emojiUnicode, unicodeVersion)) { const emojiInfo = getEmojiInfo(name);
// CSS sprite fallback takes precedence over image fallback
if (hasCssSpriteFalback) { if (emojiInfo) {
if (!gon.emoji_sprites_css_added && gon.emoji_sprites_css_path) { if (name !== emojiInfo.name) {
const emojiSpriteLinkTag = document.createElement('link'); ({ name } = emojiInfo);
emojiSpriteLinkTag.setAttribute('rel', 'stylesheet'); this.dataset.name = emojiInfo.name;
emojiSpriteLinkTag.setAttribute('href', gon.emoji_sprites_css_path); }
document.head.appendChild(emojiSpriteLinkTag); unicodeVersion = emojiInfo.u;
gon.emoji_sprites_css_added = true; this.dataset.unicodeVersion = unicodeVersion;
emojiUnicode = emojiInfo.e;
this.innerHTML = emojiInfo.e;
this.title = emojiInfo.d;
}
}
const isEmojiUnicode =
this.childNodes &&
Array.prototype.every.call(this.childNodes, childNode => childNode.nodeType === 3);
if (
emojiUnicode &&
isEmojiUnicode &&
!isEmojiUnicodeSupported(emojiUnicode, unicodeVersion)
) {
const hasImageFallback = fallbackSrc && fallbackSrc.length > 0;
const hasCssSpriteFallback = fallbackSpriteClass && fallbackSpriteClass.length > 0;
// CSS sprite fallback takes precedence over image fallback
if (hasCssSpriteFallback) {
if (!gon.emoji_sprites_css_added && gon.emoji_sprites_css_path) {
const emojiSpriteLinkTag = document.createElement('link');
emojiSpriteLinkTag.setAttribute('rel', 'stylesheet');
emojiSpriteLinkTag.setAttribute('href', gon.emoji_sprites_css_path);
document.head.appendChild(emojiSpriteLinkTag);
gon.emoji_sprites_css_added = true;
}
// IE 11 doesn't like adding multiple at once :(
this.classList.add('emoji-icon');
this.classList.add(fallbackSpriteClass);
} else if (hasImageFallback) {
this.innerHTML = emojiImageTag(name, fallbackSrc);
} else {
const src = emojiFallbackImageSrc(name);
this.innerHTML = emojiImageTag(name, src);
} }
// IE 11 doesn't like adding multiple at once :(
this.classList.add('emoji-icon');
this.classList.add(fallbackSpriteClass);
} else {
import(/* webpackChunkName: 'emoji' */ '../emoji')
.then(({ emojiImageTag, emojiFallbackImageSrc }) => {
if (hasImageFallback) {
this.innerHTML = emojiImageTag(name, fallbackSrc);
} else {
const src = emojiFallbackImageSrc(name);
this.innerHTML = emojiImageTag(name, src);
}
})
.catch(() => {
// do nothing
});
} }
} });
} }
} }
......
import { uniq } from 'lodash'; import { uniq } from 'lodash';
import emojiMap from 'emojis/digests.json';
import emojiAliases from 'emojis/aliases.json'; import emojiAliases from 'emojis/aliases.json';
import axios from '../lib/utils/axios_utils';
export const validEmojiNames = [...Object.keys(emojiMap), ...Object.keys(emojiAliases)]; import AccessorUtilities from '../lib/utils/accessor';
let emojiMap = null;
let emojiPromise = null;
let validEmojiNames = null;
export const EMOJI_VERSION = '1';
const isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
export function initEmojiMap() {
emojiPromise =
emojiPromise ||
new Promise((resolve, reject) => {
if (emojiMap) {
resolve(emojiMap);
} else if (
isLocalStorageAvailable &&
window.localStorage.getItem('gl-emoji-map-version') === EMOJI_VERSION &&
window.localStorage.getItem('gl-emoji-map')
) {
emojiMap = JSON.parse(window.localStorage.getItem('gl-emoji-map'));
validEmojiNames = [...Object.keys(emojiMap), ...Object.keys(emojiAliases)];
resolve(emojiMap);
} else {
// We load the JSON file direct from the server
// because it can't be loaded from a CDN due to
// cross domain problems with JSON
axios
.get(`${gon.relative_url_root || ''}/-/emojis/${EMOJI_VERSION}/emojis.json`)
.then(({ data }) => {
emojiMap = data;
validEmojiNames = [...Object.keys(emojiMap), ...Object.keys(emojiAliases)];
resolve(emojiMap);
if (isLocalStorageAvailable) {
window.localStorage.setItem('gl-emoji-map-version', EMOJI_VERSION);
window.localStorage.setItem('gl-emoji-map', JSON.stringify(emojiMap));
}
})
.catch(err => {
reject(err);
});
}
});
return emojiPromise;
}
export function normalizeEmojiName(name) { export function normalizeEmojiName(name) {
return Object.prototype.hasOwnProperty.call(emojiAliases, name) ? emojiAliases[name] : name; return Object.prototype.hasOwnProperty.call(emojiAliases, name) ? emojiAliases[name] : name;
} }
export function getValidEmojiNames() {
return validEmojiNames;
}
export function isEmojiNameValid(name) { export function isEmojiNameValid(name) {
return validEmojiNames.indexOf(name) >= 0; return validEmojiNames.indexOf(name) >= 0;
} }
...@@ -36,8 +86,8 @@ export function getEmojiCategoryMap() { ...@@ -36,8 +86,8 @@ export function getEmojiCategoryMap() {
}; };
Object.keys(emojiMap).forEach(name => { Object.keys(emojiMap).forEach(name => {
const emoji = emojiMap[name]; const emoji = emojiMap[name];
if (emojiCategoryMap[emoji.category]) { if (emojiCategoryMap[emoji.c]) {
emojiCategoryMap[emoji.category].push(name); emojiCategoryMap[emoji.c].push(name);
} }
}); });
} }
...@@ -58,8 +108,9 @@ export function getEmojiInfo(query) { ...@@ -58,8 +108,9 @@ export function getEmojiInfo(query) {
} }
export function emojiFallbackImageSrc(inputName) { export function emojiFallbackImageSrc(inputName) {
const { name, digest } = getEmojiInfo(inputName); const { name } = getEmojiInfo(inputName);
return `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/emoji/${name}-${digest}.png`; return `${gon.asset_host || ''}${gon.relative_url_root ||
''}/-/emojis/${EMOJI_VERSION}/${name}.png`;
} }
export function emojiImageTag(name, src) { export function emojiImageTag(name, src) {
...@@ -67,36 +118,17 @@ export function emojiImageTag(name, src) { ...@@ -67,36 +118,17 @@ export function emojiImageTag(name, src) {
} }
export function glEmojiTag(inputName, options) { export function glEmojiTag(inputName, options) {
const opts = { sprite: false, forceFallback: false, ...options }; const opts = { sprite: false, ...options };
const { name, ...emojiInfo } = getEmojiInfo(inputName); const name = normalizeEmojiName(inputName);
const fallbackImageSrc = emojiFallbackImageSrc(name);
const fallbackSpriteClass = `emoji-${name}`; const fallbackSpriteClass = `emoji-${name}`;
const classList = [];
if (opts.forceFallback && opts.sprite) {
classList.push('emoji-icon');
classList.push(fallbackSpriteClass);
}
const classAttribute = classList.length > 0 ? `class="${classList.join(' ')}"` : '';
const fallbackSpriteAttribute = opts.sprite const fallbackSpriteAttribute = opts.sprite
? `data-fallback-sprite-class="${fallbackSpriteClass}"` ? `data-fallback-sprite-class="${fallbackSpriteClass}"`
: ''; : '';
let contents = emojiInfo.moji;
if (opts.forceFallback && !opts.sprite) {
contents = emojiImageTag(name, fallbackImageSrc);
}
return ` return `
<gl-emoji <gl-emoji
${classAttribute}
data-name="${name}"
data-fallback-src="${fallbackImageSrc}"
${fallbackSpriteAttribute} ${fallbackSpriteAttribute}
data-unicode-version="${emojiInfo.unicodeVersion}" data-name="${name}"></gl-emoji>
title="${emojiInfo.description}"
>
${contents}
</gl-emoji>
`; `;
} }
...@@ -7,6 +7,7 @@ import DropdownUtils from '~/filtered_search/dropdown_utils'; ...@@ -7,6 +7,7 @@ import DropdownUtils from '~/filtered_search/dropdown_utils';
import Flash from '~/flash'; import Flash from '~/flash';
import UsersCache from '~/lib/utils/users_cache'; import UsersCache from '~/lib/utils/users_cache';
import { __ } from '~/locale'; import { __ } from '~/locale';
import * as Emoji from '~/emoji';
export default class VisualTokenValue { export default class VisualTokenValue {
constructor(tokenValue, tokenType, tokenOperator) { constructor(tokenValue, tokenType, tokenOperator) {
...@@ -137,18 +138,13 @@ export default class VisualTokenValue { ...@@ -137,18 +138,13 @@ export default class VisualTokenValue {
const element = tokenValueElement; const element = tokenValueElement;
const value = this.tokenValue; const value = this.tokenValue;
return ( return Emoji.initEmojiMap().then(() => {
import(/* webpackChunkName: 'emoji' */ '../emoji') if (!Emoji.isEmojiNameValid(value)) {
.then(Emoji => { return;
if (!Emoji.isEmojiNameValid(value)) { }
return;
}
container.dataset.originalValue = value; container.dataset.originalValue = value;
element.innerHTML = Emoji.glEmojiTag(value); element.innerHTML = Emoji.glEmojiTag(value);
}) });
// ignore error and leave emoji name in the search bar
.catch(() => {})
);
} }
} }
...@@ -5,6 +5,7 @@ import SidebarMediator from '~/sidebar/sidebar_mediator'; ...@@ -5,6 +5,7 @@ import SidebarMediator from '~/sidebar/sidebar_mediator';
import glRegexp from './lib/utils/regexp'; import glRegexp from './lib/utils/regexp';
import AjaxCache from './lib/utils/ajax_cache'; import AjaxCache from './lib/utils/ajax_cache';
import { spriteIcon } from './lib/utils/common_utils'; import { spriteIcon } from './lib/utils/common_utils';
import * as Emoji from '~/emoji';
function sanitize(str) { function sanitize(str) {
return str.replace(/<(?:.|\n)*?>/gm, ''); return str.replace(/<(?:.|\n)*?>/gm, '');
...@@ -586,14 +587,12 @@ class GfmAutoComplete { ...@@ -586,14 +587,12 @@ class GfmAutoComplete {
if (this.cachedData[at]) { if (this.cachedData[at]) {
this.loadData($input, at, this.cachedData[at]); this.loadData($input, at, this.cachedData[at]);
} else if (GfmAutoComplete.atTypeMap[at] === 'emojis') { } else if (GfmAutoComplete.atTypeMap[at] === 'emojis') {
import(/* webpackChunkName: 'emoji' */ './emoji') Emoji.initEmojiMap()
.then(({ validEmojiNames, glEmojiTag }) => { .then(() => {
this.loadData($input, at, validEmojiNames); this.loadData($input, at, Emoji.getValidEmojiNames());
GfmAutoComplete.glEmojiTag = glEmojiTag; GfmAutoComplete.glEmojiTag = Emoji.glEmojiTag;
}) })
.catch(() => { .catch(() => {});
this.isLoadingData[at] = false;
});
} else if (dataSource) { } else if (dataSource) {
AjaxCache.retrieve(dataSource, true) AjaxCache.retrieve(dataSource, true)
.then(data => { .then(data => {
......
...@@ -4,6 +4,7 @@ import emojiRegex from 'emoji-regex'; ...@@ -4,6 +4,7 @@ import emojiRegex from 'emoji-regex';
import createFlash from '~/flash'; import createFlash from '~/flash';
import EmojiMenu from './emoji_menu'; import EmojiMenu from './emoji_menu';
import { __ } from '~/locale'; import { __ } from '~/locale';
import * as Emoji from '~/emoji';
const defaultStatusEmoji = 'speech_balloon'; const defaultStatusEmoji = 'speech_balloon';
...@@ -55,8 +56,8 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -55,8 +56,8 @@ document.addEventListener('DOMContentLoaded', () => {
} }
}); });
import(/* webpackChunkName: 'emoji' */ '~/emoji') Emoji.initEmojiMap()
.then(Emoji => { .then(() => {
const emojiMenu = new EmojiMenu( const emojiMenu = new EmojiMenu(
Emoji, Emoji,
toggleEmojiMenuButtonSelector, toggleEmojiMenuButtonSelector,
......
...@@ -8,6 +8,7 @@ import { __, s__ } from '~/locale'; ...@@ -8,6 +8,7 @@ import { __, s__ } from '~/locale';
import Api from '~/api'; import Api from '~/api';
import eventHub from './event_hub'; import eventHub from './event_hub';
import EmojiMenuInModal from './emoji_menu_in_modal'; import EmojiMenuInModal from './emoji_menu_in_modal';
import * as Emoji from '~/emoji';
const emojiMenuClass = 'js-modal-status-emoji-menu'; const emojiMenuClass = 'js-modal-status-emoji-menu';
...@@ -64,8 +65,8 @@ export default { ...@@ -64,8 +65,8 @@ export default {
const emojiAutocomplete = new GfmAutoComplete(); const emojiAutocomplete = new GfmAutoComplete();
emojiAutocomplete.setup($(this.$refs.statusMessageField), { emojis: true }); emojiAutocomplete.setup($(this.$refs.statusMessageField), { emojis: true });
import(/* webpackChunkName: 'emoji' */ '~/emoji') Emoji.initEmojiMap()
.then(Emoji => { .then(() => {
if (this.emoji) { if (this.emoji) {
this.emojiTag = Emoji.glEmojiTag(this.emoji); this.emojiTag = Emoji.glEmojiTag(this.emoji);
} }
......
import $ from 'jquery'; import $ from 'jquery';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import loadAwardsHandler from '~/awards_handler'; import loadAwardsHandler from '~/awards_handler';
import '~/lib/utils/common_utils'; import '~/lib/utils/common_utils';
import waitForPromises from './helpers/wait_for_promises'; import waitForPromises from './helpers/wait_for_promises';
import { EMOJI_VERSION } from '~/emoji';
window.gl = window.gl || {}; window.gl = window.gl || {};
window.gon = window.gon || {}; window.gon = window.gon || {};
let openAndWaitForEmojiMenu; let openAndWaitForEmojiMenu;
let mock;
let awardsHandler = null; let awardsHandler = null;
const urlRoot = gon.relative_url_root; const urlRoot = gon.relative_url_root;
...@@ -24,8 +28,13 @@ const lazyAssert = (done, assertFn) => { ...@@ -24,8 +28,13 @@ const lazyAssert = (done, assertFn) => {
}; };
describe('AwardsHandler', () => { describe('AwardsHandler', () => {
const emojiData = getJSONFixture('emojis/emojis.json');
preloadFixtures('snippets/show.html'); preloadFixtures('snippets/show.html');
beforeEach(done => { beforeEach(done => {
mock = new MockAdapter(axios);
mock.onGet(`/-/emojis/${EMOJI_VERSION}/emojis.json`).reply(200, emojiData);
loadFixtures('snippets/show.html'); loadFixtures('snippets/show.html');
loadAwardsHandler(true) loadAwardsHandler(true)
.then(obj => { .then(obj => {
...@@ -58,6 +67,8 @@ describe('AwardsHandler', () => { ...@@ -58,6 +67,8 @@ describe('AwardsHandler', () => {
// restore original url root value // restore original url root value
gon.relative_url_root = urlRoot; gon.relative_url_root = urlRoot;
mock.restore();
// Undo what we did to the shared <body> // Undo what we did to the shared <body>
$('body').removeAttr('data-page'); $('body').removeAttr('data-page');
......
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import { initEmojiMap, EMOJI_VERSION } from '~/emoji';
import installGlEmojiElement from '~/behaviors/gl_emoji';
import * as EmojiUnicodeSupport from '~/emoji/support';
import waitForPromises from 'jest/helpers/wait_for_promises';
jest.mock('~/emoji/support');
describe('gl_emoji', () => {
let mock;
const emojiData = getJSONFixture('emojis/emojis.json');
beforeAll(() => {
jest.spyOn(EmojiUnicodeSupport, 'default').mockReturnValue(true);
installGlEmojiElement();
});
function markupToDomElement(markup) {
const div = document.createElement('div');
div.innerHTML = markup;
document.body.appendChild(div);
return div.firstElementChild;
}
beforeEach(() => {
mock = new MockAdapter(axios);
mock.onGet(`/-/emojis/${EMOJI_VERSION}/emojis.json`).reply(200, emojiData);
return initEmojiMap().catch(() => {});
});
afterEach(() => {
mock.restore();
document.body.innerHTML = '';
});
describe.each([
[
'bomb emoji just with name attribute',
'<gl-emoji data-name="bomb"></gl-emoji>',
'<gl-emoji data-name="bomb" data-unicode-version="6.0" title="bomb">💣</gl-emoji>',
'<gl-emoji data-name="bomb" data-unicode-version="6.0" title="bomb"><img class="emoji" title=":bomb:" alt=":bomb:" src="/-/emojis/1/bomb.png" width="20" height="20" align="absmiddle"></gl-emoji>',
],
[
'bomb emoji with name attribute and unicode version',
'<gl-emoji data-name="bomb" data-unicode-version="6.0">💣</gl-emoji>',
'<gl-emoji data-name="bomb" data-unicode-version="6.0">💣</gl-emoji>',
'<gl-emoji data-name="bomb" data-unicode-version="6.0"><img class="emoji" title=":bomb:" alt=":bomb:" src="/-/emojis/1/bomb.png" width="20" height="20" align="absmiddle"></gl-emoji>',
],
[
'bomb emoji with sprite fallback',
'<gl-emoji data-fallback-sprite-class="emoji-bomb" data-name="bomb"></gl-emoji>',
'<gl-emoji data-fallback-sprite-class="emoji-bomb" data-name="bomb" data-unicode-version="6.0" title="bomb">💣</gl-emoji>',
'<gl-emoji data-fallback-sprite-class="emoji-bomb" data-name="bomb" data-unicode-version="6.0" title="bomb" class="emoji-icon emoji-bomb">💣</gl-emoji>',
],
[
'bomb emoji with image fallback',
'<gl-emoji data-fallback-src="/bomb.png" data-name="bomb"></gl-emoji>',
'<gl-emoji data-fallback-src="/bomb.png" data-name="bomb" data-unicode-version="6.0" title="bomb">💣</gl-emoji>',
'<gl-emoji data-fallback-src="/bomb.png" data-name="bomb" data-unicode-version="6.0" title="bomb"><img class="emoji" title=":bomb:" alt=":bomb:" src="/bomb.png" width="20" height="20" align="absmiddle"></gl-emoji>',
],
[
'invalid emoji',
'<gl-emoji data-name="invalid_emoji"></gl-emoji>',
'<gl-emoji data-name="grey_question" data-unicode-version="6.0" title="white question mark ornament">❔</gl-emoji>',
'<gl-emoji data-name="grey_question" data-unicode-version="6.0" title="white question mark ornament"><img class="emoji" title=":grey_question:" alt=":grey_question:" src="/-/emojis/1/grey_question.png" width="20" height="20" align="absmiddle"></gl-emoji>',
],
])('%s', (name, markup, withEmojiSupport, withoutEmojiSupport) => {
it(`renders correctly with emoji support`, async () => {
jest.spyOn(EmojiUnicodeSupport, 'default').mockReturnValue(true);
const glEmojiElement = markupToDomElement(markup);
await waitForPromises();
expect(glEmojiElement.outerHTML).toBe(withEmojiSupport);
});
it(`renders correctly without emoji support`, async () => {
jest.spyOn(EmojiUnicodeSupport, 'default').mockReturnValue(false);
const glEmojiElement = markupToDomElement(markup);
await waitForPromises();
expect(glEmojiElement.outerHTML).toBe(withoutEmojiSupport);
});
});
it('Adds sprite CSS if emojis are not supported', async () => {
const testPath = '/test-path.css';
jest.spyOn(EmojiUnicodeSupport, 'default').mockReturnValue(false);
window.gon.emoji_sprites_css_path = testPath;
expect(document.head.querySelector(`link[href="${testPath}"]`)).toBe(null);
expect(window.gon.emoji_sprites_css_added).toBeFalsy();
markupToDomElement(
'<gl-emoji data-fallback-sprite-class="emoji-bomb" data-name="bomb"></gl-emoji>',
);
await waitForPromises();
expect(document.head.querySelector(`link[href="${testPath}"]`).outerHTML).toBe(
'<link rel="stylesheet" href="/test-path.css">',
);
expect(window.gon.emoji_sprites_css_added).toBe(true);
});
});
import { glEmojiTag } from '~/emoji'; import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import { initEmojiMap, glEmojiTag, EMOJI_VERSION } from '~/emoji';
import isEmojiUnicodeSupported, { import isEmojiUnicodeSupported, {
isFlagEmoji, isFlagEmoji,
isRainbowFlagEmoji, isRainbowFlagEmoji,
...@@ -7,6 +9,7 @@ import isEmojiUnicodeSupported, { ...@@ -7,6 +9,7 @@ import isEmojiUnicodeSupported, {
isHorceRacingSkinToneComboEmoji, isHorceRacingSkinToneComboEmoji,
isPersonZwjEmoji, isPersonZwjEmoji,
} from '~/emoji/support/is_emoji_unicode_supported'; } from '~/emoji/support/is_emoji_unicode_supported';
import { trimText } from 'helpers/text_helper';
const emptySupportMap = { const emptySupportMap = {
personZwj: false, personZwj: false,
...@@ -50,77 +53,28 @@ const emojiFixtureMap = { ...@@ -50,77 +53,28 @@ const emojiFixtureMap = {
}, },
}; };
function markupToDomElement(markup) { describe('gl_emoji', () => {
const div = document.createElement('div'); let mock;
div.innerHTML = markup; const emojiData = getJSONFixture('emojis/emojis.json');
return div.firstElementChild;
}
function testGlEmojiImageFallback(element, name, src) {
expect(element.tagName.toLowerCase()).toBe('img');
expect(element.getAttribute('src')).toBe(src);
expect(element.getAttribute('title')).toBe(`:${name}:`);
expect(element.getAttribute('alt')).toBe(`:${name}:`);
}
const defaults = {
forceFallback: false,
sprite: false,
};
function testGlEmojiElement(element, name, unicodeVersion, unicodeMoji, options = {}) { beforeEach(() => {
const opts = { ...defaults, ...options }; mock = new MockAdapter(axios);
expect(element.tagName.toLowerCase()).toBe('gl-emoji'); mock.onGet(`/-/emojis/${EMOJI_VERSION}/emojis.json`).reply(200, emojiData);
expect(element.dataset.name).toBe(name);
expect(element.dataset.fallbackSrc.length).toBeGreaterThan(0); return initEmojiMap().catch(() => {});
expect(element.dataset.unicodeVersion).toBe(unicodeVersion); });
const fallbackSpriteClass = `emoji-${name}`; afterEach(() => {
if (opts.sprite) { mock.restore();
expect(element.dataset.fallbackSpriteClass).toBe(fallbackSpriteClass); });
}
if (opts.forceFallback && opts.sprite) {
expect(element.getAttribute('class')).toBe(`emoji-icon ${fallbackSpriteClass}`);
}
if (opts.forceFallback && !opts.sprite) {
// Check for image fallback
testGlEmojiImageFallback(element.firstElementChild, name, element.dataset.fallbackSrc);
} else {
// Otherwise make sure things are still unicode text
expect(element.textContent.trim()).toBe(unicodeMoji);
}
}
describe('gl_emoji', () => {
describe('glEmojiTag', () => { describe('glEmojiTag', () => {
it('bomb emoji', () => { it('bomb emoji', () => {
const emojiKey = 'bomb'; const emojiKey = 'bomb';
const markup = glEmojiTag(emojiFixtureMap[emojiKey].name); const markup = glEmojiTag(emojiFixtureMap[emojiKey].name);
const glEmojiElement = markupToDomElement(markup);
testGlEmojiElement(
glEmojiElement,
emojiFixtureMap[emojiKey].name,
emojiFixtureMap[emojiKey].unicodeVersion,
emojiFixtureMap[emojiKey].moji,
);
});
it('bomb emoji with image fallback', () => { expect(trimText(markup)).toMatchInlineSnapshot(
const emojiKey = 'bomb'; `"<gl-emoji data-name=\\"bomb\\"></gl-emoji>"`,
const markup = glEmojiTag(emojiFixtureMap[emojiKey].name, {
forceFallback: true,
});
const glEmojiElement = markupToDomElement(markup);
testGlEmojiElement(
glEmojiElement,
emojiFixtureMap[emojiKey].name,
emojiFixtureMap[emojiKey].unicodeVersion,
emojiFixtureMap[emojiKey].moji,
{
forceFallback: true,
},
); );
}); });
...@@ -129,65 +83,8 @@ describe('gl_emoji', () => { ...@@ -129,65 +83,8 @@ describe('gl_emoji', () => {
const markup = glEmojiTag(emojiFixtureMap[emojiKey].name, { const markup = glEmojiTag(emojiFixtureMap[emojiKey].name, {
sprite: true, sprite: true,
}); });
const glEmojiElement = markupToDomElement(markup); expect(trimText(markup)).toMatchInlineSnapshot(
testGlEmojiElement( `"<gl-emoji data-fallback-sprite-class=\\"emoji-bomb\\" data-name=\\"bomb\\"></gl-emoji>"`,
glEmojiElement,
emojiFixtureMap[emojiKey].name,
emojiFixtureMap[emojiKey].unicodeVersion,
emojiFixtureMap[emojiKey].moji,
{
sprite: true,
},
);
});
it('bomb emoji with sprite fallback', () => {
const emojiKey = 'bomb';
const markup = glEmojiTag(emojiFixtureMap[emojiKey].name, {
forceFallback: true,
sprite: true,
});
const glEmojiElement = markupToDomElement(markup);
testGlEmojiElement(
glEmojiElement,
emojiFixtureMap[emojiKey].name,
emojiFixtureMap[emojiKey].unicodeVersion,
emojiFixtureMap[emojiKey].moji,
{
forceFallback: true,
sprite: true,
},
);
});
it('question mark when invalid emoji name given', () => {
const name = 'invalid_emoji';
const emojiKey = 'grey_question';
const markup = glEmojiTag(name);
const glEmojiElement = markupToDomElement(markup);
testGlEmojiElement(
glEmojiElement,
emojiFixtureMap[emojiKey].name,
emojiFixtureMap[emojiKey].unicodeVersion,
emojiFixtureMap[emojiKey].moji,
);
});
it('question mark with image fallback when invalid emoji name given', () => {
const name = 'invalid_emoji';
const emojiKey = 'grey_question';
const markup = glEmojiTag(name, {
forceFallback: true,
});
const glEmojiElement = markupToDomElement(markup);
testGlEmojiElement(
glEmojiElement,
emojiFixtureMap[emojiKey].name,
emojiFixtureMap[emojiKey].unicodeVersion,
emojiFixtureMap[emojiKey].moji,
{
forceFallback: true,
},
); );
}); });
}); });
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Emojis (JavaScript fixtures)', type: :request do
include JavaScriptFixturesHelpers
before(:all) do
clean_frontend_fixtures('emojis/')
end
it 'emojis/emojis.json' do |example|
get '/-/emojis/1/emojis.json'
expect(response).to be_successful
end
end
...@@ -18,15 +18,8 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = ` ...@@ -18,15 +18,8 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
<gl-emoji <gl-emoji
data-fallback-src="/assets/emoji/thumbsup-59ec2457ab33e8897261d01a495f6cf5c668d0004807dc541c3b1be5294b1e61.png"
data-name="thumbsup" data-name="thumbsup"
data-unicode-version="6.0" />
title="thumbs up sign"
>
👍
</gl-emoji>
</span> </span>
...@@ -51,15 +44,8 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = ` ...@@ -51,15 +44,8 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
<gl-emoji <gl-emoji
data-fallback-src="/assets/emoji/thumbsdown-5954334e2dae5357312b3d629f10a496c728029e02216f8c8b887f9b51561c61.png"
data-name="thumbsdown" data-name="thumbsdown"
data-unicode-version="6.0" />
title="thumbs down sign"
>
👎
</gl-emoji>
</span> </span>
...@@ -84,15 +70,8 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = ` ...@@ -84,15 +70,8 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
<gl-emoji <gl-emoji
data-fallback-src="/assets/emoji/smile-14905c372d5bf7719bd727c9efae31a03291acec79801652a23710c6848c5d14.png"
data-name="smile" data-name="smile"
data-unicode-version="6.0" />
title="smiling face with open mouth and smiling eyes"
>
😄
</gl-emoji>
</span> </span>
...@@ -117,15 +96,8 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = ` ...@@ -117,15 +96,8 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
<gl-emoji <gl-emoji
data-fallback-src="/assets/emoji/ok_hand-d63002dce3cc3655b67b8765b7c28d370edba0e3758b2329b60e0e61c4d8e78d.png"
data-name="ok_hand" data-name="ok_hand"
data-unicode-version="6.0" />
title="ok hand sign"
>
👌
</gl-emoji>
</span> </span>
...@@ -150,15 +122,8 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = ` ...@@ -150,15 +122,8 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
<gl-emoji <gl-emoji
data-fallback-src="/assets/emoji/cactus-2c5c4c35f26c7046fdc002b337e0d939729b33a26980e675950f9934c91e40fd.png"
data-name="cactus" data-name="cactus"
data-unicode-version="6.0" />
title="cactus"
>
🌵
</gl-emoji>
</span> </span>
...@@ -183,15 +148,8 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = ` ...@@ -183,15 +148,8 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
<gl-emoji <gl-emoji
data-fallback-src="/assets/emoji/a-bddbb39e8a1d35d42b7c08e7d47f63988cb4d8614b79f74e70b9c67c221896cc.png"
data-name="a" data-name="a"
data-unicode-version="6.0" />
title="negative squared latin capital letter a"
>
🅰
</gl-emoji>
</span> </span>
...@@ -216,15 +174,8 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = ` ...@@ -216,15 +174,8 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
<gl-emoji <gl-emoji
data-fallback-src="/assets/emoji/b-722f9db9442e7c0fc0d0ac0f5291fbf47c6a0ac4d8abd42e97957da705fb82bf.png"
data-name="b" data-name="b"
data-unicode-version="6.0" />
title="negative squared latin capital letter b"
>
🅱
</gl-emoji>
</span> </span>
......
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