Emoji Rendering Efficiency (#35568)
This commit is contained in:
@@ -1,94 +1,184 @@
|
||||
import { customEmojiFactory, unicodeEmojiFactory } from '@/testing/factories';
|
||||
|
||||
import {
|
||||
EMOJI_MODE_NATIVE,
|
||||
EMOJI_MODE_NATIVE_WITH_FLAGS,
|
||||
EMOJI_MODE_TWEMOJI,
|
||||
} from './constants';
|
||||
import { emojifyElement, tokenizeText } from './render';
|
||||
import type { CustomEmojiData, UnicodeEmojiData } from './types';
|
||||
import * as db from './database';
|
||||
import {
|
||||
emojifyElement,
|
||||
emojifyText,
|
||||
testCacheClear,
|
||||
tokenizeText,
|
||||
} from './render';
|
||||
import type { EmojiAppState, ExtraCustomEmojiMap } from './types';
|
||||
|
||||
vitest.mock('./database', () => ({
|
||||
searchCustomEmojisByShortcodes: vitest.fn(
|
||||
() =>
|
||||
[
|
||||
{
|
||||
shortcode: 'custom',
|
||||
static_url: 'emoji/static',
|
||||
url: 'emoji/custom',
|
||||
category: 'test',
|
||||
visible_in_picker: true,
|
||||
},
|
||||
] satisfies CustomEmojiData[],
|
||||
),
|
||||
searchEmojisByHexcodes: vitest.fn(
|
||||
() =>
|
||||
[
|
||||
{
|
||||
function mockDatabase() {
|
||||
return {
|
||||
searchCustomEmojisByShortcodes: vi
|
||||
.spyOn(db, 'searchCustomEmojisByShortcodes')
|
||||
.mockResolvedValue([customEmojiFactory()]),
|
||||
searchEmojisByHexcodes: vi
|
||||
.spyOn(db, 'searchEmojisByHexcodes')
|
||||
.mockResolvedValue([
|
||||
unicodeEmojiFactory({
|
||||
hexcode: '1F60A',
|
||||
group: 0,
|
||||
label: 'smiling face with smiling eyes',
|
||||
order: 0,
|
||||
tags: ['smile', 'happy'],
|
||||
unicode: '😊',
|
||||
},
|
||||
{
|
||||
}),
|
||||
unicodeEmojiFactory({
|
||||
hexcode: '1F1EA-1F1FA',
|
||||
group: 0,
|
||||
label: 'flag-eu',
|
||||
order: 0,
|
||||
tags: ['flag', 'european union'],
|
||||
unicode: '🇪🇺',
|
||||
},
|
||||
] satisfies UnicodeEmojiData[],
|
||||
),
|
||||
findMissingLocales: vitest.fn(() => []),
|
||||
}));
|
||||
}),
|
||||
]),
|
||||
};
|
||||
}
|
||||
|
||||
const expectedSmileImage =
|
||||
'<img draggable="false" class="emojione" alt="😊" title="smiling face with smiling eyes" src="/emoji/1f60a.svg">';
|
||||
const expectedFlagImage =
|
||||
'<img draggable="false" class="emojione" alt="🇪🇺" title="flag-eu" src="/emoji/1f1ea-1f1fa.svg">';
|
||||
const expectedCustomEmojiImage =
|
||||
'<img draggable="false" class="emojione custom-emoji" alt=":custom:" title=":custom:" src="emoji/custom/static" data-original="emoji/custom" data-static="emoji/custom/static">';
|
||||
const expectedRemoteCustomEmojiImage =
|
||||
'<img draggable="false" class="emojione custom-emoji" alt=":remote:" title=":remote:" src="remote.social/static" data-original="remote.social/custom" data-static="remote.social/static">';
|
||||
|
||||
const mockExtraCustom: ExtraCustomEmojiMap = {
|
||||
remote: {
|
||||
shortcode: 'remote',
|
||||
static_url: 'remote.social/static',
|
||||
url: 'remote.social/custom',
|
||||
},
|
||||
};
|
||||
|
||||
function testAppState(state: Partial<EmojiAppState> = {}) {
|
||||
return {
|
||||
locales: ['en'],
|
||||
mode: EMOJI_MODE_TWEMOJI,
|
||||
currentLocale: 'en',
|
||||
darkTheme: false,
|
||||
...state,
|
||||
} satisfies EmojiAppState;
|
||||
}
|
||||
|
||||
describe('emojifyElement', () => {
|
||||
const testElement = document.createElement('div');
|
||||
testElement.innerHTML = '<p>Hello 😊🇪🇺!</p><p>:custom:</p>';
|
||||
|
||||
const expectedSmileImage =
|
||||
'<img draggable="false" class="emojione" alt="😊" title="smiling face with smiling eyes" src="/emoji/1f60a.svg">';
|
||||
const expectedFlagImage =
|
||||
'<img draggable="false" class="emojione" alt="🇪🇺" title="flag-eu" src="/emoji/1f1ea-1f1fa.svg">';
|
||||
const expectedCustomEmojiImage =
|
||||
'<img draggable="false" class="emojione custom-emoji" alt=":custom:" title=":custom:" src="emoji/static" data-original="emoji/custom" data-static="emoji/static">';
|
||||
|
||||
function cloneTestElement() {
|
||||
return testElement.cloneNode(true) as HTMLElement;
|
||||
function testElement(text = '<p>Hello 😊🇪🇺!</p><p>:custom:</p>') {
|
||||
const testElement = document.createElement('div');
|
||||
testElement.innerHTML = text;
|
||||
return testElement;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
testCacheClear();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
test('caches element rendering results', async () => {
|
||||
const { searchCustomEmojisByShortcodes, searchEmojisByHexcodes } =
|
||||
mockDatabase();
|
||||
await emojifyElement(testElement(), testAppState());
|
||||
await emojifyElement(testElement(), testAppState());
|
||||
await emojifyElement(testElement(), testAppState());
|
||||
expect(searchEmojisByHexcodes).toHaveBeenCalledExactlyOnceWith(
|
||||
['1F1EA-1F1FA', '1F60A'],
|
||||
'en',
|
||||
);
|
||||
expect(searchCustomEmojisByShortcodes).toHaveBeenCalledExactlyOnceWith([
|
||||
'custom',
|
||||
]);
|
||||
});
|
||||
|
||||
test('emojifies custom emoji in native mode', async () => {
|
||||
const emojifiedElement = await emojifyElement(cloneTestElement(), {
|
||||
locales: ['en'],
|
||||
mode: EMOJI_MODE_NATIVE,
|
||||
currentLocale: 'en',
|
||||
});
|
||||
expect(emojifiedElement.innerHTML).toBe(
|
||||
const { searchEmojisByHexcodes } = mockDatabase();
|
||||
const actual = await emojifyElement(
|
||||
testElement(),
|
||||
testAppState({ mode: EMOJI_MODE_NATIVE }),
|
||||
);
|
||||
assert(actual);
|
||||
expect(actual.innerHTML).toBe(
|
||||
`<p>Hello 😊🇪🇺!</p><p>${expectedCustomEmojiImage}</p>`,
|
||||
);
|
||||
expect(searchEmojisByHexcodes).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('emojifies flag emoji in native-with-flags mode', async () => {
|
||||
const emojifiedElement = await emojifyElement(cloneTestElement(), {
|
||||
locales: ['en'],
|
||||
mode: EMOJI_MODE_NATIVE_WITH_FLAGS,
|
||||
currentLocale: 'en',
|
||||
});
|
||||
expect(emojifiedElement.innerHTML).toBe(
|
||||
const { searchEmojisByHexcodes } = mockDatabase();
|
||||
const actual = await emojifyElement(
|
||||
testElement(),
|
||||
testAppState({ mode: EMOJI_MODE_NATIVE_WITH_FLAGS }),
|
||||
);
|
||||
assert(actual);
|
||||
expect(actual.innerHTML).toBe(
|
||||
`<p>Hello 😊${expectedFlagImage}!</p><p>${expectedCustomEmojiImage}</p>`,
|
||||
);
|
||||
expect(searchEmojisByHexcodes).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
test('emojifies everything in twemoji mode', async () => {
|
||||
const emojifiedElement = await emojifyElement(cloneTestElement(), {
|
||||
locales: ['en'],
|
||||
mode: EMOJI_MODE_TWEMOJI,
|
||||
currentLocale: 'en',
|
||||
});
|
||||
expect(emojifiedElement.innerHTML).toBe(
|
||||
const { searchCustomEmojisByShortcodes, searchEmojisByHexcodes } =
|
||||
mockDatabase();
|
||||
const actual = await emojifyElement(testElement(), testAppState());
|
||||
assert(actual);
|
||||
expect(actual.innerHTML).toBe(
|
||||
`<p>Hello ${expectedSmileImage}${expectedFlagImage}!</p><p>${expectedCustomEmojiImage}</p>`,
|
||||
);
|
||||
expect(searchEmojisByHexcodes).toHaveBeenCalledOnce();
|
||||
expect(searchCustomEmojisByShortcodes).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
test('emojifies with provided custom emoji', async () => {
|
||||
const { searchCustomEmojisByShortcodes, searchEmojisByHexcodes } =
|
||||
mockDatabase();
|
||||
const actual = await emojifyElement(
|
||||
testElement('<p>hi :remote:</p>'),
|
||||
testAppState(),
|
||||
mockExtraCustom,
|
||||
);
|
||||
assert(actual);
|
||||
expect(actual.innerHTML).toBe(
|
||||
`<p>hi ${expectedRemoteCustomEmojiImage}</p>`,
|
||||
);
|
||||
expect(searchEmojisByHexcodes).not.toHaveBeenCalled();
|
||||
expect(searchCustomEmojisByShortcodes).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('returns null when no emoji are found', async () => {
|
||||
mockDatabase();
|
||||
const actual = await emojifyElement(
|
||||
testElement('<p>here is just text :)</p>'),
|
||||
testAppState(),
|
||||
);
|
||||
expect(actual).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('emojifyText', () => {
|
||||
test('returns original input when no emoji are in string', async () => {
|
||||
const actual = await emojifyText('nothing here', testAppState());
|
||||
expect(actual).toBe('nothing here');
|
||||
});
|
||||
|
||||
test('renders Unicode emojis to twemojis', async () => {
|
||||
mockDatabase();
|
||||
const actual = await emojifyText('Hello 😊🇪🇺!', testAppState());
|
||||
expect(actual).toBe(`Hello ${expectedSmileImage}${expectedFlagImage}!`);
|
||||
});
|
||||
|
||||
test('renders custom emojis', async () => {
|
||||
mockDatabase();
|
||||
const actual = await emojifyText('Hello :custom:!', testAppState());
|
||||
expect(actual).toBe(`Hello ${expectedCustomEmojiImage}!`);
|
||||
});
|
||||
|
||||
test('renders provided extra emojis', async () => {
|
||||
const actual = await emojifyText(
|
||||
'remote emoji :remote:',
|
||||
testAppState(),
|
||||
mockExtraCustom,
|
||||
);
|
||||
expect(actual).toBe(`remote emoji ${expectedRemoteCustomEmojiImage}`);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user