feat: implement favorites
This commit is contained in:
parent
af7180bada
commit
596a702cb9
|
@ -108,6 +108,7 @@
|
|||
"customElements",
|
||||
"CustomEvent",
|
||||
"fetch",
|
||||
"getComputedStyle",
|
||||
"indexedDB",
|
||||
"IDBKeyRange",
|
||||
"matchMedia",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { uniqBy } from './uniqBy'
|
||||
import { uniqBy } from '../../shared/uniqBy'
|
||||
|
||||
export function uniqEmoji (emojis) {
|
||||
return uniqBy(emojis, _ => _.unicode)
|
||||
|
|
|
@ -93,7 +93,7 @@
|
|||
aria-hidden="true">
|
||||
<div class="indicator"
|
||||
style={indicatorStyle}
|
||||
use:calculateWidth>
|
||||
use:calculateIndicatorWidth>
|
||||
</div>
|
||||
</div>
|
||||
{#if message}
|
||||
|
@ -110,14 +110,15 @@
|
|||
role={searchMode ? 'listbox' : 'menu'}
|
||||
aria-label={searchMode ? i18n.searchResultsLabel : i18n.categories[currentCategory.name]}
|
||||
id="search-results"
|
||||
use:calculateEmojiGridWith
|
||||
>
|
||||
{#each currentEmojis as emoji, i (emoji.unicode)}
|
||||
<button role={searchMode ? 'option' : 'menuitem'}
|
||||
{...(searchMode ? { 'aria-selected': i == activeSearchItem } : null)}
|
||||
aria-label={emoji.unicode + ', ' + emoji.shortcodes.join(', ')}
|
||||
title={emoji.shortcodes.join(', ')}
|
||||
class="tabpanel-emoji emoji {searchMode && i === activeSearchItem ? 'active' : ''}"
|
||||
id="emoji-{emoji.unicode}">
|
||||
aria-label={emojiLabel(emoji)}
|
||||
title={emojiTitle(emoji)}
|
||||
class="emoji {searchMode && i === activeSearchItem ? 'active' : ''}"
|
||||
data-emoji={emoji.unicode}>
|
||||
{#if currentSkinTone > 0 && emoji.skins && emoji.skins[currentSkinTone]}
|
||||
{emoji.skins[currentSkinTone]}
|
||||
{:else}
|
||||
|
@ -128,6 +129,26 @@
|
|||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="favorites emoji-menu"
|
||||
role="menu"
|
||||
aria-label={i18n.favoritesLabel}
|
||||
style="padding-right: {scrollbarWidth}px;"
|
||||
on:click={onEmojiClick}
|
||||
data-testid="favorites">
|
||||
{#each currentFavorites as emoji, i (emoji.unicode)}
|
||||
<button role="menuitem"
|
||||
aria-label={emojiLabel(emoji)}
|
||||
title={emojiTitle(emoji)}
|
||||
class="emoji"
|
||||
data-emoji={emoji.unicode}>
|
||||
{#if currentSkinTone > 0 && emoji.skins && emoji.skins[currentSkinTone]}
|
||||
{emoji.skins[currentSkinTone]}
|
||||
{:else}
|
||||
{emoji.unicode}
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<div aria-hidden="true" class="hidden abs-pos">
|
||||
<button tabindex="-1" class="emoji baseline-emoji" bind:this={baselineEmoji}>😀</button>
|
||||
</div>
|
||||
|
|
|
@ -16,7 +16,13 @@ import { applySkinTone } from '../../utils/applySkinTone'
|
|||
import { halt } from '../../utils/halt'
|
||||
import { incrementOrDecrement } from '../../utils/incrementOrDecrement'
|
||||
import { tick } from 'svelte'
|
||||
import { DEFAULT_SKIN_TONE_EMOJI, TIMEOUT_BEFORE_LOADING_MESSAGE } from '../../constants'
|
||||
import {
|
||||
DEFAULT_NUM_COLUMNS,
|
||||
DEFAULT_SKIN_TONE_EMOJI,
|
||||
MOST_COMMONLY_USED_EMOJI,
|
||||
TIMEOUT_BEFORE_LOADING_MESSAGE
|
||||
} from '../../constants'
|
||||
import { uniqBy } from '../../../shared/uniqBy'
|
||||
|
||||
let skinToneEmoji = DEFAULT_SKIN_TONE_EMOJI
|
||||
let i18n = enI18n
|
||||
|
@ -42,6 +48,11 @@ let style = '' // eslint-disable-line no-unused-vars
|
|||
let skinToneButtonLabel = '' // eslint-disable-line no-unused-vars
|
||||
let skinToneTextForSkinTone = ''
|
||||
let skinTones = []
|
||||
let currentFavorites = [] // eslint-disable-line no-unused-vars
|
||||
let defaultFavoriteEmojisPromise
|
||||
let numColumns = DEFAULT_NUM_COLUMNS
|
||||
let scrollbarWidth = 0 // eslint-disable-line no-unused-vars
|
||||
let shouldUpdateFavorites = {} // hack to force svelte to recalc favorites
|
||||
|
||||
const getBaselineEmojiWidth = thunk(() => calculateTextWidth(baselineEmoji))
|
||||
|
||||
|
@ -62,7 +73,6 @@ $: {
|
|||
}, TIMEOUT_BEFORE_LOADING_MESSAGE)
|
||||
try {
|
||||
await database.ready()
|
||||
currentSkinTone = await database.getPreferredSkinTone()
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
message = i18n.networkError
|
||||
|
@ -107,15 +117,80 @@ $: indicatorStyle = (resizeObserverSupported
|
|||
: `transform: translateX(${currentCategoryIndex * 100}%);`// fallback to percent-based
|
||||
)
|
||||
|
||||
$: {
|
||||
async function updatePreferredSkinTone () {
|
||||
if (database) {
|
||||
currentSkinTone = await database.getPreferredSkinTone()
|
||||
}
|
||||
}
|
||||
updatePreferredSkinTone()
|
||||
}
|
||||
|
||||
$: {
|
||||
async function updateDefaultFavoriteEmojis () {
|
||||
if (database) {
|
||||
defaultFavoriteEmojisPromise = (await Promise.all(MOST_COMMONLY_USED_EMOJI.map(unicode => (
|
||||
database.getEmojiByUnicode(unicode)
|
||||
)))).filter(Boolean) // filter because in Jest tests we don't have all the emoji in the DB
|
||||
}
|
||||
}
|
||||
updateDefaultFavoriteEmojis()
|
||||
}
|
||||
|
||||
$: {
|
||||
async function getFavorites () {
|
||||
const dbFavorites = await database.getTopFavoriteEmoji(numColumns)
|
||||
const defaultFavorites = await defaultFavoriteEmojisPromise
|
||||
const favs = uniqBy([
|
||||
...dbFavorites,
|
||||
...defaultFavorites
|
||||
], _ => _.unicode).slice(0, numColumns)
|
||||
return summarizeEmojis(favs)
|
||||
}
|
||||
|
||||
async function updateFavorites () {
|
||||
currentFavorites = await getFavorites()
|
||||
}
|
||||
|
||||
if (database && shouldUpdateFavorites) {
|
||||
updateFavorites()
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function calculateWidth (indicator) {
|
||||
function calculateIndicatorWidth (node) {
|
||||
return calculateWidth(node, width => {
|
||||
computedIndicatorWidth = width
|
||||
})
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function calculateEmojiGridWith (node) {
|
||||
return calculateWidth(node, width => {
|
||||
// Whenever the main emoji grid changes size, we need to
|
||||
// 1) Re-calculate the --num-columns var because it may have changed
|
||||
// 2) Re-calculate the scrollbar width because it may have changed (i.e. because the number of items changed)
|
||||
const propValue = getComputedStyle(rootElement).getPropertyValue('--num-columns')
|
||||
const newNumColumns = parseInt(propValue, 10) || DEFAULT_NUM_COLUMNS // in Jest we can't compute custom props
|
||||
const parentWidth = node.parentElement.getBoundingClientRect().width
|
||||
const newScrollbarWidth = parentWidth - width
|
||||
numColumns = newNumColumns
|
||||
scrollbarWidth = newScrollbarWidth
|
||||
})
|
||||
}
|
||||
|
||||
function calculateWidth (node, onUpdate) {
|
||||
let resizeObserver
|
||||
/* istanbul ignore if */
|
||||
if (resizeObserverSupported) {
|
||||
resizeObserver = new ResizeObserver(entries => {
|
||||
computedIndicatorWidth = entries[0].contentRect.width
|
||||
onUpdate(entries[0].contentRect.width)
|
||||
})
|
||||
resizeObserver.observe(node)
|
||||
} else {
|
||||
requestAnimationFrame(() => {
|
||||
onUpdate(node.getBoundingClientRect().width)
|
||||
})
|
||||
resizeObserver.observe(indicator)
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -167,7 +242,7 @@ function checkZwjSupport (zwjEmojisToCheck) {
|
|||
mark('checkZwjSupport')
|
||||
const rootNode = rootElement.getRootNode()
|
||||
for (const emoji of zwjEmojisToCheck) {
|
||||
const domNode = rootNode.getElementById(`emoji-${emoji.unicode}`)
|
||||
const domNode = rootNode.querySelector(`[data-emoji=${JSON.stringify(emoji.unicode)}]`)
|
||||
if (!domNode) { // happens rarely, mostly in jest tests
|
||||
continue
|
||||
}
|
||||
|
@ -295,16 +370,19 @@ async function clickEmoji (unicode) {
|
|||
emojiSupportLevelPromise,
|
||||
database.getEmojiByUnicode(unicode)
|
||||
])
|
||||
let unicodeWithSkin = unicode
|
||||
if (currentSkinTone) {
|
||||
const foundSkin = (emoji.skins || []).find(_ => _.tone === currentSkinTone)
|
||||
if (foundSkin && foundSkin.version <= emojiSupportLevel) {
|
||||
unicode = foundSkin.unicode
|
||||
unicodeWithSkin = foundSkin.unicode
|
||||
}
|
||||
}
|
||||
await database.incrementFavoriteEmojiCount(unicode)
|
||||
shouldUpdateFavorites = shouldUpdateFavorites // eslint-disable-line no-self-assign
|
||||
fireEvent('emoji-click', {
|
||||
emoji,
|
||||
skinTone: currentSkinTone,
|
||||
unicode
|
||||
unicode: unicodeWithSkin
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -315,7 +393,7 @@ async function onEmojiClick (event) {
|
|||
return
|
||||
}
|
||||
halt(event)
|
||||
const unicode = target.id.substring(6) // remove 'emoji-'
|
||||
const unicode = target.dataset.emoji
|
||||
|
||||
clickEmoji(unicode)
|
||||
}
|
||||
|
@ -386,6 +464,15 @@ async function onSkinToneOptionsBlur () {
|
|||
skinTonePickerExpanded = false
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function emojiLabel (emoji) {
|
||||
return emoji.unicode + ', ' + emoji.shortcodes.join(', ')
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function emojiTitle (emoji) {
|
||||
return emoji.shortcodes.join(', ')
|
||||
}
|
||||
|
||||
export {
|
||||
database,
|
||||
|
|
|
@ -156,6 +156,15 @@ input.search {
|
|||
}
|
||||
}
|
||||
|
||||
// favorites
|
||||
|
||||
.favorites {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border-top: var(--border-size) solid var(--border-color);
|
||||
contain: content;
|
||||
}
|
||||
|
||||
//
|
||||
// unsupported warning
|
||||
//
|
||||
|
|
|
@ -1,2 +1,24 @@
|
|||
export const TIMEOUT_BEFORE_LOADING_MESSAGE = 1000 // 1 second
|
||||
export const DEFAULT_SKIN_TONE_EMOJI = '🖐️'
|
||||
export const DEFAULT_NUM_COLUMNS = 8
|
||||
|
||||
// Based on https://fivethirtyeight.com/features/the-100-most-used-emojis/ and
|
||||
// https://blog.emojipedia.org/facebook-reveals-most-and-least-used-emojis/ with
|
||||
// a bit of my own curation. (E.g. avoid the "OK" gesture because of connotations:
|
||||
// https://emojipedia.org/ok-hand/)
|
||||
export const MOST_COMMONLY_USED_EMOJI = [
|
||||
'😊',
|
||||
'😒',
|
||||
'♥️',
|
||||
'👍️',
|
||||
'😍',
|
||||
'😂',
|
||||
'😭',
|
||||
'☺️',
|
||||
'😔',
|
||||
'😩',
|
||||
'😏',
|
||||
'💕',
|
||||
'🙌',
|
||||
'😘'
|
||||
]
|
||||
|
|
|
@ -4,6 +4,7 @@ export default {
|
|||
networkError: 'Could not load emoji. Try refreshing.',
|
||||
regionLabel: 'Emoji picker',
|
||||
search: 'Search',
|
||||
favoritesLabel: 'Favorites',
|
||||
skinToneLabel: 'Choose a skin tone (currently {skinTone})',
|
||||
skinToneDescription: 'When expanded, press up or down to select and enter to choose.',
|
||||
skinTonesTitle: 'Skin tones',
|
||||
|
|
|
@ -52,6 +52,7 @@ export interface I18n {
|
|||
skinTones: string[]
|
||||
searchDescription: string
|
||||
skinToneDescription: string
|
||||
favoritesLabel: string
|
||||
}
|
||||
|
||||
export interface I18nCategories {
|
||||
|
|
|
@ -11,13 +11,14 @@ describe('Picker tests', () => {
|
|||
let picker
|
||||
let container
|
||||
|
||||
const { getAllByRole, getByRole, queryAllByRole } = new Proxy(testingLibrary, {
|
||||
const proxy = new Proxy(testingLibrary, {
|
||||
get (obj, prop) {
|
||||
return function (...args) {
|
||||
return obj[prop](container, ...args)
|
||||
}
|
||||
}
|
||||
})
|
||||
const { getAllByRole, getByRole, queryAllByRole } = proxy
|
||||
|
||||
const activeElement = () => container.getRootNode().activeElement
|
||||
|
||||
|
@ -26,7 +27,9 @@ describe('Picker tests', () => {
|
|||
picker = new Picker({ dataSource: ALL_EMOJI, locale: 'en' })
|
||||
document.body.appendChild(picker)
|
||||
container = picker.shadowRoot.querySelector('.picker')
|
||||
await waitFor(() => expect(getAllByRole('menuitem')).toHaveLength(numInGroup1))
|
||||
await waitFor(() => expect(
|
||||
testingLibrary.getAllByRole(getByRole('tabpanel'), 'menuitem')).toHaveLength(numInGroup1)
|
||||
)
|
||||
})
|
||||
afterEach(async () => {
|
||||
basicAfterEach()
|
||||
|
@ -43,12 +46,12 @@ describe('Picker tests', () => {
|
|||
expect(getAllByRole('tab')).toHaveLength(9)
|
||||
|
||||
expect(getByRole('tab', { name: 'Smileys and emoticons', selected: true })).toBeVisible()
|
||||
expect(getAllByRole('menuitem')).toHaveLength(numInGroup1)
|
||||
expect(testingLibrary.getAllByRole(getByRole('tabpanel'), 'menuitem')).toHaveLength(numInGroup1)
|
||||
|
||||
expect(getByRole('tab', { name: 'People and body' })).toBeVisible()
|
||||
fireEvent.click(getByRole('tab', { name: 'People and body' }))
|
||||
|
||||
await waitFor(() => expect(getAllByRole('menuitem')).toHaveLength(numInGroup2))
|
||||
await waitFor(() => expect(testingLibrary.getAllByRole(getByRole('tabpanel'), 'menuitem')).toHaveLength(numInGroup2))
|
||||
expect(getByRole('tab', { name: 'People and body', selected: true })).toBeVisible()
|
||||
})
|
||||
|
||||
|
@ -102,7 +105,7 @@ describe('Picker tests', () => {
|
|||
getByRole('tab', { name: 'Smileys and emoticons', selected: true }).focus()
|
||||
|
||||
const expectGroupLength = async group => {
|
||||
await waitFor(() => expect(getAllByRole('menuitem'))
|
||||
await waitFor(() => expect(testingLibrary.getAllByRole(getByRole('tabpanel'), 'menuitem'))
|
||||
.toHaveLength(truncatedEmoji.filter(_ => _.group === group).length))
|
||||
}
|
||||
|
||||
|
@ -129,7 +132,7 @@ describe('Picker tests', () => {
|
|||
test('measures zwj emoji', async () => {
|
||||
getByRole('tab', { name: 'Flags' }).click()
|
||||
await tick(20)
|
||||
await waitFor(() => expect(getAllByRole('menuitem'))
|
||||
await waitFor(() => expect(testingLibrary.getAllByRole(getByRole('tabpanel'), 'menuitem'))
|
||||
.toHaveLength(truncatedEmoji.filter(_ => _.group === 9).length))
|
||||
})
|
||||
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
import { waitFor, getByTestId, getAllByRole, getByRole, fireEvent } from '@testing-library/dom'
|
||||
import { basicAfterEach, basicBeforeEach, tick, truncatedEmoji } from '../shared'
|
||||
import Picker from '../../../src/picker/PickerElement'
|
||||
import allData from 'emojibase-data/en/data.json'
|
||||
import { MOST_COMMONLY_USED_EMOJI } from '../../../src/picker/constants'
|
||||
import { uniqBy } from '../../../src/shared/uniqBy'
|
||||
|
||||
describe('Favorites UI', () => {
|
||||
beforeEach(basicBeforeEach)
|
||||
afterEach(basicAfterEach)
|
||||
|
||||
test('Favorites UI basic test', async () => {
|
||||
const dataSource = 'with-favs.json'
|
||||
|
||||
const dataWithFavorites = uniqBy([
|
||||
...truncatedEmoji,
|
||||
...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' } }))
|
||||
|
||||
const picker = new Picker({ dataSource, locale: 'en' })
|
||||
document.body.appendChild(picker)
|
||||
const container = picker.shadowRoot.querySelector('.picker')
|
||||
|
||||
// using a testId because testing-library seems to think role=menu has no aria-label
|
||||
const favoritesBar = getByTestId(container, 'favorites')
|
||||
expect(favoritesBar).toBeVisible()
|
||||
await waitFor(() => expect(getAllByRole(favoritesBar, 'menuitem')).toHaveLength(8))
|
||||
expect(getAllByRole(favoritesBar, 'menuitem').map(_ => _.getAttribute('data-emoji'))).toStrictEqual(
|
||||
MOST_COMMONLY_USED_EMOJI.slice(0, 8)
|
||||
)
|
||||
fireEvent.click(getByRole(container, 'menuitem', { name: /🤣/ }))
|
||||
await waitFor(() => expect(getAllByRole(favoritesBar, 'menuitem')
|
||||
.map(_ => _.getAttribute('data-emoji'))).toStrictEqual([
|
||||
'🤣',
|
||||
...MOST_COMMONLY_USED_EMOJI.slice(0, 7)
|
||||
]
|
||||
))
|
||||
|
||||
await tick(20)
|
||||
await picker.database.delete()
|
||||
document.body.removeChild(picker)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,51 @@
|
|||
import allEmojis from 'emojibase-data/en/data.json'
|
||||
import { applySkinTone } from '../../../src/picker/utils/applySkinTone'
|
||||
|
||||
describe('skin tones tests', () => {
|
||||
test.skip('can compute unicode based on tones', () => {
|
||||
const debugIt = (str) => {
|
||||
const res = []
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
res.push(str.charCodeAt(i).toString(16))
|
||||
}
|
||||
return res
|
||||
}
|
||||
const wrong = []
|
||||
const right = []
|
||||
for (const emoji of allEmojis) {
|
||||
if (emoji.skins) {
|
||||
for (const skin of emoji.skins) {
|
||||
if (typeof skin.tone === 'number') {
|
||||
const actualUnicode = applySkinTone(emoji.emoji, skin.tone)
|
||||
if (actualUnicode !== skin.emoji) {
|
||||
wrong.push({ emoji, actualUnicode, skin })
|
||||
} else {
|
||||
right.push({ emoji, actualUnicode, skin })
|
||||
}
|
||||
// expect(actualUnicode).toBe(skin.emoji)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log('wrong', wrong.length, 'right', right.length)
|
||||
for (const w of wrong) {
|
||||
console.log('\n' + w.emoji.emoji + '\n' + debugIt(w.emoji.emoji).join(',') + '\n' +
|
||||
debugIt(w.skin.emoji).join(',') + '\n' +
|
||||
debugIt(w.actualUnicode).join(',') + '\n\n')
|
||||
}
|
||||
console.log('Right')
|
||||
for (const r of right) {
|
||||
console.log('\n' + r.emoji.emoji + '\n' + debugIt(r.emoji.emoji).join(',') + '\n' +
|
||||
debugIt(r.skin.emoji).join(',') + '\n' +
|
||||
debugIt(r.actualUnicode).join(',') + '\n\n')
|
||||
}
|
||||
})
|
||||
|
||||
test('can compute some correct unicode tones', () => {
|
||||
expect(applySkinTone('👍', 0)).toBe('👍')
|
||||
expect(applySkinTone('👍', 3)).toBe('👍🏽')
|
||||
expect(applySkinTone('🧘♀️', 3)).toBe('🧘🏽♀️')
|
||||
expect(applySkinTone('🤌', 2)).toBe('🤌🏼')
|
||||
expect(applySkinTone('🖐️', 5)).toBe('🖐🏿')
|
||||
})
|
||||
})
|
|
@ -1,53 +0,0 @@
|
|||
import allEmojis from 'emojibase-data/en/data.json'
|
||||
import { applySkinTone } from '../../../src/picker/utils/applySkinTone'
|
||||
|
||||
describe('skintones tests', () => {
|
||||
describe('data tests', () => {
|
||||
test.skip('can compute unicode based on tones', () => {
|
||||
const debugIt = (str) => {
|
||||
const res = []
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
res.push(str.charCodeAt(i).toString(16))
|
||||
}
|
||||
return res
|
||||
}
|
||||
const wrong = []
|
||||
const right = []
|
||||
for (const emoji of allEmojis) {
|
||||
if (emoji.skins) {
|
||||
for (const skin of emoji.skins) {
|
||||
if (typeof skin.tone === 'number') {
|
||||
const actualUnicode = applySkinTone(emoji.emoji, skin.tone)
|
||||
if (actualUnicode !== skin.emoji) {
|
||||
wrong.push({ emoji, actualUnicode, skin })
|
||||
} else {
|
||||
right.push({ emoji, actualUnicode, skin })
|
||||
}
|
||||
// expect(actualUnicode).toBe(skin.emoji)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log('wrong', wrong.length, 'right', right.length)
|
||||
for (const w of wrong) {
|
||||
console.log('\n' + w.emoji.emoji + '\n' + debugIt(w.emoji.emoji).join(',') + '\n' +
|
||||
debugIt(w.skin.emoji).join(',') + '\n' +
|
||||
debugIt(w.actualUnicode).join(',') + '\n\n')
|
||||
}
|
||||
console.log('Right')
|
||||
for (const r of right) {
|
||||
console.log('\n' + r.emoji.emoji + '\n' + debugIt(r.emoji.emoji).join(',') + '\n' +
|
||||
debugIt(r.skin.emoji).join(',') + '\n' +
|
||||
debugIt(r.actualUnicode).join(',') + '\n\n')
|
||||
}
|
||||
})
|
||||
|
||||
test('can compute some correct unicode tones', () => {
|
||||
expect(applySkinTone('👍', 0)).toBe('👍')
|
||||
expect(applySkinTone('👍', 3)).toBe('👍🏽')
|
||||
expect(applySkinTone('🧘♀️', 3)).toBe('🧘🏽♀️')
|
||||
expect(applySkinTone('🤌', 2)).toBe('🤌🏼')
|
||||
expect(applySkinTone('🖐️', 5)).toBe('🖐🏿')
|
||||
})
|
||||
})
|
||||
})
|
Loading…
Reference in New Issue