/** * Tests for KTClipboard component */ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { KTClipboard } from '../clipboard'; function flushPromises() { return new Promise((resolve) => { setTimeout(() => resolve(), 0); }); } function createFixture(options?: { withPredefinedText?: boolean; predefinedTextValue?: string; predefinedTextAttributeEmpty?: boolean; action?: 'copy' | 'cut'; withTarget?: boolean; targetExists?: boolean; targetType?: 'input' | 'textarea' | 'div'; targetValue?: string; targetTextContent?: string; }) { document.body.innerHTML = ''; const targetType = options?.targetType ?? 'input'; const targetId = 'kt-clipboard-target'; const targetExists = options?.targetExists ?? true; const container = document.createElement('div'); const trigger = document.createElement('button'); trigger.type = 'button'; trigger.setAttribute('data-kt-clipboard', 'true'); if (options?.action) { trigger.setAttribute('data-kt-clipboard-action', options.action); } if (options?.withTarget) { trigger.setAttribute('data-kt-clipboard-target', `#${targetId}`); } if (options?.withPredefinedText) { if (options.predefinedTextAttributeEmpty) { trigger.setAttribute('data-kt-clipboard-text', ''); } else { trigger.setAttribute( 'data-kt-clipboard-text', options.predefinedTextValue ?? '', ); } } trigger.textContent = 'Copy'; container.appendChild(trigger); if (targetExists && options?.withTarget) { if (targetType === 'input') { const input = document.createElement('input'); input.id = targetId; input.value = options?.targetValue ?? 'input value'; container.appendChild(input); } else if (targetType === 'textarea') { const textarea = document.createElement('textarea'); textarea.id = targetId; textarea.value = options?.targetValue ?? 'textarea value'; container.appendChild(textarea); } else { const div = document.createElement('div'); div.id = targetId; div.textContent = options?.targetTextContent ?? 'div text'; container.appendChild(div); } } document.body.appendChild(container); return { container, trigger }; } describe('KTClipboard', () => { let originalExecCommand: Document['execCommand']; beforeEach(() => { // Ensure a clean clipboard state between tests. originalExecCommand = document.execCommand; }); afterEach(() => { document.body.innerHTML = ''; // Restore execCommand (if it was replaced). document.execCommand = originalExecCommand; }); it('copies predefined text (text wins over target)', async () => { const writeTextMock = vi.fn().mockResolvedValue(undefined); Object.defineProperty(navigator, 'clipboard', { value: { writeText: writeTextMock }, configurable: true, }); const { trigger } = createFixture({ withPredefinedText: true, predefinedTextValue: 'hello', withTarget: true, targetExists: false, // target selector is missing; text must still win action: 'copy', }); const successEvents: CustomEvent[] = []; trigger.addEventListener('kt.clipboard.success', (e) => successEvents.push(e as CustomEvent), ); const errorEvents: CustomEvent[] = []; trigger.addEventListener('kt.clipboard.error', (e) => errorEvents.push(e as CustomEvent), ); new KTClipboard(trigger); trigger.click(); await flushPromises(); expect(writeTextMock).toHaveBeenCalledTimes(1); expect(writeTextMock).toHaveBeenCalledWith('hello'); expect(successEvents.length).toBe(1); expect(errorEvents.length).toBe(0); }); it('falls back to execCommand when Clipboard API is unavailable', async () => { Object.defineProperty(navigator, 'clipboard', { value: undefined, configurable: true, }); const execCommandMock = vi.fn().mockReturnValue(true); document.execCommand = execCommandMock as Document['execCommand']; const { trigger } = createFixture({ withPredefinedText: true, predefinedTextValue: 'fallback text', action: 'copy', }); const successEvents: CustomEvent[] = []; trigger.addEventListener('kt.clipboard.success', (e) => successEvents.push(e as CustomEvent), ); new KTClipboard(trigger); trigger.click(); await flushPromises(); expect(execCommandMock).toHaveBeenCalledWith('copy'); expect(successEvents.length).toBe(1); }); it('dispatches error if fallback execCommand fails', async () => { Object.defineProperty(navigator, 'clipboard', { value: undefined, configurable: true, }); const execCommandMock = vi.fn().mockReturnValue(false); document.execCommand = execCommandMock as Document['execCommand']; const { trigger } = createFixture({ withPredefinedText: true, predefinedTextValue: 'will fail', action: 'copy', }); const errorEvents: CustomEvent[] = []; trigger.addEventListener('kt.clipboard.error', (e) => errorEvents.push(e as CustomEvent), ); new KTClipboard(trigger); trigger.click(); await flushPromises(); expect(execCommandMock).toHaveBeenCalledWith('copy'); expect(errorEvents.length).toBe(1); }); it('dispatches error when data-kt-clipboard-text attribute is present but empty (and ignores target)', async () => { const writeTextMock = vi.fn().mockResolvedValue(undefined); Object.defineProperty(navigator, 'clipboard', { value: { writeText: writeTextMock }, configurable: true, }); const { trigger } = createFixture({ withPredefinedText: true, predefinedTextAttributeEmpty: true, withTarget: true, targetType: 'input', targetValue: 'should not be used', action: 'copy', }); const successEvents: CustomEvent[] = []; trigger.addEventListener('kt.clipboard.success', (e) => successEvents.push(e as CustomEvent), ); const errorEvents: CustomEvent[] = []; trigger.addEventListener('kt.clipboard.error', (e) => errorEvents.push(e as CustomEvent), ); new KTClipboard(trigger); trigger.click(); await flushPromises(); expect(writeTextMock).not.toHaveBeenCalled(); expect(successEvents.length).toBe(0); expect(errorEvents.length).toBe(1); }); it('dispatches error when Clipboard API writeText rejects', async () => { const writeTextMock = vi .fn() .mockRejectedValue(new Error('NotAllowedError: permission denied')); Object.defineProperty(navigator, 'clipboard', { value: { writeText: writeTextMock }, configurable: true, }); const { trigger } = createFixture({ withPredefinedText: true, predefinedTextValue: 'hello', action: 'copy', }); const errorEvents: CustomEvent[] = []; trigger.addEventListener('kt.clipboard.error', (e) => errorEvents.push(e as CustomEvent), ); new KTClipboard(trigger); trigger.click(); await flushPromises(); expect(writeTextMock).toHaveBeenCalledTimes(1); expect(errorEvents.length).toBe(1); }); it('dispatches error for cut with predefined text when target is missing', async () => { const writeTextMock = vi.fn().mockResolvedValue(undefined); Object.defineProperty(navigator, 'clipboard', { value: { writeText: writeTextMock }, configurable: true, }); const { trigger } = createFixture({ withPredefinedText: true, predefinedTextValue: 'hello', withTarget: false, action: 'cut', }); const errorEvents: CustomEvent[] = []; trigger.addEventListener('kt.clipboard.error', (e) => errorEvents.push(e as CustomEvent), ); new KTClipboard(trigger); trigger.click(); await flushPromises(); expect(writeTextMock).not.toHaveBeenCalled(); expect(errorEvents.length).toBe(1); }); it('copies from input target value', async () => { const writeTextMock = vi.fn().mockResolvedValue(undefined); Object.defineProperty(navigator, 'clipboard', { value: { writeText: writeTextMock }, configurable: true, }); const { trigger } = createFixture({ withPredefinedText: false, withTarget: true, targetType: 'input', targetValue: 'input value', action: 'copy', }); new KTClipboard(trigger); trigger.click(); await flushPromises(); expect(writeTextMock).toHaveBeenCalledTimes(1); expect(writeTextMock).toHaveBeenCalledWith('input value'); }); it('copies from non-input target textContent', async () => { const writeTextMock = vi.fn().mockResolvedValue(undefined); Object.defineProperty(navigator, 'clipboard', { value: { writeText: writeTextMock }, configurable: true, }); const { trigger } = createFixture({ withPredefinedText: false, withTarget: true, targetType: 'div', targetTextContent: 'div text content', action: 'copy', }); new KTClipboard(trigger); trigger.click(); await flushPromises(); expect(writeTextMock).toHaveBeenCalledTimes(1); expect(writeTextMock).toHaveBeenCalledWith('div text content'); }); it('dispatches error when selector misses and no predefined text exists', async () => { const writeTextMock = vi.fn().mockResolvedValue(undefined); Object.defineProperty(navigator, 'clipboard', { value: { writeText: writeTextMock }, configurable: true, }); const { trigger } = createFixture({ withPredefinedText: false, withTarget: true, targetExists: false, action: 'copy', }); const errorEvents: CustomEvent[] = []; trigger.addEventListener('kt.clipboard.error', (e) => errorEvents.push(e as CustomEvent), ); new KTClipboard(trigger); trigger.click(); await flushPromises(); expect(writeTextMock).not.toHaveBeenCalled(); expect(errorEvents.length).toBe(1); }); it('dispatches error for cut when target is not input/textarea', async () => { const writeTextMock = vi.fn().mockResolvedValue(undefined); Object.defineProperty(navigator, 'clipboard', { value: { writeText: writeTextMock }, configurable: true, }); const { trigger } = createFixture({ withPredefinedText: false, withTarget: true, targetType: 'div', targetTextContent: 'not cuttable', action: 'cut', }); const errorEvents: CustomEvent[] = []; trigger.addEventListener('kt.clipboard.error', (e) => errorEvents.push(e as CustomEvent), ); new KTClipboard(trigger); trigger.click(); await flushPromises(); expect(writeTextMock).not.toHaveBeenCalled(); expect(errorEvents.length).toBe(1); }); it('cuts from input target (clears value on success)', async () => { const writeTextMock = vi.fn().mockResolvedValue(undefined); Object.defineProperty(navigator, 'clipboard', { value: { writeText: writeTextMock }, configurable: true, }); const { trigger } = createFixture({ withPredefinedText: false, withTarget: true, targetType: 'input', targetValue: 'cut value', action: 'cut', }); const input = document.getElementById( 'kt-clipboard-target', ) as HTMLInputElement; expect(input.value).toBe('cut value'); new KTClipboard(trigger); trigger.click(); await flushPromises(); expect(writeTextMock).toHaveBeenCalledWith('cut value'); expect(input.value).toBe(''); }); it('is idempotent: createInstances does not double bind', async () => { const writeTextMock = vi.fn().mockResolvedValue(undefined); Object.defineProperty(navigator, 'clipboard', { value: { writeText: writeTextMock }, configurable: true, }); const { trigger } = createFixture({ withPredefinedText: true, predefinedTextValue: 'once', action: 'copy', }); // Explicit instance creation. new KTClipboard(trigger); // Should not create another handler on the same element. KTClipboard.createInstances(); trigger.click(); await flushPromises(); expect(writeTextMock).toHaveBeenCalledTimes(1); }); it('returns null for getInstance on non-initialized trigger', () => { document.body.innerHTML = ''; const trigger = document.createElement('button'); trigger.setAttribute('data-kt-clipboard', 'true'); document.body.appendChild(trigger); expect(KTClipboard.getInstance(trigger)).toBeNull(); }); });