diff options
Diffstat (limited to 'spec/frontend/emoji/index_spec.js')
-rw-r--r-- | spec/frontend/emoji/index_spec.js | 266 |
1 files changed, 220 insertions, 46 deletions
diff --git a/spec/frontend/emoji/index_spec.js b/spec/frontend/emoji/index_spec.js index 1a12bd303f1..7d6a45fbf30 100644 --- a/spec/frontend/emoji/index_spec.js +++ b/spec/frontend/emoji/index_spec.js @@ -1,10 +1,11 @@ +import MockAdapter from 'axios-mock-adapter'; import { emojiFixtureMap, - mockEmojiData, initEmojiMock, validEmoji, invalidEmoji, clearEmojiMock, + mockEmojiData, } from 'helpers/emoji'; import { trimText } from 'helpers/text_helper'; import { createMockClient } from 'helpers/mock_apollo_helper'; @@ -14,9 +15,10 @@ import { getEmojiInfo, sortEmoji, initEmojiMap, - getAllEmoji, + getEmojiMap, emojiFallbackImageSrc, loadCustomEmojiWithNames, + EMOJI_VERSION, } from '~/emoji'; import isEmojiUnicodeSupported, { @@ -27,8 +29,11 @@ import isEmojiUnicodeSupported, { isHorceRacingSkinToneComboEmoji, isPersonZwjEmoji, } from '~/emoji/support/is_emoji_unicode_supported'; -import { NEUTRAL_INTENT_MULTIPLIER } from '~/emoji/constants'; +import { CACHE_KEY, CACHE_VERSION_KEY, NEUTRAL_INTENT_MULTIPLIER } from '~/emoji/constants'; import customEmojiQuery from '~/emoji/queries/custom_emoji.query.graphql'; +import axios from '~/lib/utils/axios_utils'; +import { HTTP_STATUS_OK } from '~/lib/utils/http_status'; +import { useLocalStorageSpy } from 'jest/__helpers__/local_storage_helper'; let mockClient; jest.mock('~/lib/graphql', () => { @@ -75,6 +80,195 @@ function createMockEmojiClient() { document.body.dataset.groupFullPath = 'test-group'; } +describe('retrieval of emojis.json', () => { + useLocalStorageSpy(); + + let mock; + beforeEach(() => { + mock = new MockAdapter(axios); + mock.onGet(/emojis\.json$/).reply(HTTP_STATUS_OK, mockEmojiData); + initEmojiMap.promise = null; + }); + + afterEach(() => { + mock.restore(); + }); + + const assertCorrectLocalStorage = () => { + expect(localStorage.length).toBe(1); + expect(localStorage.getItem(CACHE_KEY)).toBe( + JSON.stringify({ data: mockEmojiData, EMOJI_VERSION }), + ); + }; + + const assertEmojiBeingLoadedCorrectly = () => { + expect(Object.keys(getEmojiMap())).toEqual(Object.keys(validEmoji)); + }; + + it('should remove the old `CACHE_VERSION_KEY`', async () => { + localStorage.setItem(CACHE_VERSION_KEY, EMOJI_VERSION); + + await initEmojiMap(); + + expect(localStorage.getItem(CACHE_VERSION_KEY)).toBe(null); + }); + + describe('when the localStorage is empty', () => { + it('should call the API and store results in localStorage', async () => { + await initEmojiMap(); + + assertEmojiBeingLoadedCorrectly(); + expect(mock.history.get.length).toBe(1); + assertCorrectLocalStorage(); + }); + }); + + describe('when the localStorage stores the correct version', () => { + beforeEach(async () => { + localStorage.setItem(CACHE_KEY, JSON.stringify({ data: mockEmojiData, EMOJI_VERSION })); + localStorage.setItem.mockClear(); + await initEmojiMap(); + }); + + it('should not call the API and not mutate the localStorage', () => { + assertEmojiBeingLoadedCorrectly(); + expect(mock.history.get.length).toBe(0); + expect(localStorage.setItem).not.toHaveBeenCalled(); + assertCorrectLocalStorage(); + }); + }); + + describe('when the localStorage stores an incorrect version', () => { + beforeEach(async () => { + localStorage.setItem( + CACHE_KEY, + JSON.stringify({ data: mockEmojiData, EMOJI_VERSION: `${EMOJI_VERSION}-different` }), + ); + localStorage.setItem.mockClear(); + await initEmojiMap(); + }); + + it('should call the API and store results in localStorage', () => { + assertEmojiBeingLoadedCorrectly(); + expect(mock.history.get.length).toBe(1); + assertCorrectLocalStorage(); + }); + }); + + describe('when the localStorage stores corrupted data', () => { + beforeEach(async () => { + localStorage.setItem(CACHE_KEY, "[invalid: 'INVALID_JSON"); + localStorage.setItem.mockClear(); + await initEmojiMap(); + }); + + it('should call the API and store results in localStorage', () => { + assertEmojiBeingLoadedCorrectly(); + expect(mock.history.get.length).toBe(1); + assertCorrectLocalStorage(); + }); + }); + + describe('when the localStorage stores data in a different format', () => { + beforeEach(async () => { + localStorage.setItem(CACHE_KEY, JSON.stringify([])); + localStorage.setItem.mockClear(); + await initEmojiMap(); + }); + + it('should call the API and store results in localStorage', () => { + assertEmojiBeingLoadedCorrectly(); + expect(mock.history.get.length).toBe(1); + assertCorrectLocalStorage(); + }); + }); + + describe('when the localStorage is full', () => { + beforeEach(async () => { + const oldSetItem = localStorage.setItem; + localStorage.setItem = jest.fn().mockImplementationOnce((key, value) => { + if (key === CACHE_KEY) { + throw new Error('Storage Full'); + } + oldSetItem(key, value); + }); + await initEmojiMap(); + }); + + it('should call API but not store the results', () => { + assertEmojiBeingLoadedCorrectly(); + expect(mock.history.get.length).toBe(1); + expect(localStorage.length).toBe(0); + expect(localStorage.setItem).toHaveBeenCalledTimes(1); + expect(localStorage.setItem).toHaveBeenCalledWith( + CACHE_KEY, + JSON.stringify({ data: mockEmojiData, EMOJI_VERSION }), + ); + }); + }); + + describe('backwards compatibility', () => { + // As per: https://gitlab.com/gitlab-org/gitlab/-/blob/62b66abd3bb7801e7c85b4e42a1bbd51fbb37c1b/app/assets/javascripts/emoji/index.js#L27-52 + async function prevImplementation() { + if ( + window.localStorage.getItem(CACHE_VERSION_KEY) === EMOJI_VERSION && + window.localStorage.getItem(CACHE_KEY) + ) { + return JSON.parse(window.localStorage.getItem(CACHE_KEY)); + } + + // 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 + const { data } = await axios.get( + `${gon.relative_url_root || ''}/-/emojis/${EMOJI_VERSION}/emojis.json`, + ); + + try { + window.localStorage.setItem(CACHE_VERSION_KEY, EMOJI_VERSION); + window.localStorage.setItem(CACHE_KEY, JSON.stringify(data)); + } catch { + // Setting data in localstorage may fail when storage quota is exceeded. + // We should continue even when this fails. + } + + return data; + } + + it('Old -> New -> Old should not break', async () => { + // The follow steps simulate a multi-version deployment. e.g. + // Hitting a page on "regular" .com, then canary, and then "regular" again + + // Load emoji the old way to pre-populate the cache + let res = await prevImplementation(); + expect(res).toEqual(mockEmojiData); + expect(mock.history.get.length).toBe(1); + localStorage.setItem.mockClear(); + + // Load emoji the new way + await initEmojiMap(); + expect(mock.history.get.length).toBe(2); + assertEmojiBeingLoadedCorrectly(); + assertCorrectLocalStorage(); + localStorage.setItem.mockClear(); + + // Load emoji the old way to pre-populate the cache + res = await prevImplementation(); + expect(res).toEqual(mockEmojiData); + expect(mock.history.get.length).toBe(3); + expect(localStorage.setItem.mock.calls).toEqual([ + [CACHE_VERSION_KEY, EMOJI_VERSION], + [CACHE_KEY, JSON.stringify(mockEmojiData)], + ]); + + // Load emoji the old way should work again (and be taken from the cache) + res = await prevImplementation(); + expect(res).toEqual(mockEmojiData); + expect(mock.history.get.length).toBe(3); + }); + }); +}); + describe('emoji', () => { beforeEach(async () => { await initEmojiMock(); @@ -90,7 +284,7 @@ describe('emoji', () => { it('should contain valid emoji', async () => { await initEmojiMap(); - const allEmoji = Object.keys(getAllEmoji()); + const allEmoji = Object.keys(getEmojiMap()); Object.keys(validEmoji).forEach((key) => { expect(allEmoji.includes(key)).toBe(true); }); @@ -99,34 +293,11 @@ describe('emoji', () => { it('should not contain invalid emoji', async () => { await initEmojiMap(); - const allEmoji = Object.keys(getAllEmoji()); + const allEmoji = Object.keys(getEmojiMap()); Object.keys(invalidEmoji).forEach((key) => { expect(allEmoji.includes(key)).toBe(false); }); }); - - it('fixes broken pride emoji', async () => { - clearEmojiMock(); - await initEmojiMock({ - gay_pride_flag: { - c: 'flags', - // Without a zero-width joiner - e: '🏳🌈', - name: 'gay_pride_flag', - u: '6.0', - }, - }); - - expect(getAllEmoji()).toEqual({ - gay_pride_flag: { - c: 'flags', - // With a zero-width joiner - e: '🏳️🌈', - name: 'gay_pride_flag', - u: '6.0', - }, - }); - }); }); describe('glEmojiTag', () => { @@ -448,11 +619,11 @@ describe('emoji', () => { describe('getEmojiInfo', () => { it.each(['atom', 'five', 'black_heart'])("should return a correct emoji for '%s'", (name) => { - expect(getEmojiInfo(name)).toEqual(mockEmojiData[name]); + expect(getEmojiInfo(name)).toEqual(getEmojiMap()[name]); }); it('should return fallback emoji by default', () => { - expect(getEmojiInfo('atjs')).toEqual(mockEmojiData.grey_question); + expect(getEmojiInfo('atjs')).toEqual(getEmojiMap().grey_question); }); it('should return null when fallback is false', () => { @@ -461,7 +632,7 @@ describe('emoji', () => { describe('when query is undefined', () => { it('should return fallback emoji by default', () => { - expect(getEmojiInfo()).toEqual(mockEmojiData.grey_question); + expect(getEmojiInfo()).toEqual(getEmojiMap().grey_question); }); it('should return null when fallback is false', () => { @@ -489,9 +660,9 @@ describe('emoji', () => { } return { - emoji: mockEmojiData[name], + emoji: getEmojiMap()[name], field: 'd', - fieldValue: mockEmojiData[name].d, + fieldValue: getEmojiMap()[name].d, score, }; }) @@ -542,7 +713,7 @@ describe('emoji', () => { const { field, score, fieldValue, name } = item; return { - emoji: mockEmojiData[name], + emoji: getEmojiMap()[name], field, fieldValue, score, @@ -669,9 +840,9 @@ describe('emoji', () => { const { field, score, name } = item; return { - emoji: mockEmojiData[name], + emoji: getEmojiMap()[name], field, - fieldValue: mockEmojiData[name][field], + fieldValue: getEmojiMap()[name][field], score, }; }); @@ -737,7 +908,7 @@ describe('emoji', () => { it.each` emoji | src - ${'thumbsup'} | ${'/-/emojis/2/thumbsup.png'} + ${'thumbsup'} | ${'/-/emojis/3/thumbsup.png'} ${'parrot'} | ${'parrot.gif'} `('returns $src for emoji with name $emoji', ({ emoji, src }) => { expect(emojiFallbackImageSrc(emoji)).toBe(src); @@ -757,7 +928,7 @@ describe('emoji', () => { it('returns empty object', async () => { const result = await loadCustomEmojiWithNames(); - expect(result).toEqual({}); + expect(result).toEqual({ emojis: {}, names: [] }); }); }); @@ -769,7 +940,7 @@ describe('emoji', () => { it('returns empty object', async () => { const result = await loadCustomEmojiWithNames(); - expect(result).toEqual({}); + expect(result).toEqual({ emojis: {}, names: [] }); }); }); @@ -778,14 +949,17 @@ describe('emoji', () => { const result = await loadCustomEmojiWithNames(); expect(result).toEqual({ - parrot: { - c: 'custom', - d: 'parrot', - e: undefined, - name: 'parrot', - src: 'parrot.gif', - u: 'custom', + emojis: { + parrot: { + c: 'custom', + d: 'parrot', + e: undefined, + name: 'parrot', + src: 'parrot.gif', + u: 'custom', + }, }, + names: ['parrot'], }); }); }); |