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