import { beforeEach, describe, expect, it, vi } from 'vitest'; import { TextareaEmoji } from './textarea-emoji'; import { TEXTAREA_EMOJI_INPUT_EVENT, TEXTAREA_EMOJI_EMOJI_EVENT, type TextareaEmojiInputEvent, type TextareaEmojiEmojiEvent, } from './textarea-emoji.types'; const MARKUP = `
`; function setup(opts = {}): { root: HTMLElement; ctrl: TextareaEmoji } { document.body.innerHTML = MARKUP; const root = document.body.firstElementChild as HTMLElement; return { root, ctrl: new TextareaEmoji(root, opts) }; } function textarea(root: HTMLElement): HTMLTextAreaElement { return root.querySelector('[data-c42-textarea-emoji-input]')!; } function counter(root: HTMLElement): HTMLElement { return root.querySelector('[data-c42-textarea-emoji-counter]')!; } describe('TextareaEmoji', () => { beforeEach(() => { document.body.innerHTML = ''; }); it('throws without textarea', () => { document.body.innerHTML = '
'; const root = document.body.firstElementChild as HTMLElement; expect(() => new TextareaEmoji(root)).toThrow(/Missing/); }); it('sets maxlength attribute and initial counter', () => { const { root } = setup({ maxLength: 50 }); expect(textarea(root).getAttribute('maxlength')).toBe('50'); expect(counter(root).textContent).toBe('0/50'); }); it('updates counter on input', () => { const { root } = setup({ maxLength: 100 }); const ta = textarea(root); ta.value = 'hello'; ta.dispatchEvent(new Event('input', { bubbles: true })); expect(counter(root).textContent).toBe('5/100'); }); it('opens and closes picker via trigger', () => { const { root } = setup(); const trigger = root.querySelector('[data-c42-textarea-emoji-trigger]')!; const picker = root.querySelector('[data-c42-textarea-emoji-picker]')!; expect(picker.hasAttribute('hidden')).toBe(true); expect(root.getAttribute('data-picker-open')).toBe('false'); trigger.click(); expect(picker.hasAttribute('hidden')).toBe(false); expect(root.getAttribute('data-picker-open')).toBe('true'); trigger.click(); expect(picker.hasAttribute('hidden')).toBe(true); expect(root.getAttribute('data-picker-open')).toBe('false'); }); it('closes picker on Escape', () => { const { ctrl } = setup(); ctrl.open(); document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })); expect(ctrl.isOpen()).toBe(false); }); it('closes picker on outside click', () => { const { ctrl } = setup(); ctrl.open(); document.body.dispatchEvent(new MouseEvent('click', { bubbles: true })); expect(ctrl.isOpen()).toBe(false); }); it('inserts emoji at cursor position', () => { const { root, ctrl } = setup(); const ta = textarea(root); ta.value = 'hello world'; ta.setSelectionRange(5, 5); ctrl.insertEmoji('πŸŽ‰'); expect(ta.value).toBe('helloπŸŽ‰ world'); expect(ta.selectionStart).toBe(7); // 5 + emoji length (2) }); it('respects maxLength when inserting emoji', () => { const { root, ctrl } = setup({ maxLength: 5 }); const ta = textarea(root); ta.value = 'abcde'; ta.setSelectionRange(5, 5); ctrl.insertEmoji('πŸŽ‰'); expect(ta.value).toBe('abcde'); // unchanged }); it('emits input event on typing', () => { const { root } = setup(); const spy = vi.fn(); root.addEventListener(TEXTAREA_EMOJI_INPUT_EVENT, spy); const ta = textarea(root); ta.value = 'test'; ta.dispatchEvent(new Event('input', { bubbles: true })); expect(spy).toHaveBeenCalledOnce(); expect((spy.mock.calls[0][0] as TextareaEmojiInputEvent).detail.value).toBe('test'); }); it('emits emoji event on insertEmoji', () => { const { root, ctrl } = setup(); const spy = vi.fn(); root.addEventListener(TEXTAREA_EMOJI_EMOJI_EVENT, spy); ctrl.insertEmoji('πŸ˜€'); expect(spy).toHaveBeenCalledOnce(); expect((spy.mock.calls[0][0] as TextareaEmojiEmojiEvent).detail.emoji).toBe('πŸ˜€'); }); it('value getter/setter work correctly', () => { const { root, ctrl } = setup({ maxLength: 10 }); ctrl.value = 'hello world is long'; expect(ctrl.value).toBe('hello worl'); // truncated to 10 expect(counter(root).textContent).toBe('10/10'); }); it('destroy removes listeners', () => { const { root, ctrl } = setup(); ctrl.open(); ctrl.destroy(); document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })); // Still open because listener was removed expect(root.getAttribute('data-picker-open')).toBe('true'); }); });