import { beforeEach, describe, expect, it, vi } from 'vitest';
import { SmsEditor } from './sms-editor';
import { SMS_EDITOR_CHANGE_EVENT, type SmsEditorChangeEvent } from './sms-editor.types';
const MARKUP = `
`;
function setup(opts = {}): { root: HTMLElement; ctrl: SmsEditor } {
document.body.innerHTML = MARKUP;
const root = document.body.firstElementChild as HTMLElement;
return { root, ctrl: new SmsEditor(root, opts) };
}
function input(root: HTMLElement): HTMLTextAreaElement {
return root.querySelector('[data-c42-sms-input]')!;
}
function text(root: HTMLElement, sel: string): string | null {
return root.querySelector(sel)!.textContent;
}
describe('SmsEditor', () => {
beforeEach(() => {
document.body.innerHTML = '';
});
it('throws without a textarea', () => {
document.body.innerHTML = '';
const root = document.body.firstElementChild as HTMLElement;
expect(() => new SmsEditor(root)).toThrow(/data-c42-sms-input/);
});
it('sets ARIA role', () => {
const { root } = setup();
expect(input(root).getAttribute('role')).toBe('textbox');
});
it('does not set maxlength unless maxLength is given', () => {
const { root } = setup();
expect(input(root).hasAttribute('maxlength')).toBe(false);
});
it('enforces maxlength when given', () => {
const { root } = setup({ maxLength: 160 });
expect(input(root).getAttribute('maxlength')).toBe('160');
});
it('reports GSM-7 counts on input', () => {
const { root } = setup();
const ta = input(root);
ta.value = 'Hello';
ta.dispatchEvent(new Event('input', { bubbles: true }));
expect(text(root, '[data-c42-sms-counter]')).toBe('5');
expect(text(root, '[data-c42-sms-segments]')).toBe('1');
expect(text(root, '[data-c42-sms-encoding]')).toBe('GSM-7');
expect(root.getAttribute('data-encoding')).toBe('GSM-7');
});
it('switches to UCS-2 with emoji', () => {
const { root } = setup();
const ta = input(root);
ta.value = 'Hi 🎉';
ta.dispatchEvent(new Event('input', { bubbles: true }));
expect(text(root, '[data-c42-sms-encoding]')).toBe('UCS-2');
expect(root.getAttribute('data-encoding')).toBe('UCS-2');
});
it('reports two segments past 160 GSM-7 chars', () => {
const { root } = setup();
const ta = input(root);
ta.value = 'a'.repeat(161);
ta.dispatchEvent(new Event('input', { bubbles: true }));
expect(text(root, '[data-c42-sms-segments]')).toBe('2');
});
it('flags over-limit when maxSegments is exceeded', () => {
const { root } = setup({ maxSegments: 1 });
const ta = input(root);
ta.value = 'a'.repeat(161);
ta.dispatchEvent(new Event('input', { bubbles: true }));
expect(root.hasAttribute('data-over-limit')).toBe(true);
});
it('inserts emoji at cursor', () => {
const { root, ctrl } = setup();
const ta = input(root);
ta.value = 'ab';
ta.setSelectionRange(1, 1);
ctrl.insertText('🎉');
expect(ta.value).toBe('a🎉b');
});
it('opens and closes the picker via trigger', () => {
const { root } = setup();
const trigger = root.querySelector('[data-c42-sms-trigger]')!;
trigger.click();
expect(root.getAttribute('data-picker-open')).toBe('true');
trigger.click();
expect(root.getAttribute('data-picker-open')).toBe('false');
});
it('closes the picker on Escape', () => {
const { ctrl } = setup();
ctrl.openPicker();
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
expect(ctrl.isPickerOpen()).toBe(false);
});
it('calls renderPicker plugin once', () => {
const renderPicker = vi.fn();
setup({ renderPicker });
expect(renderPicker).toHaveBeenCalledOnce();
});
it('hides emoji affordances when emojis:false', () => {
const { root } = setup({ emojis: false });
expect(root.querySelector('[data-c42-sms-trigger]')!.hasAttribute('hidden')).toBe(true);
});
it('emits change with segmentation detail', () => {
const { root } = setup();
const spy = vi.fn();
root.addEventListener(SMS_EDITOR_CHANGE_EVENT, spy);
const ta = input(root);
ta.value = 'hey';
ta.dispatchEvent(new Event('input', { bubbles: true }));
const detail = (spy.mock.calls.at(-1)![0] as SmsEditorChangeEvent).detail;
expect(detail.text).toBe('hey');
expect(detail.encoding).toBe('GSM-7');
expect(detail.segments).toBe(1);
expect(detail.length).toBe(3);
});
it('getSegmentInfo reflects current value', () => {
const { ctrl } = setup({ value: 'hello' });
expect(ctrl.getSegmentInfo().length).toBe(5);
});
it('fills the optional preview with escaped text and toggles data-empty', () => {
const { root } = setup();
const preview = root.querySelector('[data-c42-sms-preview]')!;
expect(preview.hasAttribute('data-empty')).toBe(true);
const ta = input(root);
ta.value = 'Hi & you';
ta.dispatchEvent(new Event('input', { bubbles: true }));
expect(preview.textContent).toBe('Hi & you');
expect(preview.innerHTML).toBe('Hi <b> & you'); // textContent escapes
expect(preview.hasAttribute('data-empty')).toBe(false);
});
it('destroy removes listeners', () => {
const { root, ctrl } = setup();
ctrl.openPicker();
ctrl.destroy();
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
expect(root.getAttribute('data-picker-open')).toBe('true');
});
});