fix: fix getEmojiByShortcode, add tests

This commit is contained in:
Nolan Lawson 2020-06-06 19:36:45 -07:00
parent 4904e85bd2
commit 44d4398d24
10 changed files with 280 additions and 148 deletions

View File

@ -1,6 +1,5 @@
module.exports = {
testMatch: [
'<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}',
'<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}'
],
transform: {

View File

@ -10,7 +10,7 @@ import { versionsAndTestEmoji } from './bin/versionsAndTestEmoji'
const dev = process.env.NODE_ENV !== 'production'
const svelte = dev ? hotSvelte : mainSvelte
// Build Database.js and Picker.js as separate modules at build times so that they are properly tree-shakeable.
// Build Database.test.js and Picker.js as separate modules at build times so that they are properly tree-shakeable.
// Most of this has to happen because customElements.define() has side effects
const baseConfig = {
plugins: [
@ -44,7 +44,7 @@ const entryPoints = [
output: './picker.js'
},
{
input: './src/database/Database.js',
input: './src/database/Database.test.js',
output: './database.js'
}
]

View File

@ -1,80 +1,15 @@
import Database from '../Database'
import allEmoji from 'emojibase-data/en/data.json'
import { pick } from 'lodash-es'
import frEmoji from 'emojibase-data/fr/data.json'
import {
basicAfterEach, basicBeforeEach, ALL_EMOJI, ALL_EMOJI_MISCONFIGURED_ETAG,
ALL_EMOJI_NO_ETAG, truncatedEmoji, truncateEmoji
} from './shared'
const { Response } = fetch
function truncateEmoji (allEmoji) {
// just take the first few emoji from each category, or else it takes forever to insert
// into fake-indexeddb: https://github.com/dumbmatter/fakeIndexedDB/issues/44
const groupsToEmojis = new Map()
for (const emoji of allEmoji) {
let emojis = groupsToEmojis.get(emoji.group)
if (!emojis) {
emojis = []
groupsToEmojis.set(emoji.group, emojis)
}
if (emojis.length < 20) {
emojis.push(emoji)
}
}
return [...groupsToEmojis.values()].flat()
}
const truncatedEmoji = truncateEmoji(allEmoji)
const ALL_EMOJI = 'http://localhost/emoji.json'
const ALL_EMOJI_NO_ETAG = 'http://localhost/emoji-no-etag.json'
const ALL_EMOJI_MISCONFIGURED_ETAG = 'http://localhost/emoji-misconfigured-etag.json'
beforeEach(() => {
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))
})
afterEach(() => {
fetch.mockClear()
fetch.reset()
})
describe('fetch tests', () => {
test('make sure fetch-mock-jest is working correctly', async () => {
expect(fetch).toHaveBeenCalledTimes(0)
const resp = await fetch(ALL_EMOJI)
expect(resp.headers.get('etag')).toBe('W/xxx')
expect(await (resp).json()).toStrictEqual(truncatedEmoji)
expect(fetch).toHaveBeenCalledTimes(1)
expect(fetch).toHaveBeenLastCalledWith(ALL_EMOJI, undefined)
})
test('make sure fetch-mock-jest is working correctly 2', async () => {
expect(fetch).toHaveBeenCalledTimes(0)
const resp = await fetch(ALL_EMOJI_NO_ETAG)
expect(resp.headers.get('etag')).toBeFalsy()
expect(await (resp).json()).toStrictEqual(truncatedEmoji)
expect(fetch).toHaveBeenCalledTimes(1)
expect(fetch).toHaveBeenLastCalledWith(ALL_EMOJI_NO_ETAG, undefined)
})
test('make sure fetch-mock-jest is working correctly 3', async () => {
expect(fetch).toHaveBeenCalledTimes(0)
const resp = await fetch(ALL_EMOJI, { method: 'HEAD' })
expect(resp.headers.get('etag')).toBe('W/xxx')
expect(fetch).toHaveBeenCalledTimes(1)
expect(fetch).toHaveBeenLastCalledWith(ALL_EMOJI, { method: 'HEAD' })
})
})
beforeEach(basicBeforeEach)
afterEach(basicAfterEach)
describe('database tests', () => {
test('basic emoji database test', async () => {
@ -248,64 +183,57 @@ describe('database tests', () => {
await db.delete()
})
test('getEmojiBySearchQuery', async () => {
const db = new Database({ dataSource: ALL_EMOJI })
test('URL change causes an update', async () => {
const dataSource = 'http://localhost/will-change.json'
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' } }))
let db = new Database({ dataSource })
await db.ready()
const search = async query => (await db.getEmojiBySearchQuery(query)).map(_ => pick(_, ['annotation', 'order']))
expect(await search('face')).toStrictEqual([
{ annotation: 'grinning face', order: 1 },
{ annotation: 'grinning face with big eyes', order: 2 },
{ annotation: 'grinning face with smiling eyes', order: 3 },
{ annotation: 'beaming face with smiling eyes', order: 4 },
{ annotation: 'grinning squinting face', order: 5 },
{ annotation: 'grinning face with sweat', order: 6 },
{ annotation: 'rolling on the floor laughing', order: 7 },
{ annotation: 'face with tears of joy', order: 8 },
{ annotation: 'slightly smiling face', order: 9 },
{ annotation: 'upside-down face', order: 10 },
{ annotation: 'winking face', order: 11 },
{ annotation: 'smiling face with smiling eyes', order: 12 },
{ annotation: 'smiling face with halo', order: 13 },
{ annotation: 'smiling face with hearts', order: 14 },
{ annotation: 'smiling face with heart-eyes', order: 15 },
{ annotation: 'star-struck', order: 16 },
{ annotation: 'face blowing a kiss', order: 17 },
{ annotation: 'kissing face', order: 18 },
{ annotation: 'smiling face', order: 20 },
{ annotation: 'kissing face with closed eyes', order: 21 },
{ annotation: 'monkey face', order: 2657 },
{ annotation: 'dog face', order: 2661 },
{ annotation: 'wolf', order: 2666 },
{ annotation: 'fox', order: 2667 },
{ annotation: 'cat face', order: 2669 },
{ annotation: 'lion', order: 2672 },
{ annotation: 'tiger face', order: 2673 },
{ annotation: 'horse face', order: 2676 }
])
expect(await search('monk')).toStrictEqual([
{ annotation: 'monkey face', order: 2657 },
{ annotation: 'monkey', order: 2658 }
])
expect(await search('monkey')).toStrictEqual([
{ annotation: 'monkey face', order: 2657 },
{ annotation: 'monkey', order: 2658 }
])
expect(await search('monkey')).toStrictEqual([
{ annotation: 'monkey face', order: 2657 },
{ annotation: 'monkey', order: 2658 }
])
expect(await search('MoNkEy')).toStrictEqual([
{ annotation: 'monkey face', order: 2657 },
{ annotation: 'monkey', order: 2658 }
])
expect(await search('monkey fac')).toStrictEqual([
{ annotation: 'monkey face', order: 2657 }
])
expect(await search('face monk')).toStrictEqual([
{ annotation: 'monkey face', order: 2657 }
])
expect(await search('monkey facee')).toStrictEqual([])
expect(await search('monk face')).toStrictEqual([])
expect(fetch).toHaveBeenCalledTimes(1)
expect(fetch).toHaveBeenLastCalledWith(dataSource, undefined)
expect((await db.getEmojiByShortcode('rofl')).annotation).toBe('rolling on the floor laughing')
expect(await db.getEmojiByShortcode('weary_cat')).toBeFalsy()
await db.close()
const changedEmoji = JSON.parse(JSON.stringify(truncatedEmoji))
const roflIndex = allEmoji.findIndex(_ => _.annotation === 'rolling on the floor laughing')
changedEmoji[roflIndex] = allEmoji.find(_ => _.annotation === 'pineapple') // replace rofl
// second time - update, data is v2
fetch.mockClear()
fetch.reset()
fetch.get(dataSource2, () => new Response(JSON.stringify(changedEmoji), { headers: { ETag: 'W/yyy' } }))
fetch.head(dataSource2, () => new Response(null, { headers: { ETag: 'W/yyy' } }))
db = new Database({ dataSource: dataSource2 })
await db.ready()
await new Promise(resolve => setTimeout(resolve, 50))
expect(fetch).toHaveBeenCalledTimes(2)
expect(fetch).toHaveBeenLastCalledWith(dataSource2, undefined)
expect(fetch).toHaveBeenNthCalledWith(1, dataSource2, { method: 'HEAD' })
expect((await db.getEmojiByShortcode('rofl'))).toBeFalsy()
expect((await db.getEmojiByShortcode('pineapple')).annotation).toBe('pineapple')
// third time - no update, data is v2
fetch.mockClear()
fetch.reset()
fetch.get(dataSource2, () => new Response(JSON.stringify(changedEmoji), { headers: { ETag: 'W/yyy' } }))
fetch.head(dataSource2, () => new Response(null, { headers: { ETag: 'W/yyy' } }))
db = new Database({ dataSource: dataSource2 })
await db.ready()
await new Promise(resolve => setTimeout(resolve, 50))
expect(fetch).toHaveBeenCalledTimes(1)
expect(fetch).toHaveBeenLastCalledWith(dataSource2, { method: 'HEAD' })
expect((await db.getEmojiByShortcode('rofl'))).toBeFalsy()
expect((await db.getEmojiByShortcode('pineapple')).annotation).toBe('pineapple')
await db.delete()
})
@ -342,8 +270,46 @@ describe('database tests', () => {
await db.delete()
})
// test - race conditions opening two DBs with same name at same time ?
// test - URL changed
// test - are shortcodes unique?
// test - separate languages
test('multiple databases, close one', async () => {
const db1 = new Database({ dataSource: ALL_EMOJI })
const db2 = new Database({ dataSource: ALL_EMOJI })
await db1.close()
await expect(() => db1.getEmojiByGroup(1)).rejects.toThrow()
await expect(() => db2.getEmojiByGroup(1)).rejects.toThrow()
const db3 = new Database({ dataSource: ALL_EMOJI })
await db3.ready()
await new Promise(resolve => setTimeout(resolve, 50))
await db3.delete()
})
test('multiple databases, delete one', async () => {
const db1 = new Database({ dataSource: ALL_EMOJI })
const db2 = new Database({ dataSource: ALL_EMOJI })
await db1.delete()
await expect(() => db1.getEmojiByGroup(1)).rejects.toThrow()
await expect(() => db2.getEmojiByGroup(1)).rejects.toThrow()
})
test('multiple databases in multiple locales', async () => {
const truncatedFrEmoji = truncateEmoji(frEmoji)
const dataSourceFr = 'http://localhost/fr.json'
fetch.get(dataSourceFr, () => new Response(JSON.stringify(truncatedFrEmoji), { headers: { ETag: 'W/zzz' } }))
fetch.head(dataSourceFr, () => new Response(null, { headers: { ETag: 'W/zzz' } }))
const en = new Database({ dataSource: ALL_EMOJI })
const fr = new Database({ dataSource: dataSourceFr, locale: 'fr' })
expect((await en.getEmojiBySearchQuery('monkey face')).map(_ => _.annotation)).toStrictEqual(['monkey face'])
expect((await fr.getEmojiBySearchQuery('tête singe')).map(_ => _.annotation)).toStrictEqual(['tête de singe'])
await en.delete()
// deleting en has no impact on fr
await expect(() => en.getEmojiBySearchQuery('monkey face')).rejects.toThrow()
expect((await fr.getEmojiBySearchQuery('tête singe')).map(_ => _.annotation)).toStrictEqual(['tête de singe'])
await en.delete()
await fr.delete()
})
})

View File

@ -0,0 +1,32 @@
import { ALL_EMOJI, ALL_EMOJI_NO_ETAG, basicAfterEach, basicBeforeEach, truncatedEmoji } from './shared'
beforeEach(basicBeforeEach)
afterEach(basicAfterEach)
describe('basic fetch tests', () => {
test('make sure fetch-mock-jest is working correctly', async () => {
expect(fetch).toHaveBeenCalledTimes(0)
const resp = await fetch(ALL_EMOJI)
expect(resp.headers.get('etag')).toBe('W/xxx')
expect(await (resp).json()).toStrictEqual(truncatedEmoji)
expect(fetch).toHaveBeenCalledTimes(1)
expect(fetch).toHaveBeenLastCalledWith(ALL_EMOJI, undefined)
})
test('make sure fetch-mock-jest is working correctly 2', async () => {
expect(fetch).toHaveBeenCalledTimes(0)
const resp = await fetch(ALL_EMOJI_NO_ETAG)
expect(resp.headers.get('etag')).toBeFalsy()
expect(await (resp).json()).toStrictEqual(truncatedEmoji)
expect(fetch).toHaveBeenCalledTimes(1)
expect(fetch).toHaveBeenLastCalledWith(ALL_EMOJI_NO_ETAG, undefined)
})
test('make sure fetch-mock-jest is working correctly 3', async () => {
expect(fetch).toHaveBeenCalledTimes(0)
const resp = await fetch(ALL_EMOJI, { method: 'HEAD' })
expect(resp.headers.get('etag')).toBe('W/xxx')
expect(fetch).toHaveBeenCalledTimes(1)
expect(fetch).toHaveBeenLastCalledWith(ALL_EMOJI, { method: 'HEAD' })
})
})

View File

@ -0,0 +1,69 @@
import Database from '../Database'
import { pick } from 'lodash-es'
import { basicAfterEach, basicBeforeEach, ALL_EMOJI } from './shared'
beforeEach(basicBeforeEach)
afterEach(basicAfterEach)
describe('getEmojiBySearchQuery', () => {
test('basic searches', async () => {
const db = new Database({ dataSource: ALL_EMOJI })
await db.ready()
const search = async query => (await db.getEmojiBySearchQuery(query)).map(_ => pick(_, ['annotation', 'order']))
expect(await search('face')).toStrictEqual([
{ annotation: 'grinning face', order: 1 },
{ annotation: 'grinning face with big eyes', order: 2 },
{ annotation: 'grinning face with smiling eyes', order: 3 },
{ annotation: 'beaming face with smiling eyes', order: 4 },
{ annotation: 'grinning squinting face', order: 5 },
{ annotation: 'grinning face with sweat', order: 6 },
{ annotation: 'rolling on the floor laughing', order: 7 },
{ annotation: 'face with tears of joy', order: 8 },
{ annotation: 'slightly smiling face', order: 9 },
{ annotation: 'upside-down face', order: 10 },
{ annotation: 'winking face', order: 11 },
{ annotation: 'smiling face with smiling eyes', order: 12 },
{ annotation: 'smiling face with halo', order: 13 },
{ annotation: 'smiling face with hearts', order: 14 },
{ annotation: 'smiling face with heart-eyes', order: 15 },
{ annotation: 'star-struck', order: 16 },
{ annotation: 'face blowing a kiss', order: 17 },
{ annotation: 'kissing face', order: 18 },
{ annotation: 'smiling face', order: 20 },
{ annotation: 'kissing face with closed eyes', order: 21 },
{ annotation: 'monkey face', order: 2657 },
{ annotation: 'dog face', order: 2661 },
{ annotation: 'wolf', order: 2666 },
{ annotation: 'fox', order: 2667 },
{ annotation: 'cat face', order: 2669 },
{ annotation: 'lion', order: 2672 },
{ annotation: 'tiger face', order: 2673 },
{ annotation: 'horse face', order: 2676 }
])
expect(await search('monk')).toStrictEqual([
{ annotation: 'monkey face', order: 2657 },
{ annotation: 'monkey', order: 2658 }
])
expect(await search('monkey')).toStrictEqual([
{ annotation: 'monkey face', order: 2657 },
{ annotation: 'monkey', order: 2658 }
])
expect(await search('monkey')).toStrictEqual([
{ annotation: 'monkey face', order: 2657 },
{ annotation: 'monkey', order: 2658 }
])
expect(await search('MoNkEy')).toStrictEqual([
{ annotation: 'monkey face', order: 2657 },
{ annotation: 'monkey', order: 2658 }
])
expect(await search('monkey fac')).toStrictEqual([
{ annotation: 'monkey face', order: 2657 }
])
expect(await search('face monk')).toStrictEqual([
{ annotation: 'monkey face', order: 2657 }
])
expect(await search('monkey facee')).toStrictEqual([])
expect(await search('monk face')).toStrictEqual([])
await db.delete()
})
})

View File

@ -0,0 +1,20 @@
import { ALL_EMOJI, basicAfterEach, basicBeforeEach } from './shared'
import Database from '../Database'
beforeEach(basicBeforeEach)
afterEach(basicAfterEach)
describe('getEmojiByShortcode', () => {
test('basic test', async () => {
const db = new Database({ dataSource: ALL_EMOJI })
expect((await db.getEmojiByShortcode('monkey')).annotation).toEqual('monkey')
expect((await db.getEmojiByShortcode('monkey_face')).annotation).toEqual('monkey face')
expect((await db.getEmojiByShortcode('MONKEY')).annotation).toEqual('monkey')
expect((await db.getEmojiByShortcode('MONKEY_FACE')).annotation).toEqual('monkey face')
expect((await db.getEmojiByShortcode('face monkey'))).toBe(null)
expect((await db.getEmojiByShortcode('monk'))).toBe(null)
expect((await db.getEmojiByShortcode(':monkey_face:'))).toBe(null)
await db.delete()
})
})

View File

@ -0,0 +1,47 @@
import allEmoji from 'emojibase-data/en/data.json'
const { Response } = fetch
export function truncateEmoji (allEmoji) {
// just take the first few emoji from each category, or else it takes forever to insert
// into fake-indexeddb: https://github.com/dumbmatter/fakeIndexedDB/issues/44
const groupsToEmojis = new Map()
for (const emoji of allEmoji) {
let emojis = groupsToEmojis.get(emoji.group)
if (!emojis) {
emojis = []
groupsToEmojis.set(emoji.group, emojis)
}
if (emojis.length < 20) {
emojis.push(emoji)
}
}
return [...groupsToEmojis.values()].flat()
}
export const truncatedEmoji = truncateEmoji(allEmoji)
export const ALL_EMOJI = 'http://localhost/emoji.json'
export const ALL_EMOJI_NO_ETAG = 'http://localhost/emoji-no-etag.json'
export const ALL_EMOJI_MISCONFIGURED_ETAG = 'http://localhost/emoji-misconfigured-etag.json'
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))
}
export function basicAfterEach () {
fetch.mockClear()
fetch.reset()
}

View File

@ -126,16 +126,11 @@ export async function getEmojiBySearchQuery (db, query) {
}
export async function getEmojiByShortcode (db, shortcode) {
shortcode = shortcode.toLowerCase()
return dbPromise(db, STORE_EMOJI, MODE_READONLY, (emojiStore, cb) => {
const range = IDBKeyRange.only(shortcode)
emojiStore.index(INDEX_TOKENS).getAll(range).onsuccess = e => {
// of course, we could add an extra index just for shortcodes, but it seems
// simpler to just re-use the existing tokens index and filter in-memory
const results = e.target.result.filter(emoji => emoji.shortcodes.includes(shortcode))
cb(results[0])
}
})
const emojis = await getEmojiBySearchQuery(db, shortcode)
return emojis.filter(_ => {
const lowerShortcodes = _.shortcodes.map(_ => _.toLowerCase())
return lowerShortcodes.includes(shortcode.toLowerCase())
})[0] || null
}
export async function getEmojiByUnicode (db, unicode) {

View File

@ -7,8 +7,8 @@ export function transformEmojiBaseData (emojiBaseData) {
const res = emojiBaseData.map(({ annotation, emoticon, group, order, shortcodes, tags, emoji, version }) => {
const tokens = [...new Set(
[
...shortcodes.map(extractTokens).flat(),
...tags.map(extractTokens).flat(),
...(shortcodes || []).map(extractTokens).flat(),
...(tags || []).map(extractTokens).flat(),
...extractTokens(annotation),
emoticon
]

View File

@ -46,6 +46,10 @@ export default class Database {
/**
* Return a single emoji matching the shortcode, or null if not found.
*
* The colons around the shortcode should not be included when querying, e.g.
* use "slight_smile", not ":slight_smile:". Uppercase versus lowercase
* does not matter.
* @param shortcode
*/
getEmojiByShortcode(shortcode: string): Promise<Emoji | null> {