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