Files
upstream-rocketchat/apps/meteor/app/emoji-native/lib/generateEmojiData.spec.ts
upstream-mirror 30810eecd7
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Code scanning - action / CodeQL-Build (push) Failing after 2s
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
2026-09-16 01:21:02 +05:00

197 lines
7.3 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { Emoji } from 'emojibase';
import data from 'emojibase-data/en/data.json';
import emojibaseShortcodes from 'emojibase-data/en/shortcodes/emojibase.json';
import joypixelsShortcodes from 'emojibase-data/en/shortcodes/joypixels.json';
import type { EmojiEntry } from './generateEmojiData';
import { getEmojiData } from './generateEmojiData';
import { legacyEmojioneMap } from './legacyEmojioneMap';
import { shortnameToUnicode } from './shortnameToUnicode';
const asArray = (entry: string | string[] | undefined): string[] => {
if (!entry) return [];
return Array.isArray(entry) ? entry : [entry];
};
/**
* The emoji we expose shortcodes for, derived independently from `generateEmojiData` so this suite
* fails if the two ever drift apart.
*/
const emittedEmojis = (() => {
const emojis = new Map<string, string>();
for (const emojiData of data as Emoji[]) {
if (emojiData.group === 2) continue;
const { hexcode } = emojiData;
const codepoint = hexcode.includes('-') ? NaN : parseInt(hexcode, 16);
const isRegional = codepoint >= 0x1f1e6 && codepoint <= 0x1f1ff;
if (emojiData.group === undefined && !isRegional) continue;
emojis.set(hexcode, emojiData.emoji);
for (const skin of emojiData.skins ?? []) {
if (!skin.tone) continue;
emojis.set(skin.hexcode, skin.emoji);
}
}
return emojis;
})();
describe('native emoji shortcodes', () => {
const { emojiList, emojisByCategory } = getEmojiData();
const unicodeOf = (shortcode: string) => emojiList[`:${shortcode}:`]?.unicode;
// Shortcodes both presets define, pointing at different emoji. Reaction keys and message
// shortcodes stored before the native emoji migration — and everything the mobile clients
// send today — use the joypixels/emojione meaning, so these must not follow emojibase.
describe.each([
['up', '🆙', 'arrow_up_small', '🔼'],
['cat', '🐱', 'cat2', '🐈️'],
['dog', '🐶', 'dog2', '🐕️'],
['o', '⭕️', 'o2', '🅾️'],
['point_up', '☝️', 'point_up_2', '👆️'],
['sunglasses', '😎', 'dark_sunglasses', '🕶️'],
['pencil', '📝', 'pencil2', '✏️'],
['train', '🚋', 'train2', '🚆'],
['whale', '🐳', 'whale2', '🐋'],
['umbrella', '☔️', 'umbrella2', '☂️'],
['computer', '💻️', 'desktop_computer', '🖥️'],
['satellite', '📡', 'satellite_orbital', '🛰️'],
['tiger', '🐯', 'tiger2', '🐅'],
['no', '🇳🇴', 'thumbsdown', '👎️'],
['om', '🇴🇲', 'om_symbol', '🕉️'],
['sunny', '☀️', 'white_sun_small_cloud', '🌤️'],
['japan', '🗾', 'flag_jp', '🇯🇵'],
])('contested shortcode :%s:', (contested, expected, displacedShortcode, displaced) => {
it(`resolves to ${expected}, not ${displaced}`, () => {
expect(unicodeOf(contested)).toBe(expected);
});
it(`leaves ${displaced} reachable as :${displacedShortcode}:`, () => {
expect(unicodeOf(displacedShortcode)).toBe(displaced);
});
});
it('falls back to emojibase for emoji joypixels never named', () => {
// joypixels stopped shipping, so everything added to Unicode since then only has an
// emojibase name — without the fallback these emoji disappear from the picker entirely
expect(unicodeOf('fingerprint')).toBe('🫆');
expect(unicodeOf('face_with_eye_bags')).toBe('🫩');
expect(unicodeOf('leafless_tree')).toBe('🪾');
expect(unicodeOf('root_vegetable')).toBe('🫜');
});
it('only backfills legacy shortcodes the presets do not provide', () => {
// the map is hand-generated; if a preset already names a shortcode, the entry is dead weight
// and, worse, hides which name is authoritative
const redundant = Object.keys(legacyEmojioneMap).filter((shortcode) => emojiList[`:${shortcode}:`]);
expect(redundant).toEqual([]);
});
it('resolves the legacy shortcodes that survived', () => {
expect(shortnameToUnicode(':bride_with_veil:')).toBe('👰');
expect(shortnameToUnicode(':bride_with_veil_tone3:')).toBe('👰🏽');
expect(shortnameToUnicode(':digit_zero:')).toBe('0️⃣');
expect(shortnameToUnicode(':tone3:')).toBe('🏽');
});
it('keeps joypixels aliases of contested emoji working', () => {
expect(unicodeOf('point_up_2')).toBe('👆️');
expect(unicodeOf('cat_face')).toBe('🐱');
expect(unicodeOf('tram_car')).toBe('🚋');
});
it('applies the same precedence to skin tone variants', () => {
expect(unicodeOf('point_up_tone1')).toBe('☝🏻');
expect(unicodeOf('point_up_2_tone1')).toBe('👆🏻');
});
it('resolves contested shortcodes to unicode for shortnameToUnicode consumers', () => {
expect(shortnameToUnicode(':up:')).toBe('🆙');
expect(shortnameToUnicode(':cat:')).toBe('🐱');
expect(shortnameToUnicode(':o:')).toBe('⭕️');
});
it('gives joypixels precedence for every shortcode the two presets contest', () => {
const contested: string[] = [];
for (const [hexcode, emoji] of emittedEmojis) {
for (const shortcode of asArray((joypixelsShortcodes as Record<string, string | string[]>)[hexcode])) {
const entry = emojiList[`:${shortcode}:`];
if (entry && entry.unicode !== emoji) {
contested.push(`:${shortcode}: is ${entry.unicode}, joypixels says ${emoji}`);
}
}
}
expect(contested).toEqual([]);
});
it('never strands an emoji without a shortcode', () => {
const stranded: string[] = [];
const reachable = new Set(Object.values(emojiList).map((entry) => entry.unicode));
for (const [hexcode, emoji] of emittedEmojis) {
const hadShortcode =
asArray((emojibaseShortcodes as Record<string, string | string[]>)[hexcode]).length > 0 ||
asArray((joypixelsShortcodes as Record<string, string | string[]>)[hexcode]).length > 0;
if (hadShortcode && !reachable.has(emoji)) {
stranded.push(`${emoji} (${hexcode})`);
}
}
expect(stranded).toEqual([]);
});
it('keeps every picker entry resolvable', () => {
const unresolvable = Object.values(emojisByCategory)
.flat()
.filter((shortcode) => !emojiList[`:${shortcode}:`]);
expect(unresolvable).toEqual([]);
});
it('keeps every picker entry pointing at its own emoji', () => {
// a picker entry whose name resolves to a *different* emoji would insert the wrong emoji
const misdirected = Object.values(emojisByCategory)
.flat()
.filter((shortcode) => emojiList[`:${shortcode}:`]?.name !== shortcode);
expect(misdirected).toEqual([]);
});
it('registers the primary shortcode before any of its aliases', () => {
// picker search and unicodeToShortcodeMap both take the first registered name for an entry,
// so an alias registered ahead of the primary surfaces the wrong name to users
const firstSeen = new Map<EmojiEntry, string>();
for (const [shortcode, entry] of Object.entries(emojiList)) {
if (!firstSeen.has(entry)) firstSeen.set(entry, shortcode);
}
const outOfOrder = Object.values(emojisByCategory)
.flat()
.filter((shortcode) => firstSeen.get(emojiList[`:${shortcode}:`]) !== `:${shortcode}:`);
expect(outOfOrder).toEqual([]);
});
it('never lists a shortname that resolves to another emoji', () => {
// emoji picker search reads `shortnames`, so a stale alias surfaces the wrong result
const stale: string[] = [];
for (const entry of Object.values(emojiList)) {
for (const shortname of entry.shortnames) {
if (emojiList[shortname] !== entry) {
stale.push(`${entry.unicode} lists ${shortname}, which resolves to ${emojiList[shortname]?.unicode}`);
}
}
}
expect(stale).toEqual([]);
});
});