From 0687c12b92e87fb7ab8ac94af0bba7c0ba6f9656 Mon Sep 17 00:00:00 2001 From: KarthiDreamr Date: Wed, 13 Aug 2025 21:01:18 +0530 Subject: [PATCH] feat: added tests --- tests/clipboard-functionality.test.js | 480 ++++++++++++++++++++++++ tests/integration-scenarios.test.js | 521 ++++++++++++++++++++++++++ tests/preference-persistence.test.js | 414 ++++++++++++++++++++ tests/setup.js | 237 ++++++++++++ 4 files changed, 1652 insertions(+) create mode 100644 tests/clipboard-functionality.test.js create mode 100644 tests/integration-scenarios.test.js create mode 100644 tests/preference-persistence.test.js create mode 100644 tests/setup.js diff --git a/tests/clipboard-functionality.test.js b/tests/clipboard-functionality.test.js new file mode 100644 index 0000000..ffdf9bd --- /dev/null +++ b/tests/clipboard-functionality.test.js @@ -0,0 +1,480 @@ +/** + * Comprehensive test suite for clipboard functionality + * Tests clipboard API availability detection, operations with different backgrounds, + * error handling, and preference persistence + */ + +// Mock ScreenshotSelector class for testing +class MockScreenshotSelector { + constructor() { + this.copyToClipboard = true; + this.background = 'black'; + } + + checkClipboardAPIAvailability() { + if (!global.navigator.clipboard) { + return { available: false, reason: 'Clipboard API not available in this browser' }; + } + if (!global.window.ClipboardItem) { + return { available: false, reason: 'ClipboardItem not supported in this browser' }; + } + if (!global.window.isSecureContext) { + return { available: false, reason: 'Clipboard API requires a secure context (HTTPS)' }; + } + if (!global.navigator.clipboard.write) { + return { available: false, reason: 'Clipboard write functionality not available' }; + } + return { available: true }; + } + + async copyCanvasToClipboard(canvas) { + try { + const availability = this.checkClipboardAPIAvailability(); + if (!availability.available) { + return { success: false, error: availability.reason, errorType: 'api_unavailable' }; + } + + const blob = await Promise.race([ + new Promise((resolve, reject) => { + canvas.toBlob((blob) => { + if (blob) resolve(blob); + else reject(new Error('Canvas to blob conversion returned null')); + }, 'image/png'); + }), + new Promise((_, reject) => + setTimeout(() => reject(new Error('Canvas to blob conversion timed out')), 10000) + ) + ]); + + const maxBlobSize = 50 * 1024 * 1024; + if (blob.size > maxBlobSize) { + return { + success: false, + error: `Image too large for clipboard (${Math.round(blob.size / 1024 / 1024)}MB). Try capturing a smaller area.`, + errorType: 'size_limit' + }; + } + + let clipboardItem; + try { + clipboardItem = new global.window.ClipboardItem({ 'image/png': blob }); + } catch (clipboardItemError) { + return { + success: false, + error: 'Failed to create clipboard item. Your browser may not support image clipboard operations.', + errorType: 'clipboard_item_creation' + }; + } + + await Promise.race([ + global.navigator.clipboard.write([clipboardItem]), + new Promise((_, reject) => + setTimeout(() => reject(new Error('Clipboard write timed out')), 5000) + ) + ]); + + return { success: true }; + } catch (error) { + if (error.name === 'NotAllowedError') { + return { success: false, error: 'Permission denied', errorType: 'permission_denied' }; + } else if (error.name === 'SecurityError') { + return { success: false, error: 'Security error', errorType: 'security_error' }; + } else if (error.message.includes('Canvas to blob conversion returned null')) { + return { success: false, error: error.message, errorType: 'canvas_conversion' }; + } else if (error.message.includes('ClipboardItem creation failed')) { + return { success: false, error: error.message, errorType: 'clipboard_item_creation' }; + } else if (error.message.includes('timed out')) { + return { success: false, error: error.message, errorType: 'timeout' }; + } + return { success: false, error: error.message, errorType: 'unexpected' }; + } + } + + getClipboardErrorMessage(clipboardResult) { + if (!clipboardResult || !clipboardResult.error) { + return 'Clipboard copy failed due to unknown error.'; + } + + const errorType = clipboardResult.errorType; + const errorMessage = clipboardResult.error; + + switch (errorType) { + case 'permission_denied': + return 'Clipboard copy failed: Permission denied. Please allow clipboard access in your browser settings.'; + case 'not_supported': + case 'api_unavailable': + return 'Clipboard copy not available in this browser. Screenshot was still downloaded.'; + case 'security_error': + return 'Clipboard copy blocked by browser security. Try using HTTPS or check site permissions.'; + case 'size_limit': + case 'quota_exceeded': + return 'Clipboard copy failed: Image too large. Try capturing a smaller area.'; + case 'timeout': + return 'Clipboard copy timed out. The image may be too large or complex.'; + default: + return `Clipboard copy failed: ${errorMessage}`; + } + } + + getClipboardSupportFeedback() { + const availability = this.checkClipboardAPIAvailability(); + + if (availability.available) { + return { + supported: true, + message: 'Clipboard copying is available and ready to use.' + }; + } + + let userMessage = ''; + if (availability.reason.includes('not available') || availability.reason.includes('not supported')) { + userMessage = 'Your browser doesn\'t support clipboard copying. Screenshots will still be downloaded.'; + } else if (availability.reason.includes('secure context')) { + userMessage = 'Clipboard copying requires HTTPS. Screenshots will still be downloaded.'; + } else if (availability.reason.includes('write functionality')) { + userMessage = 'Clipboard writing is not available. Screenshots will still be downloaded.'; + } else { + userMessage = 'Clipboard copying is not available. Screenshots will still be downloaded.'; + } + + return { + supported: false, + message: userMessage + }; + } +} + +describe('Clipboard API Availability Detection', () => { + let screenshotSelector; + + beforeEach(() => { + jest.clearAllMocks(); + screenshotSelector = new MockScreenshotSelector(); + }); + + test('should detect clipboard API availability when all APIs are present', () => { + global.navigator = { + clipboard: { + write: jest.fn() + } + }; + global.window.ClipboardItem = jest.fn(); + global.window.isSecureContext = true; + + const result = screenshotSelector.checkClipboardAPIAvailability(); + + expect(result.available).toBe(true); + expect(result.reason).toBeUndefined(); + }); + + test('should detect missing navigator.clipboard', () => { + global.navigator = {}; + + const result = screenshotSelector.checkClipboardAPIAvailability(); + + expect(result.available).toBe(false); + expect(result.reason).toBe('Clipboard API not available in this browser'); + }); + + test('should detect missing ClipboardItem', () => { + global.navigator = { + clipboard: { write: jest.fn() } + }; + global.window.ClipboardItem = undefined; + + const result = screenshotSelector.checkClipboardAPIAvailability(); + + expect(result.available).toBe(false); + expect(result.reason).toBe('ClipboardItem not supported in this browser'); + }); + + test('should detect insecure context', () => { + global.navigator = { + clipboard: { write: jest.fn() } + }; + global.window.ClipboardItem = jest.fn(); + global.window.isSecureContext = false; + + const result = screenshotSelector.checkClipboardAPIAvailability(); + + expect(result.available).toBe(false); + expect(result.reason).toBe('Clipboard API requires a secure context (HTTPS)'); + }); + + test('should detect missing clipboard.write', () => { + global.navigator = { + clipboard: {} + }; + global.window.ClipboardItem = jest.fn(); + global.window.isSecureContext = true; + + const result = screenshotSelector.checkClipboardAPIAvailability(); + + expect(result.available).toBe(false); + expect(result.reason).toBe('Clipboard write functionality not available'); + }); +}); + +describe('Clipboard Operations with Different Background Settings', () => { + let screenshotSelector; + let mockCanvas; + + beforeEach(() => { + jest.clearAllMocks(); + + global.navigator = { + clipboard: { + write: jest.fn().mockResolvedValue() + } + }; + global.window.ClipboardItem = jest.fn(); + global.window.isSecureContext = true; + + mockCanvas = { + toBlob: jest.fn((callback, format) => { + const mockBlob = new Blob(['mock image data'], { type: format }); + callback(mockBlob); + }), + toDataURL: jest.fn(() => 'data:image/png;base64,mockdata') + }; + + screenshotSelector = new MockScreenshotSelector(); + }); + + test('should copy to clipboard with transparent background', async () => { + screenshotSelector.background = 'transparent'; + screenshotSelector.copyToClipboard = true; + + const result = await screenshotSelector.copyCanvasToClipboard(mockCanvas); + + expect(result.success).toBe(true); + expect(mockCanvas.toBlob).toHaveBeenCalledWith(expect.any(Function), 'image/png'); + expect(global.navigator.clipboard.write).toHaveBeenCalled(); + }); + + test('should copy to clipboard with black background', async () => { + screenshotSelector.background = 'black'; + screenshotSelector.copyToClipboard = true; + + const result = await screenshotSelector.copyCanvasToClipboard(mockCanvas); + + expect(result.success).toBe(true); + expect(mockCanvas.toBlob).toHaveBeenCalledWith(expect.any(Function), 'image/png'); + expect(global.navigator.clipboard.write).toHaveBeenCalled(); + }); + + test('should copy to clipboard with white background', async () => { + screenshotSelector.background = 'white'; + screenshotSelector.copyToClipboard = true; + + const result = await screenshotSelector.copyCanvasToClipboard(mockCanvas); + + expect(result.success).toBe(true); + expect(mockCanvas.toBlob).toHaveBeenCalledWith(expect.any(Function), 'image/png'); + expect(global.navigator.clipboard.write).toHaveBeenCalled(); + }); + + test('should handle canvas to blob conversion failure', async () => { + mockCanvas.toBlob = jest.fn((callback) => { + callback(null); + }); + + const result = await screenshotSelector.copyCanvasToClipboard(mockCanvas); + + expect(result.success).toBe(false); + expect(result.error).toBe('Canvas to blob conversion returned null'); + expect(result.errorType).toBe('canvas_conversion'); + }); + + test('should handle large blob size limit', async () => { + const largeMockBlob = { + size: 60 * 1024 * 1024, + type: 'image/png' + }; + + mockCanvas.toBlob = jest.fn((callback) => { + callback(largeMockBlob); + }); + + const result = await screenshotSelector.copyCanvasToClipboard(mockCanvas); + + expect(result.success).toBe(false); + expect(result.error).toContain('Image too large for clipboard'); + expect(result.errorType).toBe('size_limit'); + }); +}); + +describe('Error Handling for Clipboard Operations', () => { + let screenshotSelector; + let mockCanvas; + + beforeEach(() => { + jest.clearAllMocks(); + + mockCanvas = { + toBlob: jest.fn((callback) => { + const mockBlob = new Blob(['mock data'], { type: 'image/png' }); + callback(mockBlob); + }) + }; + + screenshotSelector = new MockScreenshotSelector(); + }); + + test('should handle permission denied errors', async () => { + global.navigator = { + clipboard: { + write: jest.fn().mockRejectedValue(new DOMException('Permission denied', 'NotAllowedError')) + } + }; + global.window.ClipboardItem = jest.fn(); + global.window.isSecureContext = true; + + const result = await screenshotSelector.copyCanvasToClipboard(mockCanvas); + + expect(result.success).toBe(false); + expect(result.errorType).toBe('permission_denied'); + expect(result.error).toContain('Permission denied'); + }); + + test('should handle security errors', async () => { + global.navigator = { + clipboard: { + write: jest.fn().mockRejectedValue(new DOMException('Security error', 'SecurityError')) + } + }; + global.window.ClipboardItem = jest.fn(); + global.window.isSecureContext = true; + + const result = await screenshotSelector.copyCanvasToClipboard(mockCanvas); + + expect(result.success).toBe(false); + expect(result.errorType).toBe('security_error'); + }); + + test('should handle ClipboardItem creation failure', async () => { + global.navigator = { + clipboard: { write: jest.fn() } + }; + global.window.ClipboardItem = jest.fn(() => { + throw new Error('ClipboardItem creation failed'); + }); + global.window.isSecureContext = true; + + const result = await screenshotSelector.copyCanvasToClipboard(mockCanvas); + + expect(result.success).toBe(false); + expect(result.errorType).toBe('clipboard_item_creation'); + }); +}); + +describe('User-Friendly Error Messages', () => { + let screenshotSelector; + + beforeEach(() => { + screenshotSelector = new MockScreenshotSelector(); + }); + + test('should provide user-friendly message for permission denied', () => { + const clipboardResult = { + success: false, + error: 'Permission denied', + errorType: 'permission_denied' + }; + + const message = screenshotSelector.getClipboardErrorMessage(clipboardResult); + + expect(message).toBe('Clipboard copy failed: Permission denied. Please allow clipboard access in your browser settings.'); + }); + + test('should provide user-friendly message for API unavailable', () => { + const clipboardResult = { + success: false, + error: 'API not supported', + errorType: 'api_unavailable' + }; + + const message = screenshotSelector.getClipboardErrorMessage(clipboardResult); + + expect(message).toBe('Clipboard copy not available in this browser. Screenshot was still downloaded.'); + }); + + test('should provide user-friendly message for size limit', () => { + const clipboardResult = { + success: false, + error: 'Image too large', + errorType: 'size_limit' + }; + + const message = screenshotSelector.getClipboardErrorMessage(clipboardResult); + + expect(message).toBe('Clipboard copy failed: Image too large. Try capturing a smaller area.'); + }); + + test('should provide user-friendly message for timeout', () => { + const clipboardResult = { + success: false, + error: 'Operation timed out', + errorType: 'timeout' + }; + + const message = screenshotSelector.getClipboardErrorMessage(clipboardResult); + + expect(message).toBe('Clipboard copy timed out. The image may be too large or complex.'); + }); + + test('should handle unknown error types gracefully', () => { + const clipboardResult = { + success: false, + error: 'Unknown error occurred', + errorType: 'unknown' + }; + + const message = screenshotSelector.getClipboardErrorMessage(clipboardResult); + + expect(message).toBe('Clipboard copy failed: Unknown error occurred'); + }); +}); + +describe('Clipboard Support Feedback', () => { + let screenshotSelector; + + beforeEach(() => { + screenshotSelector = new MockScreenshotSelector(); + }); + + test('should provide positive feedback when clipboard is supported', () => { + global.navigator = { + clipboard: { write: jest.fn() } + }; + global.window.ClipboardItem = jest.fn(); + global.window.isSecureContext = true; + + const feedback = screenshotSelector.getClipboardSupportFeedback(); + + expect(feedback.supported).toBe(true); + expect(feedback.message).toBe('Clipboard copying is available and ready to use.'); + }); + + test('should provide helpful feedback when clipboard is not supported', () => { + global.navigator = {}; + + const feedback = screenshotSelector.getClipboardSupportFeedback(); + + expect(feedback.supported).toBe(false); + expect(feedback.message).toBe('Your browser doesn\'t support clipboard copying. Screenshots will still be downloaded.'); + }); + + test('should provide HTTPS requirement feedback', () => { + global.navigator = { + clipboard: { write: jest.fn() } + }; + global.window.ClipboardItem = jest.fn(); + global.window.isSecureContext = false; + + const feedback = screenshotSelector.getClipboardSupportFeedback(); + + expect(feedback.supported).toBe(false); + expect(feedback.message).toBe('Clipboard copying requires HTTPS. Screenshots will still be downloaded.'); + }); +}); \ No newline at end of file diff --git a/tests/integration-scenarios.test.js b/tests/integration-scenarios.test.js new file mode 100644 index 0000000..2221bf7 --- /dev/null +++ b/tests/integration-scenarios.test.js @@ -0,0 +1,521 @@ +/** + * Integration test suite for clipboard functionality across different scenarios + * Tests end-to-end workflows combining clipboard operations with various settings + */ + +// Mock Chrome APIs +global.chrome = { + storage: { + sync: { + get: jest.fn(), + set: jest.fn() + } + }, + runtime: { + sendMessage: jest.fn() + }, + tabs: { + query: jest.fn(), + sendMessage: jest.fn() + } +}; + +// Mock DOM APIs +global.document = { + createElement: jest.fn(() => ({ + style: {}, + classList: { add: jest.fn(), remove: jest.fn() }, + appendChild: jest.fn(), + click: jest.fn(), + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + innerHTML: '', + textContent: '', + id: '', + className: '' + })), + head: { appendChild: jest.fn() }, + body: { appendChild: jest.fn() }, + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + querySelectorAll: jest.fn(() => []), + getElementById: jest.fn() +}; + +global.window = { + getComputedStyle: jest.fn(() => ({})), + devicePixelRatio: 2, + isSecureContext: true, + CSS: { escape: jest.fn(str => str) }, + setTimeout: jest.fn((fn, delay) => { + if (delay === 0) fn(); + return 1; + }) +}; + +// Mock html2canvas +global.html2canvas = jest.fn(); + +describe('Integration Scenarios - Clipboard with Different Backgrounds', () => { + let screenshotSelector; + let mockElement; + let mockCanvas; + + beforeEach(() => { + jest.clearAllMocks(); + + // Mock element to capture + mockElement = { + classList: { + add: jest.fn(), + remove: jest.fn() + }, + style: {}, + getBoundingClientRect: () => ({ left: 0, top: 0, width: 100, height: 100 }), + scrollHeight: 100, + clientHeight: 100, + scrollWidth: 100, + clientWidth: 100, + children: [], + parentNode: { children: [] } + }; + + // Mock canvas + mockCanvas = { + toBlob: jest.fn((callback, format) => { + const mockBlob = new Blob(['mock image data'], { type: format }); + Object.defineProperty(mockBlob, 'size', { value: 1024 }); // 1KB + callback(mockBlob); + }), + toDataURL: jest.fn(() => 'data:image/png;base64,mockdata') + }; + + global.html2canvas = jest.fn().mockResolvedValue(mockCanvas); + + // Mock full clipboard API support + global.navigator = { + clipboard: { + write: jest.fn().mockResolvedValue() + } + }; + global.window.ClipboardItem = jest.fn(); + + // Import ScreenshotSelector (simplified for testing) + const ScreenshotSelector = class { + constructor() { + this.isActive = false; + this.background = 'black'; + this.copyToClipboard = true; + this.overlay = null; + } + + async init(background = 'black', copyToClipboard = true) { + this.background = background; + this.copyToClipboard = copyToClipboard; + this.isActive = true; + this.createUI(); + } + + createUI() { + this.overlay = global.document.createElement('div'); + global.document.body.appendChild(this.overlay); + } + + checkClipboardAPIAvailability() { + if (!global.navigator.clipboard) { + return { available: false, reason: 'Clipboard API not available in this browser' }; + } + if (!global.window.ClipboardItem) { + return { available: false, reason: 'ClipboardItem not supported in this browser' }; + } + if (!global.window.isSecureContext) { + return { available: false, reason: 'Clipboard API requires a secure context (HTTPS)' }; + } + if (!global.navigator.clipboard.write) { + return { available: false, reason: 'Clipboard write functionality not available' }; + } + return { available: true }; + } + + async copyCanvasToClipboard(canvas) { + try { + const availability = this.checkClipboardAPIAvailability(); + if (!availability.available) { + return { success: false, error: availability.reason, errorType: 'api_unavailable' }; + } + + const blob = await new Promise((resolve, reject) => { + canvas.toBlob((blob) => { + if (blob) resolve(blob); + else reject(new Error('Canvas to blob conversion returned null')); + }, 'image/png'); + }); + + const maxBlobSize = 50 * 1024 * 1024; + if (blob.size > maxBlobSize) { + return { + success: false, + error: `Image too large for clipboard (${Math.round(blob.size / 1024 / 1024)}MB)`, + errorType: 'size_limit' + }; + } + + const clipboardItem = new global.window.ClipboardItem({ 'image/png': blob }); + await global.navigator.clipboard.write([clipboardItem]); + + return { success: true }; + } catch (error) { + if (error.name === 'NotAllowedError') { + return { success: false, error: 'Permission denied', errorType: 'permission_denied' }; + } + return { success: false, error: error.message, errorType: 'unexpected' }; + } + } + + async captureElement(element) { + try { + // Generate canvas with background-specific settings + const canvas = await global.html2canvas(element, { + backgroundColor: this.background === 'transparent' ? null : + this.background === 'white' ? '#ffffff' : '#000000', + useCORS: true, + allowTaint: false + }); + + // Always download first + const link = global.document.createElement('a'); + link.href = canvas.toDataURL('image/png'); + link.download = `screenshot-${new Date().toISOString().slice(0, 19).replace(/[:.]/g, '-')}.png`; + link.click(); + + // Handle clipboard if enabled + let clipboardResult = null; + if (this.copyToClipboard) { + clipboardResult = await this.copyCanvasToClipboard(canvas); + } + + // Update UI based on results + let successMessage = 'Screenshot downloaded successfully!'; + if (this.copyToClipboard && clipboardResult && clipboardResult.success) { + successMessage = 'Screenshot downloaded and copied to clipboard!'; + } else if (this.copyToClipboard && clipboardResult && !clipboardResult.success) { + successMessage = `Screenshot downloaded successfully! Clipboard copy failed: ${clipboardResult.error}`; + } + + this.overlay.innerHTML = `
${successMessage}
`; + + return { + success: true, + downloadSuccess: true, + clipboardResult: clipboardResult, + message: successMessage + }; + + } catch (error) { + this.overlay.innerHTML = `
Screenshot failed: ${error.message}
`; + return { + success: false, + error: error.message + }; + } + } + }; + + screenshotSelector = new ScreenshotSelector(); + }); + + test('should capture with transparent background and copy to clipboard', async () => { + await screenshotSelector.init('transparent', true); + + const result = await screenshotSelector.captureElement(mockElement); + + expect(global.html2canvas).toHaveBeenCalledWith(mockElement, expect.objectContaining({ + backgroundColor: null, // Transparent background + useCORS: true, + allowTaint: false + })); + + expect(result.success).toBe(true); + expect(result.downloadSuccess).toBe(true); + expect(result.clipboardResult.success).toBe(true); + expect(result.message).toBe('Screenshot downloaded and copied to clipboard!'); + }); + + test('should capture with black background and copy to clipboard', async () => { + await screenshotSelector.init('black', true); + + const result = await screenshotSelector.captureElement(mockElement); + + expect(global.html2canvas).toHaveBeenCalledWith(mockElement, expect.objectContaining({ + backgroundColor: '#000000', // Black background + useCORS: true, + allowTaint: false + })); + + expect(result.success).toBe(true); + expect(result.clipboardResult.success).toBe(true); + expect(result.message).toBe('Screenshot downloaded and copied to clipboard!'); + }); + + test('should capture with white background and copy to clipboard', async () => { + await screenshotSelector.init('white', true); + + const result = await screenshotSelector.captureElement(mockElement); + + expect(global.html2canvas).toHaveBeenCalledWith(mockElement, expect.objectContaining({ + backgroundColor: '#ffffff', // White background + useCORS: true, + allowTaint: false + })); + + expect(result.success).toBe(true); + expect(result.clipboardResult.success).toBe(true); + expect(result.message).toBe('Screenshot downloaded and copied to clipboard!'); + }); + + test('should capture without clipboard when disabled', async () => { + await screenshotSelector.init('black', false); + + const result = await screenshotSelector.captureElement(mockElement); + + expect(result.success).toBe(true); + expect(result.downloadSuccess).toBe(true); + expect(result.clipboardResult).toBe(null); // No clipboard operation attempted + expect(result.message).toBe('Screenshot downloaded successfully!'); + expect(global.navigator.clipboard.write).not.toHaveBeenCalled(); + }); + + test('should handle clipboard failure gracefully while maintaining download', async () => { + // Mock clipboard failure + global.navigator.clipboard.write.mockRejectedValue(new DOMException('Permission denied', 'NotAllowedError')); + + await screenshotSelector.init('black', true); + + const result = await screenshotSelector.captureElement(mockElement); + + expect(result.success).toBe(true); + expect(result.downloadSuccess).toBe(true); + expect(result.clipboardResult.success).toBe(false); + expect(result.clipboardResult.errorType).toBe('permission_denied'); + expect(result.message).toContain('Screenshot downloaded successfully!'); + expect(result.message).toContain('Clipboard copy failed'); + }); + + test('should handle clipboard API unavailable scenario', async () => { + // Mock clipboard API not available + global.navigator.clipboard = undefined; + + await screenshotSelector.init('transparent', true); + + const result = await screenshotSelector.captureElement(mockElement); + + expect(result.success).toBe(true); + expect(result.downloadSuccess).toBe(true); + expect(result.clipboardResult.success).toBe(false); + expect(result.clipboardResult.errorType).toBe('api_unavailable'); + expect(result.message).toContain('Screenshot downloaded successfully!'); + expect(result.message).toContain('Clipboard copy failed'); + }); +}); + +describe('Integration Scenarios - Complete Workflow', () => { + let mockPopupElements; + + beforeEach(() => { + jest.clearAllMocks(); + + // Mock popup elements + mockPopupElements = { + clipboardToggle: { checked: true, addEventListener: jest.fn() }, + backgroundSelect: { value: 'black', addEventListener: jest.fn() }, + startButton: { addEventListener: jest.fn(), style: { display: 'block' } }, + status: { textContent: '', className: '', style: { display: 'none' } } + }; + + global.document.getElementById = jest.fn((id) => { + switch (id) { + case 'clipboard-toggle': return mockPopupElements.clipboardToggle; + case 'background-select': return mockPopupElements.backgroundSelect; + case 'start-selector': return mockPopupElements.startButton; + case 'status': return mockPopupElements.status; + default: return null; + } + }); + + // Mock Chrome APIs + global.chrome.storage.sync.get.mockResolvedValue({ + backgroundPreference: 'black', + copyToClipboard: true + }); + global.chrome.tabs.query.mockResolvedValue([{ id: 123, url: 'https://example.com' }]); + global.chrome.tabs.sendMessage.mockResolvedValue(); + }); + + test('should complete full workflow from popup to content script', async () => { + let startButtonCallback; + mockPopupElements.startButton.addEventListener = jest.fn((event, callback) => { + if (event === 'click') startButtonCallback = callback; + }); + + // Simulate popup initialization + const popupScript = ` + document.addEventListener('DOMContentLoaded', async () => { + const backgroundSelect = document.getElementById('background-select'); + const clipboardToggle = document.getElementById('clipboard-toggle'); + const startBtn = document.getElementById('start-selector'); + + // Load preferences + const saved = await chrome.storage.sync.get(['backgroundPreference', 'copyToClipboard']); + if (saved.backgroundPreference) { + backgroundSelect.value = saved.backgroundPreference; + } + clipboardToggle.checked = saved.copyToClipboard !== undefined ? saved.copyToClipboard : true; + + // Start selector + startBtn.addEventListener('click', async () => { + const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); + + await chrome.tabs.sendMessage(tab.id, { + action: 'startSelector', + background: backgroundSelect.value, + copyToClipboard: clipboardToggle.checked + }); + }); + }); + `; + + // Execute popup initialization + await new Promise(resolve => { + global.document.addEventListener = jest.fn((event, callback) => { + if (event === 'DOMContentLoaded') { + callback(); + resolve(); + } + }); + eval(popupScript); + }); + + // Verify preferences were loaded + expect(mockPopupElements.backgroundSelect.value).toBe('black'); + expect(mockPopupElements.clipboardToggle.checked).toBe(true); + + // Simulate start button click + await startButtonCallback(); + + // Verify message was sent to content script with correct parameters + expect(global.chrome.tabs.sendMessage).toHaveBeenCalledWith(123, { + action: 'startSelector', + background: 'black', + copyToClipboard: true + }); + }); + + test('should handle preference changes and persist them', async () => { + let clipboardChangeCallback; + let backgroundChangeCallback; + + mockPopupElements.clipboardToggle.addEventListener = jest.fn((event, callback) => { + if (event === 'change') clipboardChangeCallback = callback; + }); + + mockPopupElements.backgroundSelect.addEventListener = jest.fn((event, callback) => { + if (event === 'change') backgroundChangeCallback = callback; + }); + + // Initialize popup + await new Promise(resolve => { + global.document.addEventListener = jest.fn((event, callback) => { + if (event === 'DOMContentLoaded') { + callback(); + resolve(); + } + }); + + const popupScript = ` + document.addEventListener('DOMContentLoaded', async () => { + const backgroundSelect = document.getElementById('background-select'); + const clipboardToggle = document.getElementById('clipboard-toggle'); + + backgroundSelect.addEventListener('change', () => { + chrome.storage.sync.set({ backgroundPreference: backgroundSelect.value }); + }); + + clipboardToggle.addEventListener('change', () => { + chrome.storage.sync.set({ copyToClipboard: clipboardToggle.checked }); + }); + }); + `; + eval(popupScript); + }); + + // Change background preference + mockPopupElements.backgroundSelect.value = 'transparent'; + backgroundChangeCallback(); + + expect(global.chrome.storage.sync.set).toHaveBeenCalledWith({ backgroundPreference: 'transparent' }); + + // Change clipboard preference + mockPopupElements.clipboardToggle.checked = false; + clipboardChangeCallback(); + + expect(global.chrome.storage.sync.set).toHaveBeenCalledWith({ copyToClipboard: false }); + }); + + test('should handle incompatible pages gracefully', async () => { + // Mock incompatible page + global.chrome.tabs.query.mockResolvedValue([{ + id: 123, + url: 'chrome://settings/' + }]); + + let startButtonCallback; + mockPopupElements.startButton.addEventListener = jest.fn((event, callback) => { + if (event === 'click') startButtonCallback = callback; + }); + + // Initialize popup with incompatible page handling + const popupScript = ` + document.addEventListener('DOMContentLoaded', async () => { + const startBtn = document.getElementById('start-selector'); + const status = document.getElementById('status'); + + startBtn.addEventListener('click', async () => { + const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); + + if (tab.url.startsWith('chrome://') || + tab.url.startsWith('chrome-extension://') || + tab.url.startsWith('edge://') || + tab.url.startsWith('about:')) { + status.textContent = 'Error: Cannot capture screenshots on this page type.'; + status.className = 'status error'; + status.style.display = 'block'; + return; + } + + // Normal flow would continue here + }); + }); + `; + + await new Promise(resolve => { + global.document.addEventListener = jest.fn((event, callback) => { + if (event === 'DOMContentLoaded') { + callback(); + resolve(); + } + }); + eval(popupScript); + }); + + // Simulate start button click on incompatible page + await startButtonCallback(); + + // Verify error message was shown + expect(mockPopupElements.status.textContent).toBe('Error: Cannot capture screenshots on this page type.'); + expect(mockPopupElements.status.className).toBe('status error'); + expect(mockPopupElements.status.style.display).toBe('block'); + + // Verify no message was sent to content script + expect(global.chrome.tabs.sendMessage).not.toHaveBeenCalled(); + }); +}); \ No newline at end of file diff --git a/tests/preference-persistence.test.js b/tests/preference-persistence.test.js new file mode 100644 index 0000000..6db7941 --- /dev/null +++ b/tests/preference-persistence.test.js @@ -0,0 +1,414 @@ +/** + * Test suite for clipboard preference persistence across browser sessions + * Tests storage, retrieval, and default behavior of clipboard preferences + */ + +// Mock Chrome APIs +global.chrome = { + storage: { + sync: { + get: jest.fn(), + set: jest.fn() + } + }, + runtime: { + sendMessage: jest.fn() + }, + tabs: { + query: jest.fn(), + sendMessage: jest.fn() + } +}; + +// Mock DOM APIs +global.document = { + createElement: jest.fn(() => ({ + style: {}, + classList: { add: jest.fn(), remove: jest.fn() }, + appendChild: jest.fn(), + click: jest.fn(), + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + id: '', + checked: false, + value: '' + })), + head: { appendChild: jest.fn() }, + body: { appendChild: jest.fn() }, + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + querySelectorAll: jest.fn(() => []), + getElementById: jest.fn() +}; + +global.window = { + close: jest.fn(), + setTimeout: jest.fn((fn) => fn()) +}; + +describe('Clipboard Preference Persistence', () => { + let mockClipboardToggle; + let mockBackgroundSelect; + + beforeEach(() => { + jest.clearAllMocks(); + + // Mock DOM elements + mockClipboardToggle = { + checked: false, + addEventListener: jest.fn() + }; + + mockBackgroundSelect = { + value: 'black', + addEventListener: jest.fn() + }; + + global.document.getElementById = jest.fn((id) => { + switch (id) { + case 'clipboard-toggle': + return mockClipboardToggle; + case 'background-select': + return mockBackgroundSelect; + case 'start-selector': + return { addEventListener: jest.fn(), style: { display: 'block' } }; + case 'stop-selector': + return { addEventListener: jest.fn(), style: { display: 'none' } }; + case 'status': + return { textContent: '', className: '', style: { display: 'none' } }; + default: + return null; + } + }); + }); + + test('should load default clipboard preference when no saved preference exists', async () => { + // Mock storage returning no saved preferences + global.chrome.storage.sync.get.mockResolvedValue({}); + + // Simulate popup script loading + const popupScript = ` + document.addEventListener('DOMContentLoaded', async () => { + const clipboardToggle = document.getElementById('clipboard-toggle'); + const saved = await chrome.storage.sync.get(['backgroundPreference', 'copyToClipboard']); + clipboardToggle.checked = saved.copyToClipboard !== undefined ? saved.copyToClipboard : true; + }); + `; + + // Execute the popup script logic + await new Promise(resolve => { + global.document.addEventListener = jest.fn((event, callback) => { + if (event === 'DOMContentLoaded') { + callback(); + resolve(); + } + }); + eval(popupScript); + }); + + expect(global.chrome.storage.sync.get).toHaveBeenCalledWith(['backgroundPreference', 'copyToClipboard']); + expect(mockClipboardToggle.checked).toBe(true); // Default should be true + }); + + test('should load saved clipboard preference when it exists', async () => { + // Mock storage returning saved preferences + global.chrome.storage.sync.get.mockResolvedValue({ + backgroundPreference: 'white', + copyToClipboard: false + }); + + // Simulate popup script loading + const popupScript = ` + document.addEventListener('DOMContentLoaded', async () => { + const clipboardToggle = document.getElementById('clipboard-toggle'); + const saved = await chrome.storage.sync.get(['backgroundPreference', 'copyToClipboard']); + clipboardToggle.checked = saved.copyToClipboard !== undefined ? saved.copyToClipboard : true; + }); + `; + + // Execute the popup script logic + await new Promise(resolve => { + global.document.addEventListener = jest.fn((event, callback) => { + if (event === 'DOMContentLoaded') { + callback(); + resolve(); + } + }); + eval(popupScript); + }); + + expect(mockClipboardToggle.checked).toBe(false); // Should load saved value + }); + + test('should save clipboard preference when toggle is changed', async () => { + let changeCallback; + mockClipboardToggle.addEventListener = jest.fn((event, callback) => { + if (event === 'change') { + changeCallback = callback; + } + }); + + // Simulate popup script initialization + const popupScript = ` + document.addEventListener('DOMContentLoaded', async () => { + const clipboardToggle = document.getElementById('clipboard-toggle'); + clipboardToggle.addEventListener('change', () => { + chrome.storage.sync.set({ copyToClipboard: clipboardToggle.checked }); + }); + }); + `; + + // Execute the popup script logic + await new Promise(resolve => { + global.document.addEventListener = jest.fn((event, callback) => { + if (event === 'DOMContentLoaded') { + callback(); + resolve(); + } + }); + eval(popupScript); + }); + + // Simulate user toggling the checkbox + mockClipboardToggle.checked = true; + changeCallback(); + + expect(global.chrome.storage.sync.set).toHaveBeenCalledWith({ copyToClipboard: true }); + }); + + test('should persist clipboard preference across multiple sessions', async () => { + // Simulate first session - user enables clipboard + global.chrome.storage.sync.get.mockResolvedValueOnce({}); + + let firstSessionChangeCallback; + mockClipboardToggle.addEventListener = jest.fn((event, callback) => { + if (event === 'change') { + firstSessionChangeCallback = callback; + } + }); + + // First session initialization + await new Promise(resolve => { + global.document.addEventListener = jest.fn((event, callback) => { + if (event === 'DOMContentLoaded') { + callback(); + resolve(); + } + }); + + const popupScript = ` + document.addEventListener('DOMContentLoaded', async () => { + const clipboardToggle = document.getElementById('clipboard-toggle'); + const saved = await chrome.storage.sync.get(['backgroundPreference', 'copyToClipboard']); + clipboardToggle.checked = saved.copyToClipboard !== undefined ? saved.copyToClipboard : true; + + clipboardToggle.addEventListener('change', () => { + chrome.storage.sync.set({ copyToClipboard: clipboardToggle.checked }); + }); + }); + `; + eval(popupScript); + }); + + // User disables clipboard in first session + mockClipboardToggle.checked = false; + firstSessionChangeCallback(); + + expect(global.chrome.storage.sync.set).toHaveBeenCalledWith({ copyToClipboard: false }); + + // Simulate second session - should load the saved preference + jest.clearAllMocks(); + global.chrome.storage.sync.get.mockResolvedValueOnce({ + copyToClipboard: false + }); + + // Reset mock element + mockClipboardToggle.checked = true; // Reset to opposite of expected + + // Second session initialization + await new Promise(resolve => { + global.document.addEventListener = jest.fn((event, callback) => { + if (event === 'DOMContentLoaded') { + callback(); + resolve(); + } + }); + + const popupScript = ` + document.addEventListener('DOMContentLoaded', async () => { + const clipboardToggle = document.getElementById('clipboard-toggle'); + const saved = await chrome.storage.sync.get(['backgroundPreference', 'copyToClipboard']); + clipboardToggle.checked = saved.copyToClipboard !== undefined ? saved.copyToClipboard : true; + }); + `; + eval(popupScript); + }); + + expect(mockClipboardToggle.checked).toBe(false); // Should load saved preference from first session + }); + + test('should handle storage errors gracefully', async () => { + // Mock storage error + global.chrome.storage.sync.get.mockRejectedValue(new Error('Storage error')); + + let errorOccurred = false; + + try { + const popupScript = ` + document.addEventListener('DOMContentLoaded', async () => { + try { + const clipboardToggle = document.getElementById('clipboard-toggle'); + const saved = await chrome.storage.sync.get(['backgroundPreference', 'copyToClipboard']); + clipboardToggle.checked = saved.copyToClipboard !== undefined ? saved.copyToClipboard : true; + } catch (error) { + // Should fall back to default behavior + const clipboardToggle = document.getElementById('clipboard-toggle'); + clipboardToggle.checked = true; // Default value + } + }); + `; + + await new Promise(resolve => { + global.document.addEventListener = jest.fn((event, callback) => { + if (event === 'DOMContentLoaded') { + callback(); + resolve(); + } + }); + eval(popupScript); + }); + } catch (error) { + errorOccurred = true; + } + + expect(errorOccurred).toBe(false); // Should handle error gracefully + expect(mockClipboardToggle.checked).toBe(true); // Should fall back to default + }); + + test('should save both background and clipboard preferences independently', async () => { + let clipboardChangeCallback; + let backgroundChangeCallback; + + mockClipboardToggle.addEventListener = jest.fn((event, callback) => { + if (event === 'change') { + clipboardChangeCallback = callback; + } + }); + + mockBackgroundSelect.addEventListener = jest.fn((event, callback) => { + if (event === 'change') { + backgroundChangeCallback = callback; + } + }); + + // Initialize popup script + const popupScript = ` + document.addEventListener('DOMContentLoaded', async () => { + const backgroundSelect = document.getElementById('background-select'); + const clipboardToggle = document.getElementById('clipboard-toggle'); + + backgroundSelect.addEventListener('change', () => { + chrome.storage.sync.set({ backgroundPreference: backgroundSelect.value }); + }); + + clipboardToggle.addEventListener('change', () => { + chrome.storage.sync.set({ copyToClipboard: clipboardToggle.checked }); + }); + }); + `; + + await new Promise(resolve => { + global.document.addEventListener = jest.fn((event, callback) => { + if (event === 'DOMContentLoaded') { + callback(); + resolve(); + } + }); + eval(popupScript); + }); + + // Change background preference + mockBackgroundSelect.value = 'transparent'; + backgroundChangeCallback(); + + expect(global.chrome.storage.sync.set).toHaveBeenCalledWith({ backgroundPreference: 'transparent' }); + + // Change clipboard preference + mockClipboardToggle.checked = false; + clipboardChangeCallback(); + + expect(global.chrome.storage.sync.set).toHaveBeenCalledWith({ copyToClipboard: false }); + + // Verify both calls were made independently + expect(global.chrome.storage.sync.set).toHaveBeenCalledTimes(2); + }); + + test('should include clipboard preference in message to content script', async () => { + // Mock tab query and message sending + global.chrome.tabs.query.mockResolvedValue([{ id: 123, url: 'https://example.com' }]); + global.chrome.tabs.sendMessage.mockResolvedValue(); + + let startButtonCallback; + const mockStartButton = { + addEventListener: jest.fn((event, callback) => { + if (event === 'click') { + startButtonCallback = callback; + } + }), + style: { display: 'block' } + }; + + global.document.getElementById = jest.fn((id) => { + switch (id) { + case 'start-selector': + return mockStartButton; + case 'clipboard-toggle': + return { checked: true }; // Clipboard enabled + case 'background-select': + return { value: 'white' }; + case 'status': + return { textContent: '', className: '', style: { display: 'none' } }; + default: + return mockClipboardToggle; + } + }); + + // Initialize popup script + const popupScript = ` + document.addEventListener('DOMContentLoaded', async () => { + const startBtn = document.getElementById('start-selector'); + const backgroundSelect = document.getElementById('background-select'); + const clipboardToggle = document.getElementById('clipboard-toggle'); + + startBtn.addEventListener('click', async () => { + const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); + + await chrome.tabs.sendMessage(tab.id, { + action: 'startSelector', + background: backgroundSelect.value, + copyToClipboard: clipboardToggle.checked + }); + }); + }); + `; + + await new Promise(resolve => { + global.document.addEventListener = jest.fn((event, callback) => { + if (event === 'DOMContentLoaded') { + callback(); + resolve(); + } + }); + eval(popupScript); + }); + + // Simulate start button click + await startButtonCallback(); + + expect(global.chrome.tabs.sendMessage).toHaveBeenCalledWith(123, { + action: 'startSelector', + background: 'white', + copyToClipboard: true + }); + }); +}); \ No newline at end of file diff --git a/tests/setup.js b/tests/setup.js new file mode 100644 index 0000000..4ba46b1 --- /dev/null +++ b/tests/setup.js @@ -0,0 +1,237 @@ +/** + * Jest setup file for clipboard functionality tests + * Provides common mocks and utilities for all test files + */ + +// Global test utilities +global.createMockBlob = (data = 'mock data', type = 'image/png', size = 1024) => { + const blob = new Blob([data], { type }); + Object.defineProperty(blob, 'size', { value: size }); + return blob; +}; + +global.createMockCanvas = (width = 100, height = 100) => { + return { + width, + height, + toBlob: jest.fn((callback, format = 'image/png') => { + const blob = global.createMockBlob('mock canvas data', format); + callback(blob); + }), + toDataURL: jest.fn((format = 'image/png') => `data:${format};base64,mockcanvasdata`), + getContext: jest.fn(() => ({ + drawImage: jest.fn(), + getImageData: jest.fn(), + putImageData: jest.fn() + })) + }; +}; + +global.createMockElement = (options = {}) => { + return { + classList: { + add: jest.fn(), + remove: jest.fn(), + contains: jest.fn(() => false) + }, + style: {}, + getBoundingClientRect: () => ({ + left: options.left || 0, + top: options.top || 0, + width: options.width || 100, + height: options.height || 100 + }), + scrollHeight: options.scrollHeight || 100, + clientHeight: options.clientHeight || 100, + scrollWidth: options.scrollWidth || 100, + clientWidth: options.clientWidth || 100, + children: options.children || [], + parentNode: { children: options.siblings || [] }, + tagName: options.tagName || 'DIV', + id: options.id || '', + addEventListener: jest.fn(), + removeEventListener: jest.fn() + }; +}; + +// Mock Chrome extension APIs consistently +global.mockChromeAPIs = () => { + global.chrome = { + storage: { + sync: { + get: jest.fn().mockResolvedValue({}), + set: jest.fn().mockResolvedValue() + } + }, + runtime: { + sendMessage: jest.fn().mockResolvedValue(), + onMessage: { + addListener: jest.fn() + } + }, + tabs: { + query: jest.fn().mockResolvedValue([{ id: 123, url: 'https://example.com' }]), + sendMessage: jest.fn().mockResolvedValue() + } + }; +}; + +// Mock DOM APIs consistently +global.mockDOMAPIs = () => { + global.document = { + createElement: jest.fn(() => ({ + style: {}, + classList: { add: jest.fn(), remove: jest.fn() }, + appendChild: jest.fn(), + click: jest.fn(), + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + innerHTML: '', + textContent: '', + id: '', + className: '', + checked: false, + value: '' + })), + head: { appendChild: jest.fn() }, + body: { appendChild: jest.fn() }, + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + querySelectorAll: jest.fn(() => []), + getElementById: jest.fn(() => null) + }; + + global.window = { + getComputedStyle: jest.fn(() => ({})), + devicePixelRatio: 2, + isSecureContext: true, + CSS: { escape: jest.fn(str => str) }, + setTimeout: jest.fn((fn, delay) => { + if (delay === 0) fn(); + return 1; + }), + clearTimeout: jest.fn(), + close: jest.fn() + }; +}; + +// Mock clipboard APIs with different scenarios +global.mockClipboardAPI = (scenario = 'supported') => { + switch (scenario) { + case 'supported': + global.navigator = { + clipboard: { + write: jest.fn().mockResolvedValue() + } + }; + global.window.ClipboardItem = jest.fn(); + global.window.isSecureContext = true; + break; + + case 'no-clipboard': + global.navigator = {}; + break; + + case 'no-clipboarditem': + global.navigator = { + clipboard: { write: jest.fn() } + }; + global.window.ClipboardItem = undefined; + break; + + case 'insecure-context': + global.navigator = { + clipboard: { write: jest.fn() } + }; + global.window.ClipboardItem = jest.fn(); + global.window.isSecureContext = false; + break; + + case 'permission-denied': + global.navigator = { + clipboard: { + write: jest.fn().mockRejectedValue(new DOMException('Permission denied', 'NotAllowedError')) + } + }; + global.window.ClipboardItem = jest.fn(); + global.window.isSecureContext = true; + break; + + case 'security-error': + global.navigator = { + clipboard: { + write: jest.fn().mockRejectedValue(new DOMException('Security error', 'SecurityError')) + } + }; + global.window.ClipboardItem = jest.fn(); + global.window.isSecureContext = true; + break; + } +}; + +// Setup default mocks before each test +beforeEach(() => { + // Clear all mocks first + jest.clearAllMocks(); + + global.mockChromeAPIs(); + global.mockDOMAPIs(); + global.mockClipboardAPI('supported'); + + // Mock html2canvas + global.html2canvas = jest.fn().mockResolvedValue(global.createMockCanvas()); +}); + +// Cleanup after each test +afterEach(() => { + jest.restoreAllMocks(); +}); + +// Custom matchers for better test assertions +expect.extend({ + toHaveBeenCalledWithClipboardMessage(received, expectedBackground, expectedClipboard) { + const calls = received.mock.calls; + const matchingCall = calls.find(call => + call[1] && + call[1].action === 'startSelector' && + call[1].background === expectedBackground && + call[1].copyToClipboard === expectedClipboard + ); + + if (matchingCall) { + return { + message: () => `Expected not to be called with clipboard message`, + pass: true + }; + } else { + return { + message: () => `Expected to be called with clipboard message (background: ${expectedBackground}, clipboard: ${expectedClipboard})`, + pass: false + }; + } + }, + + toHaveClipboardResult(received, expectedSuccess, expectedErrorType) { + if (!received.clipboardResult) { + return { + message: () => `Expected clipboard result to exist`, + pass: false + }; + } + + const success = received.clipboardResult.success === expectedSuccess; + const errorType = !expectedErrorType || received.clipboardResult.errorType === expectedErrorType; + + if (success && errorType) { + return { + message: () => `Expected clipboard result not to match`, + pass: true + }; + } else { + return { + message: () => `Expected clipboard result (success: ${expectedSuccess}, errorType: ${expectedErrorType}), got (success: ${received.clipboardResult.success}, errorType: ${received.clipboardResult.errorType})`, + pass: false + }; + } + } +}); \ No newline at end of file