test: centralize fetch mocks in one place

This commit is contained in:
Nolan Lawson 2024-03-10 20:07:58 -07:00
parent 55872ba996
commit 49cc5e529f
11 changed files with 72 additions and 72 deletions

View File

@ -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')

View File

@ -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 })

View File

@ -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 })

View File

@ -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 })

View File

@ -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()

View File

@ -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 = [

31
test/spec/mockFetch.js Normal file
View File

@ -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 })
}

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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' } })
}