import { beforeEach, describe, expect, it, vi } from 'vitest'; import { Clipboard } from './clipboard'; import { CLIPBOARD_COPY_EVENT, CLIPBOARD_ERROR_EVENT, type ClipboardCopyEvent, type ClipboardErrorEvent, } from './clipboard.types'; const MARKUP = `
`; function setup(): HTMLElement { document.body.innerHTML = MARKUP; return document.body.firstElementChild as HTMLElement; } function trigger(root: HTMLElement): HTMLButtonElement { return root.querySelector('[data-c42-clipboard-trigger]')!; } let writeText: ReturnType; describe('Clipboard', () => { beforeEach(() => { writeText = vi.fn().mockResolvedValue(undefined); Object.assign(navigator, { clipboard: { writeText } }); document.body.innerHTML = ''; }); it('copies the source element value via the public copy() method', async () => { const root = setup(); const cb = new Clipboard(root); await cb.copy(); expect(writeText).toHaveBeenCalledWith('npm i @42/core'); expect(root.dataset.state).toBe('copied'); }); it('copies on trigger click', async () => { const root = setup(); new Clipboard(root); trigger(root).click(); await vi.waitFor(() => expect(writeText).toHaveBeenCalledWith('npm i @42/core')); }); it('reverts to idle after the timeout', async () => { vi.useFakeTimers(); try { const root = setup(); const cb = new Clipboard(root, { timeout: 1000 }); await cb.copy(); expect(root.dataset.state).toBe('copied'); vi.advanceTimersByTime(1000); expect(root.dataset.state).toBe('idle'); } finally { vi.useRealTimers(); } }); it('prefers the explicit text option over the source element', async () => { const root = setup(); const cb = new Clipboard(root, { text: 'override' }); await cb.copy(); expect(writeText).toHaveBeenCalledWith('override'); }); it('setText updates the copied value', async () => { const root = setup(); const cb = new Clipboard(root); cb.setText('changed'); await cb.copy(); expect(writeText).toHaveBeenCalledWith('changed'); }); it('reads textContent for non-input sources', async () => { document.body.innerHTML = `
echo hi
`; const root = document.body.firstElementChild as HTMLElement; const cb = new Clipboard(root); await cb.copy(); expect(writeText).toHaveBeenCalledWith('echo hi'); }); it('emits clipboard:copy on success', async () => { const root = setup(); const cb = new Clipboard(root); const spy = vi.fn(); cb.on(CLIPBOARD_COPY_EVENT, spy); await cb.copy(); expect(spy).toHaveBeenCalledOnce(); expect(spy.mock.calls[0][0].detail).toEqual({ text: 'npm i @42/core' }); }); it('emits clipboard:error and returns false on failure', async () => { writeText.mockRejectedValueOnce(new Error('denied')); const root = setup(); const cb = new Clipboard(root); const spy = vi.fn(); cb.on(CLIPBOARD_ERROR_EVENT, spy); const ok = await cb.copy(); expect(ok).toBe(false); expect(spy).toHaveBeenCalledOnce(); expect(root.dataset.state).toBe('idle'); }); it('sets type=button on a bare button trigger', () => { const root = setup(); new Clipboard(root); expect(trigger(root).type).toBe('button'); }); it('throws when the trigger is missing', () => { document.body.innerHTML = '
'; const root = document.body.firstElementChild as HTMLElement; expect(() => new Clipboard(root)).toThrow(/data-c42-clipboard-trigger/); }); it('removes listeners on destroy', async () => { const root = setup(); const cb = new Clipboard(root); cb.destroy(); trigger(root).click(); await Promise.resolve(); expect(writeText).not.toHaveBeenCalled(); }); });