diff --git a/test/spec/database/Database.test.js b/test/spec/database/Database.test.js index 2e60626..b6f2c37 100644 --- a/test/spec/database/Database.test.js +++ b/test/spec/database/Database.test.js @@ -4,6 +4,7 @@ import { ALL_EMOJI_NO_ETAG, tick, mockFrenchDataSource, FR_EMOJI, truncatedEmoji, NO_SHORTCODES, mockDataSourceWithNoShortcodes } from '../shared' import trimEmojiData from '../../../src/trimEmojiData' +import { mockFetch, mockGetAndHead } from '../mockFetch.js' describe('database tests', () => { beforeEach(basicBeforeEach) @@ -89,11 +90,11 @@ describe('database tests', () => { const EMPTY = 'empty.json' const NULL_ARRAY = 'null-array.json' const BAD_OBJECT = 'bad-object.json' - fetch.get(NULL, () => new Response('null')) - fetch.get(NOT_ARRAY, () => new Response('{}')) - fetch.get(EMPTY, () => new Response('[]')) - fetch.get(NULL_ARRAY, () => new Response('[null]')) - fetch.get(BAD_OBJECT, () => new Response('[{"missing": true}]')) + mockFetch('get', NULL, 'null') + mockFetch('get', NOT_ARRAY, '{}') + mockFetch('get', EMPTY, '[]') + mockFetch('get', NULL_ARRAY, '[null]') + mockFetch('get', BAD_OBJECT, '[{"missing": true}]') const makeDB = async (dataSource) => { const db = new Database({ dataSource }) @@ -152,6 +153,7 @@ describe('database tests', () => { await db1.ready() const db2 = new Database({ dataSource: ALL_EMOJI }) await db2.ready() + await db2._lazyUpdate // TODO [#407] Skipping this causes an InvalidStateError in IDB await db1.close() expect((await db1.getEmojiByUnicodeOrName('🐵')).annotation).toBe('monkey face') await db2.close() @@ -195,8 +197,7 @@ describe('database tests', () => { test('basic trimEmojiData test', async () => { const trimmed = trimEmojiData(truncatedEmoji) const dataSource = 'trimmed.js' - fetch.get(dataSource, () => new Response(JSON.stringify(trimmed), { headers: { ETag: 'W/trim' } })) - fetch.head(dataSource, () => new Response(null, { headers: { ETag: 'W/trim' } })) + mockGetAndHead(dataSource, trimmed, { headers: { ETag: 'W/trim' } }) const db = new Database({ dataSource }) const emojis = await db.getEmojiBySearchQuery('face') diff --git a/test/spec/database/getEmojiBySearchQuery.test.js b/test/spec/database/getEmojiBySearchQuery.test.js index c433f07..d4713ac 100644 --- a/test/spec/database/getEmojiBySearchQuery.test.js +++ b/test/spec/database/getEmojiBySearchQuery.test.js @@ -2,6 +2,7 @@ import allEmoji from 'emoji-picker-element-data/en/emojibase/data.json' import Database from '../../../src/database/Database' import { pick, omit } from 'lodash-es' import { basicAfterEach, basicBeforeEach, ALL_EMOJI, truncatedEmoji } from '../shared' +import { mockGetAndHead } from '../mockFetch.js' // order can change from version to version const expectToBeSorted = results => { @@ -128,10 +129,7 @@ describe('getEmojiBySearchQuery', () => { const EMOJI_WITH_APOS = 'http://localhost/apos.json' - fetch.get(EMOJI_WITH_APOS, () => new Response(JSON.stringify(emojiWithTwelveOclock), { - headers: { ETag: 'W/apos' } - })) - fetch.head(EMOJI_WITH_APOS, () => new Response(null, { headers: { ETag: 'W/apos' } })) + mockGetAndHead(EMOJI_WITH_APOS, emojiWithTwelveOclock, { headers: { ETag: 'W/apos' } }) const db = new Database({ dataSource: EMOJI_WITH_APOS }) @@ -159,10 +157,7 @@ describe('getEmojiBySearchQuery', () => { const EMOJI = 'http://localhost/apos.json' - fetch.get(EMOJI, () => new Response(JSON.stringify(emoji), { - headers: { ETag: 'W/blond' } - })) - fetch.head(EMOJI, () => new Response(null, { headers: { ETag: 'W/blond' } })) + mockGetAndHead(EMOJI, emoji, { headers: { ETag: 'W/blond' } }) const db = new Database({ dataSource: EMOJI }) diff --git a/test/spec/database/getEmojiByShortcode.test.js b/test/spec/database/getEmojiByShortcode.test.js index 0f645bd..fc73631 100644 --- a/test/spec/database/getEmojiByShortcode.test.js +++ b/test/spec/database/getEmojiByShortcode.test.js @@ -1,5 +1,6 @@ import { ALL_EMOJI, basicAfterEach, basicBeforeEach, truncatedEmoji } from '../shared' import Database from '../../../src/database/Database' +import { mockGetAndHead } from '../mockFetch.js' describe('getEmojiByShortcode', () => { beforeEach(basicBeforeEach) @@ -63,9 +64,7 @@ describe('getEmojiByShortcode', () => { } } - fetch - .get(dataSource, () => new Response(JSON.stringify(emojis), { headers: { ETag: 'W/optional' } })) - .head(dataSource, () => new Response(null, { headers: { ETag: 'W/optional' } })) + mockGetAndHead(dataSource, emojis, { headers: { ETag: 'W/optional' } }) const db = new Database({ dataSource }) diff --git a/test/spec/database/getEmojiByUnicode.test.js b/test/spec/database/getEmojiByUnicode.test.js index 754b04c..5ac89a5 100644 --- a/test/spec/database/getEmojiByUnicode.test.js +++ b/test/spec/database/getEmojiByUnicode.test.js @@ -1,6 +1,7 @@ import allEmoji from 'emoji-picker-element-data/en/emojibase/data.json' import { ALL_EMOJI, basicAfterEach, basicBeforeEach, truncatedEmoji } from '../shared' import Database from '../../../src/database/Database' +import { mockGetAndHead } from '../mockFetch.js' describe('getEmojiByUnicode', () => { beforeEach(basicBeforeEach) @@ -14,10 +15,7 @@ describe('getEmojiByUnicode', () => { ] const EMOJI_WITH_PIRATES = 'http://localhost/pirate.json' - fetch.get(EMOJI_WITH_PIRATES, () => new Response(JSON.stringify(emojiPlusPirateFlag), { - headers: { ETag: 'W/yarrr' } - })) - fetch.head(EMOJI_WITH_PIRATES, () => new Response(null, { headers: { ETag: 'W/yarrr' } })) + mockGetAndHead(EMOJI_WITH_PIRATES, emojiPlusPirateFlag, { headers: { ETag: 'W/yarrr' } }) const db = new Database({ dataSource: EMOJI_WITH_PIRATES }) diff --git a/test/spec/database/offlineFirst.test.js b/test/spec/database/offlineFirst.test.js index 5cbd44f..0f559eb 100644 --- a/test/spec/database/offlineFirst.test.js +++ b/test/spec/database/offlineFirst.test.js @@ -1,6 +1,7 @@ import { vi } from 'vitest' import { ALL_EMOJI, basicAfterEach, basicBeforeEach } from '../shared' import Database from '../../../src/database/Database' +import { mock500GetAndHead } from '../mockFetch.js' describe('offline first', () => { beforeEach(() => { @@ -14,8 +15,7 @@ describe('offline first', () => { await db.close() fetch.reset() - fetch.get(ALL_EMOJI, { body: null, status: 500 }) - fetch.head(ALL_EMOJI, { body: null, status: 500 }) + mock500GetAndHead(ALL_EMOJI) db = new Database({ dataSource: ALL_EMOJI }) await db.ready() @@ -27,8 +27,7 @@ describe('offline first', () => { test('basic error test', async () => { const ERROR = 'error.json' - fetch.get(ERROR, { body: null, status: 500 }) - fetch.head(ERROR, { body: null, status: 500 }) + mock500GetAndHead(ERROR) const db = new Database({ dataSource: ERROR }) await (expect(() => db.ready())).rejects.toThrow() diff --git a/test/spec/database/secondLoad.test.js b/test/spec/database/secondLoad.test.js index 29142e3..72cb077 100644 --- a/test/spec/database/secondLoad.test.js +++ b/test/spec/database/secondLoad.test.js @@ -1,11 +1,11 @@ import { ALL_EMOJI, ALL_EMOJI_NO_ETAG, basicAfterEach, basicBeforeEach, tick, truncatedEmoji } from '../shared' import Database from '../../../src/database/Database' import allEmoji from 'emoji-picker-element-data/en/emojibase/data.json' +import { mockGetAndHead } from '../mockFetch.js' function mockEmoji (dataSource, data, etag) { fetch.reset() - fetch.get(dataSource, () => new Response(JSON.stringify(data), etag && { headers: { ETag: etag } })) - fetch.head(dataSource, () => new Response(null, etag && { headers: { ETag: etag } })) + mockGetAndHead(dataSource, data, etag && { headers: { ETag: etag } }) } async function testDataChange (firstData, secondData, firstCallback, secondCallback, thirdCallback) { @@ -185,8 +185,7 @@ describe('database second load and update', () => { const dataSource2 = 'http://localhost/will-change2.json' // first time - data is v1 - fetch.get(dataSource, () => new Response(JSON.stringify(truncatedEmoji), { headers: { ETag: 'W/xxx' } })) - fetch.head(dataSource, () => new Response(null, { headers: { ETag: 'W/xxx' } })) + mockGetAndHead(dataSource, truncatedEmoji, { headers: { ETag: 'W/xxx' } }) let db = new Database({ dataSource }) await db.ready() @@ -205,8 +204,7 @@ describe('database second load and update', () => { // second time - update, data is v2 fetch.reset() - fetch.get(dataSource2, () => new Response(JSON.stringify(changedEmoji), { headers: { ETag: 'W/yyy' } })) - fetch.head(dataSource2, () => new Response(null, { headers: { ETag: 'W/yyy' } })) + mockGetAndHead(dataSource2, changedEmoji, { headers: { ETag: 'W/yyy' } }) db = new Database({ dataSource: dataSource2 }) await db.ready() @@ -221,8 +219,7 @@ describe('database second load and update', () => { // third time - no update, data is v2 fetch.reset() - fetch.get(dataSource2, () => new Response(JSON.stringify(changedEmoji), { headers: { ETag: 'W/yyy' } })) - fetch.head(dataSource2, () => new Response(null, { headers: { ETag: 'W/yyy' } })) + mockGetAndHead(dataSource2, changedEmoji, { headers: { ETag: 'W/yyy' } }) db = new Database({ dataSource: dataSource2 }) await db.ready() @@ -264,8 +261,7 @@ describe('database second load and update', () => { // second time - update, data is v2 fetch.reset() - fetch.get(ALL_EMOJI, () => new Response(JSON.stringify(changedEmoji), { headers: { ETag: 'W/yyy' } })) - fetch.head(ALL_EMOJI, () => new Response(null, { headers: { ETag: 'W/yyy' } })) + mockGetAndHead(ALL_EMOJI, changedEmoji, { headers: { ETag: 'W/yyy' } }) // open two at once const dbs = [ diff --git a/test/spec/mockFetch.js b/test/spec/mockFetch.js new file mode 100644 index 0000000..b4cfd6c --- /dev/null +++ b/test/spec/mockFetch.js @@ -0,0 +1,31 @@ +// centralize all our fetch mocks in one place so we can have +// consistent timeouts, and smooth over some of the boilerplate + +export function mockFetch (method, url, response, { headers, delay } = {}) { + let responseToUse + if (!response) { + responseToUse = null + } else if (typeof response === 'string') { + responseToUse = response + } else { + responseToUse = JSON.stringify(response) + } + + fetch[method]( + url, + () => new Response(responseToUse, { headers }), + // use a delay of 1 because it's more realistic than a fetch() that resolves in a microtask + { delay: typeof delay === 'number' ? delay : 1 } + ) +} + +// convenience util for mocking a typical get and a head +export function mockGetAndHead (url, response, options = {}) { + mockFetch('get', url, response, options) + mockFetch('head', url, null, options) +} + +export function mock500GetAndHead (url) { + fetch.get(url, { body: null, status: 500 }) + fetch.head(url, { body: null, status: 500 }) +} diff --git a/test/spec/picker/element.test.js b/test/spec/picker/element.test.js index 8edd0f6..1efec36 100644 --- a/test/spec/picker/element.test.js +++ b/test/spec/picker/element.test.js @@ -13,6 +13,7 @@ import enI18n from '../../../src/picker/i18n/en' import Database from '../../../src/database/Database' import { DEFAULT_SKIN_TONE_EMOJI } from '../../../src/picker/constants' import { DEFAULT_DATA_SOURCE } from '../../../src/database/constants' +import { mockGetAndHead } from '../mockFetch.js' const { type } = userEvent // Workaround for clear() not working in shadow roots: https://github.com/testing-library/user-event/issues/1143 @@ -123,8 +124,7 @@ describe('element tests', () => { describe('defaults test', () => { beforeEach(() => { - fetch.get(DEFAULT_DATA_SOURCE, () => new Response(JSON.stringify(truncatedEmoji), { headers: { ETag: 'W/aaa' } })) - fetch.head(DEFAULT_DATA_SOURCE, () => new Response(null, { headers: { ETag: 'W/aaa' } })) + mockGetAndHead(DEFAULT_DATA_SOURCE, truncatedEmoji, { headers: { ETag: 'W/aaa' } }) }) afterEach(basicAfterEach) diff --git a/test/spec/picker/errors.test.js b/test/spec/picker/errors.test.js index 32cff57..87ad85a 100644 --- a/test/spec/picker/errors.test.js +++ b/test/spec/picker/errors.test.js @@ -3,6 +3,7 @@ import Picker from '../../../src/picker/PickerElement' import { ALL_EMOJI, basicAfterEach, basicBeforeEach, tick, truncatedEmoji } from '../shared' import Database from '../../../src/database/Database' import { getByRole, waitFor } from '@testing-library/dom' +import { mock500GetAndHead, mockGetAndHead } from '../mockFetch.js' describe('errors', () => { let errorSpy @@ -34,8 +35,7 @@ describe('errors', () => { test('offline shows an error', async () => { const dataSource = 'error.json' - fetch.get(dataSource, { body: null, status: 500 }) - fetch.head(dataSource, { body: null, status: 500 }) + mock500GetAndHead(dataSource) const picker = new Picker({ dataSource }) const container = picker.shadowRoot @@ -55,10 +55,7 @@ describe('errors', () => { test('slow networks show "Loading"', async () => { const dataSource = 'slow.json' - fetch.get(dataSource, () => new Response(JSON.stringify(truncatedEmoji), { headers: { ETag: 'W/slow' } }), - { delay: 1500 }) - fetch.head(dataSource, () => new Response(null, { headers: { ETag: 'W/slow' } }), - { delay: 1500 }) + mockGetAndHead(dataSource, truncatedEmoji, { headers: { ETag: 'W/slow' }, delay: 1500 }) const picker = new Picker({ dataSource }) const container = picker.shadowRoot diff --git a/test/spec/picker/favorites.test.js b/test/spec/picker/favorites.test.js index f2cae37..7ed17c9 100644 --- a/test/spec/picker/favorites.test.js +++ b/test/spec/picker/favorites.test.js @@ -8,6 +8,7 @@ import allData from 'emoji-picker-element-data/en/emojibase/data.json' import { MOST_COMMONLY_USED_EMOJI } from '../../../src/picker/constants' import { uniqBy } from '../../../src/shared/uniqBy' import { groups } from '../../../src/picker/groups' +import { mockGetAndHead } from '../mockFetch.js' const dataSource = 'with-favs.json' @@ -23,8 +24,7 @@ describe('Favorites UI', () => { ...allData.filter(_ => MOST_COMMONLY_USED_EMOJI.includes(_.emoji)) ], _ => _.emoji) - fetch.get(dataSource, () => new Response(JSON.stringify(dataWithFavorites), { headers: { ETag: 'W/favs' } })) - fetch.head(dataSource, () => new Response(null, { headers: { ETag: 'W/favs' } })) + mockGetAndHead(dataSource, dataWithFavorites, { headers: { ETag: 'W/favs' } }) picker = new Picker({ dataSource, locale: 'en' }) document.body.appendChild(picker) diff --git a/test/spec/shared.js b/test/spec/shared.js index 3ce91f1..bad9635 100644 --- a/test/spec/shared.js +++ b/test/spec/shared.js @@ -2,6 +2,7 @@ import allEmoji from 'emoji-picker-element-data/en/emojibase/data.json' import frEmoji from 'emoji-picker-element-data/fr/cldr/data.json' import allEmojibaseV5Emoji from 'emojibase-data/en/data.json' import { DEFAULT_DATA_SOURCE } from '../../src/database/constants' +import { mockFetch, mockGetAndHead } from './mockFetch.js' export function truncateEmoji (allEmoji) { // just take the first few emoji from each category, or else it takes forever to insert @@ -36,21 +37,11 @@ export const EMOJIBASE_V5 = 'http://localhost/emojibase' export const WITH_ARRAY_SKIN_TONES = 'http://localhost/with-array-skin-tones' export function basicBeforeEach () { - fetch - .get(ALL_EMOJI, () => new Response(JSON.stringify(truncatedEmoji), { - headers: { ETag: 'W/xxx' } - })) - .head(ALL_EMOJI, () => new Response(null, { - headers: { ETag: 'W/xxx' } - })) - .get(ALL_EMOJI_NO_ETAG, truncatedEmoji) - .head(ALL_EMOJI_NO_ETAG, () => new Response(null)) - .get(ALL_EMOJI_MISCONFIGURED_ETAG, () => new Response(JSON.stringify(truncatedEmoji), { - headers: { ETag: 'W/xxx' } - })) - .head(ALL_EMOJI_MISCONFIGURED_ETAG, () => new Response(null)) - .get(DEFAULT_DATA_SOURCE, () => new Response(JSON.stringify(truncatedEmoji), { headers: { ETag: 'W/def' } })) - .head(DEFAULT_DATA_SOURCE, () => new Response(null, { headers: { ETag: 'W/def' } })) + mockGetAndHead(ALL_EMOJI, truncatedEmoji, { headers: { ETag: 'W/xxx' } }) + mockGetAndHead(ALL_EMOJI_NO_ETAG, truncatedEmoji) + mockGetAndHead(DEFAULT_DATA_SOURCE, truncatedEmoji, { headers: { ETag: 'W/def' } }) + mockFetch('get', ALL_EMOJI_MISCONFIGURED_ETAG, truncatedEmoji, { headers: { ETag: 'W/xxx' } }) + mockFetch('head', ALL_EMOJI_MISCONFIGURED_ETAG, null) } export async function basicAfterEach () { @@ -65,8 +56,7 @@ export async function tick (times = 1) { } export function mockFrenchDataSource () { - fetch.get(FR_EMOJI, () => new Response(JSON.stringify(truncatedFrEmoji), { headers: { ETag: 'W/zzz' } })) - fetch.head(FR_EMOJI, () => new Response(null, { headers: { ETag: 'W/zzz' } })) + mockGetAndHead(FR_EMOJI, truncatedFrEmoji, { headers: { ETag: 'W/zzz' } }) } export function mockDataSourceWithNoShortcodes () { @@ -75,22 +65,16 @@ export function mockDataSourceWithNoShortcodes () { delete res.shortcodes return res }) - fetch.get(NO_SHORTCODES, () => new Response(JSON.stringify(noShortcodeEmoji), { headers: { ETag: 'W/noshort' } })) - fetch.head(NO_SHORTCODES, () => new Response(null, { headers: { ETag: 'W/noshort' } })) + mockGetAndHead(NO_SHORTCODES, noShortcodeEmoji, { headers: { ETag: 'W/noshort' } }) } export function mockEmojibaseV5DataSource () { - fetch.get(EMOJIBASE_V5, () => new Response(JSON.stringify(emojibaseV5Emoji), { headers: { ETag: 'W/emojibase' } })) - fetch.head(EMOJIBASE_V5, () => new Response(null, { headers: { ETag: 'W/emojibase' } })) + mockGetAndHead(EMOJIBASE_V5, emojibaseV5Emoji, { headers: { ETag: 'W/emojibase' } }) } export function mockDataSourceWithArraySkinTones () { const emojis = JSON.parse(JSON.stringify(truncatedEmoji)) emojis.push(allEmoji.find(_ => _.annotation === 'people holding hands')) // has two skin tones, one for each person - fetch - .get(WITH_ARRAY_SKIN_TONES, () => ( - new Response(JSON.stringify(emojis), { headers: { ETag: 'W/noshort' } })) - ) - .head(WITH_ARRAY_SKIN_TONES, () => new Response(null, { headers: { ETag: 'W/noshort' } })) + mockGetAndHead(WITH_ARRAY_SKIN_TONES, emojis, { headers: { ETag: 'W/noshort' } }) }