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');
});
});