/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see .
*/
import {clearTestEmojiProvider, setupTestEmojiProvider} from '@fluxer/markdown_parser/src/__tests__/TestEmojiSetup';
import {Parser} from '@fluxer/markdown_parser/src/parser/Parser';
import {EmojiKind, NodeType, ParserFlags} from '@fluxer/markdown_parser/src/types/Enums';
import type {EmojiNode, TextNode} from '@fluxer/markdown_parser/src/types/Nodes';
import {afterAll, beforeAll, describe, expect, test} from 'vitest';
beforeAll(() => {
setupTestEmojiProvider();
});
afterAll(() => {
clearTestEmojiProvider();
});
describe('Fluxer Markdown Parser', () => {
test('standard emoji', () => {
const input = 'Hello 🦶 World!';
const flags = 0;
const parser = new Parser(input, flags);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([
{type: NodeType.Text, content: 'Hello '},
{
type: NodeType.Emoji,
kind: {
kind: EmojiKind.Standard,
raw: '🦶',
codepoints: '1f9b6',
name: expect.any(String),
},
},
{type: NodeType.Text, content: ' World!'},
]);
});
test('custom emoji static', () => {
const input = 'Check this <:mmLol:216154654256398347> emoji!';
const flags = 0;
const parser = new Parser(input, flags);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([
{type: NodeType.Text, content: 'Check this '},
{
type: NodeType.Emoji,
kind: {
kind: EmojiKind.Custom,
name: 'mmLol',
id: '216154654256398347',
animated: false,
},
},
{type: NodeType.Text, content: ' emoji!'},
]);
});
test('custom emoji animated', () => {
const input = 'Animated: ';
const flags = 0;
const parser = new Parser(input, flags);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([
{type: NodeType.Text, content: 'Animated: '},
{
type: NodeType.Emoji,
kind: {
kind: EmojiKind.Custom,
name: 'b1nzy',
id: '392938283556143104',
animated: true,
},
},
]);
});
test('generate codepoints with vs16 and zwj', () => {
const input = '👨👩👧👦❤️😊';
const flags = 0;
const parser = new Parser(input, flags);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([
{
type: NodeType.Emoji,
kind: {
kind: EmojiKind.Standard,
raw: '👨👩👧👦',
codepoints: '1f468-200d-1f469-200d-1f467-200d-1f466',
name: 'family_mwgb',
},
},
{
type: NodeType.Emoji,
kind: {
kind: EmojiKind.Standard,
raw: '❤️',
codepoints: '2764',
name: 'heart',
},
},
{
type: NodeType.Emoji,
kind: {
kind: EmojiKind.Standard,
raw: '😊',
codepoints: '1f60a',
name: 'blush',
},
},
]);
});
test('multiple consecutive emojis', () => {
const input = '😀😃😄😁';
const flags = 0;
const parser = new Parser(input, flags);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([
{
type: NodeType.Emoji,
kind: {
kind: EmojiKind.Standard,
raw: '😀',
codepoints: '1f600',
name: expect.any(String),
},
},
{
type: NodeType.Emoji,
kind: {
kind: EmojiKind.Standard,
raw: '😃',
codepoints: '1f603',
name: expect.any(String),
},
},
{
type: NodeType.Emoji,
kind: {
kind: EmojiKind.Standard,
raw: '😄',
codepoints: '1f604',
name: expect.any(String),
},
},
{
type: NodeType.Emoji,
kind: {
kind: EmojiKind.Standard,
raw: '😁',
codepoints: '1f601',
name: expect.any(String),
},
},
]);
});
test('special plaintext symbols should be rendered as text', () => {
const input = '™ ™️ © ©️ ® ®️';
const flags = 0;
const parser = new Parser(input, flags);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([{type: NodeType.Text, content: '™ ™ © © ® ®'}]);
});
test('copyright shortcode converts to text symbol', () => {
const input = ':copyright: normal text';
const flags = 0;
const parser = new Parser(input, flags);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([{type: NodeType.Text, content: '© normal text'}]);
});
test('trademark shortcode converts to text symbol', () => {
const input = ':tm: normal text';
const flags = 0;
const parser = new Parser(input, flags);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([{type: NodeType.Text, content: '™ normal text'}]);
});
test('registered shortcode converts to text symbol', () => {
const input = ':registered: normal text';
const flags = 0;
const parser = new Parser(input, flags);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([{type: NodeType.Text, content: '® normal text'}]);
});
test('mixed shortcodes with regular emojis', () => {
const input = ':copyright: and :smile: with :registered:';
const flags = 0;
const parser = new Parser(input, flags);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([
{type: NodeType.Text, content: '© and '},
{
type: NodeType.Emoji,
kind: {
kind: EmojiKind.Standard,
raw: '😄',
codepoints: '1f604',
name: 'smile',
},
},
{type: NodeType.Text, content: ' with ®'},
]);
});
test('shortcodes in formatted text', () => {
const input = '**Bold :tm:** *Italic :copyright:* __:registered:__';
const flags = 0;
const parser = new Parser(input, flags);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([
{
type: NodeType.Strong,
children: [{type: NodeType.Text, content: 'Bold ™'}],
},
{type: NodeType.Text, content: ' '},
{
type: NodeType.Emphasis,
children: [{type: NodeType.Text, content: 'Italic ©'}],
},
{type: NodeType.Text, content: ' '},
{
type: NodeType.Underline,
children: [{type: NodeType.Text, content: '®'}],
},
]);
});
test('emoji data loaded', () => {
const input = ':smile: :wave: :heart:';
const flags = 0;
const parser = new Parser(input, flags);
const {nodes: ast} = parser.parse();
expect(ast.length).toBeGreaterThan(0);
expect(ast.some((node) => node.type === NodeType.Emoji)).toBe(true);
});
test('emoji cache initialization', () => {
const input = ':smile: :face_holding_back_tears: :face-holding-back-tears:';
const flags = 0;
const parser = new Parser(input, flags);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([
{
type: NodeType.Emoji,
kind: {
kind: EmojiKind.Standard,
raw: '😄',
codepoints: '1f604',
name: 'smile',
},
},
{
type: NodeType.Text,
content: ' ',
},
{
type: NodeType.Emoji,
kind: {
kind: EmojiKind.Standard,
raw: '🥹',
codepoints: '1f979',
name: 'face_holding_back_tears',
},
},
{
type: NodeType.Text,
content: ' :face-holding-back-tears:',
},
]);
});
test('case sensitive emoji lookup', () => {
const validVariants = [':smile:', ':face_holding_back_tears:'];
for (const emoji of validVariants) {
const parser = new Parser(emoji, 0);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([
{
type: NodeType.Emoji,
kind: {
kind: EmojiKind.Standard,
raw: expect.any(String),
codepoints: expect.any(String),
name: expect.any(String),
},
},
]);
}
});
test('invalid case emoji lookup', () => {
const invalidVariants = [
':SMILE:',
':Smile:',
':FACE_HOLDING_BACK_TEARS:',
':Face_Holding_Back_Tears:',
':face-holding-back-tears:',
];
for (const emoji of invalidVariants) {
const parser = new Parser(emoji, 0);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([{type: NodeType.Text, content: emoji}]);
}
});
test('separator variants', () => {
const input = ':face_holding_back_tears: :face-holding-back-tears:';
const parser = new Parser(input, 0);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([
{
type: NodeType.Emoji,
kind: {
kind: EmojiKind.Standard,
raw: '🥹',
codepoints: '1f979',
name: 'face_holding_back_tears',
},
},
{
type: NodeType.Text,
content: ' :face-holding-back-tears:',
},
]);
});
test('basic emoji shortcode', () => {
const input = 'Hello :face_holding_back_tears: world!';
const flags = 0;
const parser = new Parser(input, flags);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([
{type: NodeType.Text, content: 'Hello '},
{
type: NodeType.Emoji,
kind: {
kind: EmojiKind.Standard,
raw: expect.any(String),
codepoints: expect.any(String),
name: 'face_holding_back_tears',
},
},
{type: NodeType.Text, content: ' world!'},
]);
});
test('emoji shortcode in code', () => {
const input = "`print(':face_holding_back_tears:')`";
const flags = 0;
const parser = new Parser(input, flags);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([{type: NodeType.InlineCode, content: "print(':face_holding_back_tears:')"}]);
});
test('emoji shortcode in code block', () => {
const input = '```\n:face_holding_back_tears:\n```';
const flags = ParserFlags.ALLOW_CODE_BLOCKS;
const parser = new Parser(input, flags);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([
{
type: NodeType.CodeBlock,
language: undefined,
content: ':face_holding_back_tears:\n',
},
]);
});
test('distinguishes between plaintext and emoji versions with variation selectors', () => {
const inputs = [
{text: '↩', shouldBeEmoji: false},
{text: '↩️', shouldBeEmoji: true},
{text: '↪', shouldBeEmoji: false},
{text: '↪️', shouldBeEmoji: true},
{text: '⤴', shouldBeEmoji: false},
{text: '⤴️', shouldBeEmoji: true},
];
for (const {text, shouldBeEmoji} of inputs) {
const parser = new Parser(text, 0);
const {nodes: ast} = parser.parse();
if (shouldBeEmoji) {
expect(ast[0].type).toBe(NodeType.Emoji);
const emojiNode = ast[0] as EmojiNode;
expect(emojiNode.kind.kind).toBe(EmojiKind.Standard);
expect(emojiNode.kind.name).not.toBe('');
} else {
expect(ast[0].type).toBe(NodeType.Text);
const textNode = ast[0] as TextNode;
expect(textNode.content).toBe(text);
}
}
});
test('renders mixed text with both plaintext and emoji versions', () => {
const input = '↩ is plaintext, ↩️ is emoji';
const parser = new Parser(input, 0);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([
{type: NodeType.Text, content: '↩ is plaintext, '},
{
type: NodeType.Emoji,
kind: {
kind: EmojiKind.Standard,
raw: '↩️',
codepoints: '21a9',
name: 'leftwards_arrow_with_hook',
},
},
{type: NodeType.Text, content: ' is emoji'},
]);
});
test('correctly parses dingbat emojis', () => {
const inputs = [
{emoji: '✅', name: 'white_check_mark', codepoint: '2705'},
{emoji: '❌', name: 'x', codepoint: '274c'},
];
for (const {emoji, name, codepoint} of inputs) {
const parser = new Parser(emoji, 0);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([
{
type: NodeType.Emoji,
kind: {
kind: EmojiKind.Standard,
raw: emoji,
codepoints: codepoint,
name,
},
},
]);
}
});
test('dingbat emojis in text context', () => {
const input = 'Task complete ✅ but error ❌';
const parser = new Parser(input, 0);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([
{type: NodeType.Text, content: 'Task complete '},
{
type: NodeType.Emoji,
kind: {
kind: EmojiKind.Standard,
raw: '✅',
codepoints: '2705',
name: 'white_check_mark',
},
},
{type: NodeType.Text, content: ' but error '},
{
type: NodeType.Emoji,
kind: {
kind: EmojiKind.Standard,
raw: '❌',
codepoints: '274c',
name: 'x',
},
},
]);
});
test('malformed custom emoji edge cases', () => {
const malformedCases = [
'<:ab>',
'<:abc>',
'<:name:>',
'<:name:abc>',
'<:name:123abc>',
'<:name:12ab34>',
'<::123>',
];
for (const input of malformedCases) {
const parser = new Parser(input, 0);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([
{
type: NodeType.Text,
content: input,
},
]);
}
});
test('empty custom emoji cases', () => {
const emptyCases = ['<::>', '<:name:>', '<::123>'];
for (const input of emptyCases) {
const parser = new Parser(input, 0);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([
{
type: NodeType.Text,
content: input,
},
]);
}
});
test('custom emoji with invalid ID characters', () => {
const invalidIdCases = ['<:test:123a>', '<:name:12b34>', '<:emoji:abc123>', '<:custom:123-456>', '<:sample:12_34>'];
for (const input of invalidIdCases) {
const parser = new Parser(input, 0);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([
{
type: NodeType.Text,
content: input,
},
]);
}
});
test('custom emoji with edge case names and IDs', () => {
const edgeCases = ['<::123>', '<: :123>'];
for (const input of edgeCases) {
const parser = new Parser(input, 0);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([
{
type: NodeType.Text,
content: input,
},
]);
}
});
});
describe('Special Symbols Plaintext Rendering', () => {
test('trademark symbol should render as text without variation selector', () => {
const inputs = ['™', '™️'];
for (const input of inputs) {
const parser = new Parser(input, 0);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([{type: NodeType.Text, content: '™'}]);
}
});
test('copyright symbol should render as text without variation selector', () => {
const inputs = ['©', '©️'];
for (const input of inputs) {
const parser = new Parser(input, 0);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([{type: NodeType.Text, content: '©'}]);
}
});
test('registered symbol should render as text without variation selector', () => {
const inputs = ['®', '®️'];
for (const input of inputs) {
const parser = new Parser(input, 0);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([{type: NodeType.Text, content: '®'}]);
}
});
test('mixed emoji and special symbols', () => {
const input = '™️ ©️ ®️ 👍 ❤️';
const parser = new Parser(input, 0);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([
{type: NodeType.Text, content: '™ © ® '},
{
type: NodeType.Emoji,
kind: {
kind: 'Standard',
raw: '👍',
codepoints: '1f44d',
name: 'thumbsup',
},
},
{type: NodeType.Text, content: ' '},
{
type: NodeType.Emoji,
kind: {
kind: 'Standard',
raw: '❤️',
codepoints: '2764',
name: 'heart',
},
},
]);
});
test('special symbols in formatted text', () => {
const input = '**™️** *©️* __®️__';
const parser = new Parser(input, 0);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([
{
type: NodeType.Strong,
children: [{type: NodeType.Text, content: '™'}],
},
{type: NodeType.Text, content: ' '},
{
type: NodeType.Emphasis,
children: [{type: NodeType.Text, content: '©'}],
},
{type: NodeType.Text, content: ' '},
{
type: NodeType.Underline,
children: [{type: NodeType.Text, content: '®'}],
},
]);
});
test('special symbols interspersed with text', () => {
const input = 'This product™️ is copyright©️ and registered®️';
const parser = new Parser(input, 0);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([{type: NodeType.Text, content: 'This product™ is copyright© and registered®'}]);
});
});
describe('Custom Emoji Edge Cases', () => {
test('custom emoji that does not start with <: or {
const input = '';
const parser = new Parser(input, 0);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([{type: NodeType.Text, content: ''}]);
});
test('custom emoji with closing bracket too early returns text', () => {
const input = '<:a>';
const parser = new Parser(input, 0);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([{type: NodeType.Text, content: '<:a>'}]);
});
test('custom emoji with only colon returns text', () => {
const input = '<:>';
const parser = new Parser(input, 0);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([{type: NodeType.Text, content: '<:>'}]);
});
test('custom emoji with empty name returns text', () => {
const input = '<::12345>';
const parser = new Parser(input, 0);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([{type: NodeType.Text, content: '<::12345>'}]);
});
test('custom emoji with empty ID returns text', () => {
const input = '<:name:>';
const parser = new Parser(input, 0);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([{type: NodeType.Text, content: '<:name:>'}]);
});
test('custom emoji with non-digit character in ID returns text', () => {
const input = '<:name:123abc>';
const parser = new Parser(input, 0);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([{type: NodeType.Text, content: '<:name:123abc>'}]);
});
test('custom emoji with special characters in ID returns text', () => {
const input = '<:name:123!456>';
const parser = new Parser(input, 0);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([{type: NodeType.Text, content: '<:name:123!456>'}]);
});
test('animated emoji without name returns text', () => {
const input = '';
const parser = new Parser(input, 0);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([{type: NodeType.Text, content: ''}]);
});
test('animated emoji with valid format', () => {
const input = '';
const parser = new Parser(input, 0);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([
{
type: NodeType.Emoji,
kind: {
kind: EmojiKind.Custom,
name: 'animated_emoji',
id: '123456789',
animated: true,
},
},
]);
});
test('custom emoji with underscore and hyphen in name', () => {
const input = '<:test_emoji-name:123456789>';
const parser = new Parser(input, 0);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([
{
type: NodeType.Emoji,
kind: {
kind: EmojiKind.Custom,
name: 'test_emoji-name',
id: '123456789',
animated: false,
},
},
]);
});
});
describe('Emoji Shortcode Edge Cases', () => {
test('shortcode with invalid characters in name returns text', () => {
const input = ':emoji with spaces:';
const parser = new Parser(input, 0);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([{type: NodeType.Text, content: ':emoji with spaces:'}]);
});
test('shortcode starting with colon only', () => {
const input = ':';
const parser = new Parser(input, 0);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([{type: NodeType.Text, content: ':'}]);
});
test('shortcode with two colons only', () => {
const input = '::';
const parser = new Parser(input, 0);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([{type: NodeType.Text, content: '::'}]);
});
test('shortcode that does not exist returns text', () => {
const input = ':nonexistent_emoji_name_12345:';
const parser = new Parser(input, 0);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([{type: NodeType.Text, content: ':nonexistent_emoji_name_12345:'}]);
});
test('shortcode with skin tone for emoji without skin tone support', () => {
const input = ':heart::skin-tone-3:';
const parser = new Parser(input, 0);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([
{
type: NodeType.Emoji,
kind: {
kind: EmojiKind.Standard,
raw: '❤️',
codepoints: '2764',
name: 'heart',
},
},
{type: NodeType.Text, content: ':skin-tone-3:'},
]);
});
test('shortcode with invalid skin tone number returns base emoji', () => {
const input = ':wave::skin-tone-9:';
const parser = new Parser(input, 0);
const {nodes: ast} = parser.parse();
expect(ast[0].type).toBe(NodeType.Emoji);
expect(ast[1].type).toBe(NodeType.Text);
});
test('shortcode with skin tone at invalid position', () => {
const input = ':wave::skin-tone-:';
const parser = new Parser(input, 0);
const {nodes: ast} = parser.parse();
expect(ast[0].type).toBe(NodeType.Emoji);
});
test('shortcode with skin tone 0 is invalid', () => {
const input = ':wave::skin-tone-0:';
const parser = new Parser(input, 0);
const {nodes: ast} = parser.parse();
expect(ast[0].type).toBe(NodeType.Emoji);
});
test('shortcode with skin tone 6 is invalid', () => {
const input = ':wave::skin-tone-6:';
const parser = new Parser(input, 0);
const {nodes: ast} = parser.parse();
expect(ast[0].type).toBe(NodeType.Emoji);
});
test('shortcode with empty base name returns text', () => {
const input = ':::skin-tone-1:';
const parser = new Parser(input, 0);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([{type: NodeType.Text, content: ':::skin-tone-1:'}]);
});
test('shortcode with valid skin tone in middle of string', () => {
const input = ':wave::skin-tone-3: is a wave';
const parser = new Parser(input, 0);
const {nodes: ast} = parser.parse();
expect(ast[0].type).toBe(NodeType.Emoji);
});
});
describe('Emoji Skin Tone Parsing', () => {
test('mx_claus with skin tone 5 parses as single emoji', () => {
const input = ':mx_claus::skin-tone-5:';
const parser = new Parser(input, 0);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([
{
type: NodeType.Emoji,
kind: {
kind: EmojiKind.Standard,
raw: '🧑🏿🎄',
codepoints: expect.any(String),
name: 'mx_claus',
},
},
]);
});
test('wave with skin tone 1 parses as single emoji', () => {
const input = ':wave::skin-tone-1:';
const parser = new Parser(input, 0);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([
{
type: NodeType.Emoji,
kind: {
kind: EmojiKind.Standard,
raw: '👋🏻',
codepoints: expect.any(String),
name: 'wave',
},
},
]);
});
test('thumbsup with all skin tones', () => {
const skinTonedEmojis = ['👍🏻', '👍🏼', '👍🏽', '👍🏾', '👍🏿'];
for (let tone = 1; tone <= 5; tone++) {
const input = `:thumbsup::skin-tone-${tone}:`;
const parser = new Parser(input, 0);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([
{
type: NodeType.Emoji,
kind: {
kind: EmojiKind.Standard,
raw: skinTonedEmojis[tone - 1],
codepoints: expect.any(String),
name: 'thumbsup',
},
},
]);
}
});
test('skin tone emoji in sentence', () => {
const input = 'Hello :wave::skin-tone-3: world';
const parser = new Parser(input, 0);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([
{type: NodeType.Text, content: 'Hello '},
{
type: NodeType.Emoji,
kind: {
kind: EmojiKind.Standard,
raw: '👋🏽',
codepoints: expect.any(String),
name: 'wave',
},
},
{type: NodeType.Text, content: ' world'},
]);
});
test('multiple skin tone emojis in sequence', () => {
const input = ':wave::skin-tone-1::thumbsup::skin-tone-5:';
const parser = new Parser(input, 0);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([
{
type: NodeType.Emoji,
kind: {
kind: EmojiKind.Standard,
raw: '👋🏻',
codepoints: expect.any(String),
name: 'wave',
},
},
{
type: NodeType.Emoji,
kind: {
kind: EmojiKind.Standard,
raw: '👍🏿',
codepoints: expect.any(String),
name: 'thumbsup',
},
},
]);
});
test('skin tone with formatted text', () => {
const input = '**:wave::skin-tone-2:**';
const parser = new Parser(input, 0);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([
{
type: NodeType.Strong,
children: [
{
type: NodeType.Emoji,
kind: {
kind: EmojiKind.Standard,
raw: '👋🏼',
codepoints: expect.any(String),
name: 'wave',
},
},
],
},
]);
});
test('foot emoji with skin tone', () => {
const input = ':foot::skin-tone-4:';
const parser = new Parser(input, 0);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([
{
type: NodeType.Emoji,
kind: {
kind: EmojiKind.Standard,
raw: '🦶🏾',
codepoints: expect.any(String),
name: 'foot',
},
},
]);
});
test('base emoji without skin tone still works', () => {
const input = ':wave:';
const parser = new Parser(input, 0);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([
{
type: NodeType.Emoji,
kind: {
kind: EmojiKind.Standard,
raw: '👋',
codepoints: expect.any(String),
name: 'wave',
},
},
]);
});
test('skin tone on non-diversity emoji leaves skin tone as text', () => {
const input = ':smile::skin-tone-3:';
const parser = new Parser(input, 0);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([
{
type: NodeType.Emoji,
kind: {
kind: EmojiKind.Standard,
raw: '😄',
codepoints: expect.any(String),
name: 'smile',
},
},
{type: NodeType.Text, content: ':skin-tone-3:'},
]);
});
});
describe('Standard Emoji Edge Cases', () => {
test('low ASCII characters are not emojis', () => {
const input = 'abc123!@#$%^&*()';
const parser = new Parser(input, 0);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([{type: NodeType.Text, content: input}]);
});
test('empty string returns empty array', () => {
const input = '';
const parser = new Parser(input, 0);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([]);
});
test('emoji regex not matching returns null', () => {
const input = 'Regular text without emoji';
const parser = new Parser(input, 0);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([{type: NodeType.Text, content: input}]);
});
});