From 9dd4d706fa3c8d7a9229e9246950e01dd543d164 Mon Sep 17 00:00:00 2001 From: Karthikeyan N <84800257+KarthiDreamr@users.noreply.github.com> Date: Thu, 14 Aug 2025 00:59:57 +0530 Subject: [PATCH 1/2] Add copy to clipboard and save to pc toggle. * escaped node modules from vcs * feat: added copy to clipboard feature * feat: added tests * feat: added feature spec * feat: added toggle for save to pc * feat: added tests * refactor: change toggle from '?' button to the 'switch' component * Fix failing tests * Update gitignore and remove unnecessary files * Remove tooltips because they are unnecessary --------- Co-authored-by: Joey Yakimowich-Payne --- .gitignore | 3 + content.js | 444 +- package-lock.json | 4466 +++++++++++++++++ package.json | 37 + popup.html | 129 + popup.js | 125 +- tests/clipboard-functionality.test.js | 480 ++ tests/integration-scenarios.test.js | 557 ++ tests/preference-persistence.test.js | 414 ++ .../save-to-pc-clipboard-combinations.test.js | 641 +++ tests/setup.js | 297 ++ tests/validation-logic.test.js | 209 + 12 files changed, 7780 insertions(+), 22 deletions(-) create mode 100644 .gitignore create mode 100644 package-lock.json create mode 100644 package.json 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/save-to-pc-clipboard-combinations.test.js create mode 100644 tests/setup.js create mode 100644 tests/validation-logic.test.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c06c56b --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +.kiro/ +.vscode/ \ No newline at end of file diff --git a/content.js b/content.js index 45161dd..98b61c8 100644 --- a/content.js +++ b/content.js @@ -6,15 +6,19 @@ class ScreenshotSelector { this.overlay = null; this.style = null; this.background = 'black'; + this.copyToClipboard = true; // Default to true + this.saveToPc = true; // Default to true for backward compatibility } - async init(background = 'black') { + async init(background = 'black', copyToClipboard = true, saveToPc = true) { if (this.isActive) { console.log('Screenshot selector already active'); return; } this.background = background; + this.copyToClipboard = copyToClipboard; + this.saveToPc = saveToPc; this.isActive = true; this.createUI(); @@ -363,12 +367,36 @@ class ScreenshotSelector { async captureElement(element) { try { + // Determine loading message based on enabled operations + let loadingMessage = 'Capturing screenshot...'; + + if (this.saveToPc && this.copyToClipboard) { + // Both operations enabled + const availability = this.checkClipboardAPIAvailability(); + if (availability.available) { + loadingMessage = 'Capturing screenshot and preparing for clipboard...'; + } else { + loadingMessage = 'Capturing screenshot for download... (Clipboard not available)'; + } + } else if (this.saveToPc && !this.copyToClipboard) { + // Only save to PC enabled + loadingMessage = 'Capturing screenshot for download...'; + } else if (!this.saveToPc && this.copyToClipboard) { + // Only clipboard enabled + const availability = this.checkClipboardAPIAvailability(); + if (availability.available) { + loadingMessage = 'Capturing screenshot for clipboard...'; + } else { + loadingMessage = 'Capturing screenshot... (Clipboard not available)'; + } + } + // Show loading state this.overlay.innerHTML = `
- Capturing screenshot... + ${loadingMessage}
@@ -124,6 +211,48 @@ +
+
+ + +
+
+ Automatically download screenshots as PNG files to your default downloads folder. +
+
+ +
+
+ + +
+
+ Automatically copy screenshots to your clipboard for quick pasting. +
+
+ + + diff --git a/popup.js b/popup.js index d5fffc3..529b7e0 100644 --- a/popup.js +++ b/popup.js @@ -1,23 +1,70 @@ // Popup script for Chrome extension document.addEventListener('DOMContentLoaded', async () => { const backgroundSelect = document.getElementById('background-select'); + const clipboardToggle = document.getElementById('clipboard-toggle'); + const saveToPcToggle = document.getElementById('save-to-pc-toggle'); const startBtn = document.getElementById('start-selector'); const stopBtn = document.getElementById('stop-selector'); const status = document.getElementById('status'); + const validationWarning = document.getElementById('validation-warning'); - // Load saved background preference - const saved = await chrome.storage.sync.get(['backgroundPreference']); + // Load saved preferences + const saved = await chrome.storage.sync.get(['backgroundPreference', 'copyToClipboard', 'saveToPc']); if (saved.backgroundPreference) { backgroundSelect.value = saved.backgroundPreference; } + + // Set clipboard preference (default to true if not set) + clipboardToggle.checked = saved.copyToClipboard !== undefined ? saved.copyToClipboard : true; + + // Set save to PC preference (default to true if not set to maintain existing behavior) + saveToPcToggle.checked = saved.saveToPc !== undefined ? saved.saveToPc : true; + + // Validation function to ensure at least one output method is enabled + function validateOutputMethods() { + const hasValidOutput = clipboardToggle.checked || saveToPcToggle.checked; + + if (hasValidOutput) { + validationWarning.style.display = 'none'; + startBtn.disabled = false; + startBtn.setAttribute('aria-describedby', ''); + } else { + validationWarning.style.display = 'block'; + startBtn.disabled = true; + startBtn.setAttribute('aria-describedby', 'validation-warning'); + } + + return hasValidOutput; + } + + // Initial validation check + validateOutputMethods(); // Save background preference when changed backgroundSelect.addEventListener('change', () => { chrome.storage.sync.set({ backgroundPreference: backgroundSelect.value }); }); + // Save clipboard preference when changed + clipboardToggle.addEventListener('change', () => { + chrome.storage.sync.set({ copyToClipboard: clipboardToggle.checked }); + validateOutputMethods(); + }); + + // Save save to PC preference when changed + saveToPcToggle.addEventListener('change', () => { + chrome.storage.sync.set({ saveToPc: saveToPcToggle.checked }); + validateOutputMethods(); + }); + // Start selector startBtn.addEventListener('click', async () => { + // Validate output methods before proceeding + if (!validateOutputMethods()) { + showStatus('Error: Please enable at least one output method (Save to PC or Copy to Clipboard) to capture screenshots.', 'error'); + return; + } + const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); // Check if page is compatible @@ -34,10 +81,23 @@ document.addEventListener('DOMContentLoaded', async () => { try { await chrome.tabs.sendMessage(tab.id, { action: 'startSelector', - background: backgroundSelect.value + background: backgroundSelect.value, + copyToClipboard: clipboardToggle.checked, + saveToPc: saveToPcToggle.checked }); - showStatus('Selector activated! Hover over elements and click to capture.', 'success'); + // Generate activation message based on enabled operations + let activationMessage = 'Selector activated! Hover over elements and click to capture.'; + + if (saveToPcToggle.checked && clipboardToggle.checked) { + activationMessage = 'Selector activated! Screenshots will be saved and copied to clipboard.'; + } else if (saveToPcToggle.checked && !clipboardToggle.checked) { + activationMessage = 'Selector activated! Screenshots will be saved to downloads folder.'; + } else if (!saveToPcToggle.checked && clipboardToggle.checked) { + activationMessage = 'Selector activated! Screenshots will be copied to clipboard only.'; + } + + showStatus(activationMessage, 'success'); toggleButtons(true); // Auto-close popup after starting @@ -45,11 +105,27 @@ document.addEventListener('DOMContentLoaded', async () => { } catch (error) { console.error('Full error details:', error); + + let errorMessage = 'Error: Could not activate on this page.'; + if (error.message.includes('Could not establish connection')) { - showStatus('Error: Content script not loaded. Try refreshing the page.', 'error'); - } else { - showStatus('Error: Could not activate on this page.', 'error'); + errorMessage = 'Error: Content script not loaded. Try refreshing the page and try again.'; + } else if (error.message.includes('permission')) { + errorMessage = 'Error: Permission denied. Check if the page allows extensions.'; + } else if (error.message.includes('protocol')) { + errorMessage = 'Error: Cannot capture screenshots on this page type (chrome://, file://, etc.).'; } + + // Add context about what operations were attempted + if (saveToPcToggle.checked && clipboardToggle.checked) { + errorMessage += ' Neither file download nor clipboard copy could be set up.'; + } else if (saveToPcToggle.checked && !clipboardToggle.checked) { + errorMessage += ' File download could not be set up.'; + } else if (!saveToPcToggle.checked && clipboardToggle.checked) { + errorMessage += ' Clipboard copy could not be set up.'; + } + + showStatus(errorMessage, 'error'); } }); @@ -69,6 +145,8 @@ document.addEventListener('DOMContentLoaded', async () => { // Check current state checkSelectorState(); + // Tooltips removed + function toggleButtons(isActive) { if (isActive) { startBtn.style.display = 'none'; @@ -111,6 +189,8 @@ document.addEventListener('DOMContentLoaded', async () => { console.log('Content script not ready or selector inactive:', error.message); } } + + // Tooltips removed: no setup needed }); // Listen for messages from content script @@ -119,15 +199,44 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { const startBtn = document.getElementById('start-selector'); const stopBtn = document.getElementById('stop-selector'); const status = document.getElementById('status'); + const saveToPcToggle = document.getElementById('save-to-pc-toggle'); + const clipboardToggle = document.getElementById('clipboard-toggle'); startBtn.style.display = 'block'; stopBtn.style.display = 'none'; if (message.reason === 'screenshot-taken') { - status.textContent = 'Screenshot captured!'; + // Generate success message based on current toggle states + let successMessage = 'Screenshot captured!'; + + if (saveToPcToggle.checked && clipboardToggle.checked) { + successMessage = 'Screenshot saved and copied to clipboard!'; + } else if (saveToPcToggle.checked && !clipboardToggle.checked) { + successMessage = 'Screenshot saved to downloads folder!'; + } else if (!saveToPcToggle.checked && clipboardToggle.checked) { + successMessage = 'Screenshot copied to clipboard!'; + } + + status.textContent = successMessage; status.className = 'status success'; status.style.display = 'block'; setTimeout(() => status.style.display = 'none', 2000); + } else if (message.reason === 'screenshot-failed') { + // Handle screenshot failure + let failureMessage = 'Screenshot capture failed. Please try again.'; + + if (saveToPcToggle.checked && clipboardToggle.checked) { + failureMessage = 'Screenshot capture failed. Neither file download nor clipboard copy could be completed.'; + } else if (saveToPcToggle.checked && !clipboardToggle.checked) { + failureMessage = 'Screenshot capture failed. File download could not be completed.'; + } else if (!saveToPcToggle.checked && clipboardToggle.checked) { + failureMessage = 'Screenshot capture failed. Clipboard copy could not be completed.'; + } + + status.textContent = failureMessage; + status.className = 'status error'; + status.style.display = 'block'; + setTimeout(() => status.style.display = 'none', 3000); } } }); \ No newline at end of file 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..e32a8c1 --- /dev/null +++ b/tests/integration-scenarios.test.js @@ -0,0 +1,557 @@ +/** + * 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(); + + // Reset clipboard mock for each test + if (!global.navigator) { + global.navigator = {}; + } + global.navigator.clipboard = { + write: jest.fn().mockResolvedValue() + }; + global.window.ClipboardItem = jest.fn().mockImplementation((data) => { + return { data, type: 'image/png' }; + }); + global.window.isSecureContext = true; + + // 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); + + + + // Import ScreenshotSelector (simplified for testing) + const ScreenshotSelector = class { + constructor() { + this.isActive = false; + this.background = 'black'; + this.copyToClipboard = true; + this.saveToPc = true; + this.overlay = null; + } + + async init(background = 'black', copyToClipboard = true, saveToPc = true) { + this.background = background; + this.copyToClipboard = copyToClipboard; + this.saveToPc = saveToPc; + 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 (matching new message format) + let successMessage = 'Screenshot captured!'; + let messageIcon = '✅'; + + if (this.saveToPc && this.copyToClipboard) { + // Both operations enabled + if (clipboardResult && clipboardResult.success) { + successMessage = 'Screenshot saved and copied to clipboard!'; + messageIcon = '✅'; + } else { + // Save succeeded but clipboard failed + const errorMessage = clipboardResult ? clipboardResult.error.toLowerCase() : 'clipboard copy failed'; + successMessage = `Screenshot saved successfully! However, ${errorMessage}`; + messageIcon = '⚠️'; + } + } else if (this.saveToPc && !this.copyToClipboard) { + // Only save to PC enabled + successMessage = 'Screenshot saved to downloads folder!'; + messageIcon = '💾'; + } else if (!this.saveToPc && this.copyToClipboard) { + // Only clipboard enabled + if (clipboardResult && clipboardResult.success) { + successMessage = 'Screenshot copied to clipboard!'; + messageIcon = '📋'; + } else { + // Clipboard failed - this is a complete failure since no other output method is enabled + const errorMessage = clipboardResult ? clipboardResult.error : 'clipboard copy failed'; + successMessage = `Screenshot capture failed: ${errorMessage}`; + messageIcon = '❌'; + } + } + + 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 saved 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 saved 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 saved and copied to clipboard!'); + }); + + test('should capture without clipboard when disabled', async () => { + await screenshotSelector.init('black', false, true); // saveToPc = true, copyToClipboard = 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 saved to downloads folder!'); + if (global.navigator.clipboard?.write) { + expect(global.navigator.clipboard.write).not.toHaveBeenCalled(); + } + }); + + test('should handle clipboard failure gracefully while maintaining download', async () => { + // Reset and mock clipboard failure + global.navigator.clipboard = { + write: jest.fn().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 saved successfully!'); + expect(result.message).toContain('permission denied'); + }); + + 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 saved successfully!'); + expect(result.message).toContain('clipboard'); + }); +}); + +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/save-to-pc-clipboard-combinations.test.js b/tests/save-to-pc-clipboard-combinations.test.js new file mode 100644 index 0000000..4506c35 --- /dev/null +++ b/tests/save-to-pc-clipboard-combinations.test.js @@ -0,0 +1,641 @@ +/** + * Comprehensive test suite for all combinations of save to PC and clipboard settings + * Tests all four combinations: both enabled, save-only, clipboard-only, both disabled + * Includes preference persistence testing across browser sessions + */ + +// Mock Chrome APIs +global.chrome = { + storage: { + sync: { + get: jest.fn(), + set: jest.fn() + } + }, + runtime: { + sendMessage: jest.fn(), + onMessage: { + addListener: jest.fn() + } + }, + tabs: { + query: jest.fn(), + sendMessage: jest.fn() + } +}; + +// Mock DOM APIs +const mockCreateElement = jest.fn(); +const mockLink = { + href: '', + download: '', + click: jest.fn(), + style: {}, + classList: { add: jest.fn(), remove: jest.fn() } +}; + +global.document = { + createElement: mockCreateElement, + 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()), + getComputedStyle: jest.fn(() => ({})), + devicePixelRatio: 2, + isSecureContext: true, + CSS: { escape: jest.fn(str => str) } +}; + +// Mock html2canvas +global.html2canvas = jest.fn(); + +// Mock ScreenshotSelector class for testing +class MockScreenshotSelector { + constructor() { + this.copyToClipboard = true; + this.saveToPc = true; + this.background = 'black'; + this.isActive = false; + } + + async init(background = 'black', copyToClipboard = true, saveToPc = true) { + this.background = background; + this.copyToClipboard = copyToClipboard; + this.saveToPc = saveToPc; + this.isActive = true; + } + + 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 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' }; + } else if (error.name === 'SecurityError') { + return { success: false, error: 'Security error', errorType: 'security_error' }; + } + return { success: false, error: error.message, errorType: 'unexpected' }; + } + } + + async captureElement(element) { + // Mock canvas creation + const mockCanvas = { + toBlob: jest.fn((callback) => { + const mockBlob = new Blob(['mock data'], { type: 'image/png' }); + callback(mockBlob); + }), + toDataURL: jest.fn(() => 'data:image/png;base64,mockdata') + }; + + // Mock html2canvas + global.html2canvas.mockResolvedValue(mockCanvas); + + const canvas = await global.html2canvas(element); + + // Conditionally download based on saveToPc setting + if (this.saveToPc) { + 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 operations if enabled + let clipboardResult = null; + if (this.copyToClipboard) { + clipboardResult = await this.copyCanvasToClipboard(canvas); + } + + return { + success: true, + downloadPerformed: this.saveToPc, + clipboardResult: clipboardResult + }; + } +} + +describe('Save to PC and Clipboard Combinations', () => { + let screenshotSelector; + let mockElement; + + beforeEach(() => { + jest.clearAllMocks(); + + // Reset mocks + mockCreateElement.mockClear(); + mockCreateElement.mockReturnValue(mockLink); + // Ensure our mock is used even if the test environment adjusted document + if (global.document) { + global.document.createElement = mockCreateElement; + } + mockLink.click.mockClear(); + + // Setup clipboard API mocks + global.navigator = { + clipboard: { + write: jest.fn().mockResolvedValue() + } + }; + global.window.ClipboardItem = jest.fn(); + global.window.isSecureContext = true; + + screenshotSelector = new MockScreenshotSelector(); + mockElement = { + classList: { remove: jest.fn() }, + style: {}, + getBoundingClientRect: () => ({ left: 0, top: 0, width: 100, height: 100 }), + scrollHeight: 100, + clientHeight: 100, + scrollWidth: 100, + clientWidth: 100 + }; + }); + + describe('Combination 1: Both Save to PC and Clipboard Enabled (saveToPc: true, clipboard: true)', () => { + beforeEach(() => { + screenshotSelector.saveToPc = true; + screenshotSelector.copyToClipboard = true; + }); + + test('should perform both download and clipboard operations when both are enabled', async () => { + const result = await screenshotSelector.captureElement(mockElement); + + expect(result.success).toBe(true); + expect(result.downloadPerformed).toBe(true); + expect(result.clipboardResult).toBeDefined(); + expect(result.clipboardResult.success).toBe(true); + + // Verify download was triggered + expect(mockCreateElement).toHaveBeenCalledWith('a'); + expect(mockLink.click).toHaveBeenCalled(); + + // Verify clipboard operation was attempted + expect(global.navigator.clipboard.write).toHaveBeenCalled(); + }); + + test('should succeed with download even if clipboard fails when both are enabled', async () => { + // Mock clipboard failure + if (global.navigator.clipboard && global.navigator.clipboard.write) { + global.navigator.clipboard.write.mockRejectedValue(new DOMException('Permission denied', 'NotAllowedError')); + } + + const result = await screenshotSelector.captureElement(mockElement); + + expect(result.success).toBe(true); + expect(result.downloadPerformed).toBe(true); + expect(result.clipboardResult.success).toBe(false); + expect(result.clipboardResult.errorType).toBe('permission_denied'); + + // Download should still work + expect(mockCreateElement).toHaveBeenCalledWith('a'); + expect(mockLink.click).toHaveBeenCalled(); + }); + + test('should initialize with both settings correctly', async () => { + await screenshotSelector.init('white', true, true); + + expect(screenshotSelector.background).toBe('white'); + expect(screenshotSelector.copyToClipboard).toBe(true); + expect(screenshotSelector.saveToPc).toBe(true); + expect(screenshotSelector.isActive).toBe(true); + }); + }); + + describe('Combination 2: Save-only Mode (saveToPc: true, clipboard: false)', () => { + beforeEach(() => { + screenshotSelector.saveToPc = true; + screenshotSelector.copyToClipboard = false; + }); + + test('should only perform download when save-only mode is enabled', async () => { + const result = await screenshotSelector.captureElement(mockElement); + + expect(result.success).toBe(true); + expect(result.downloadPerformed).toBe(true); + expect(result.clipboardResult).toBeNull(); + + // Verify download was triggered + expect(mockCreateElement).toHaveBeenCalledWith('a'); + expect(mockLink.click).toHaveBeenCalled(); + + // Verify clipboard operation was NOT attempted + expect(global.navigator.clipboard.write).not.toHaveBeenCalled(); + }); + + test('should initialize with save-only settings correctly', async () => { + await screenshotSelector.init('transparent', false, true); + + expect(screenshotSelector.background).toBe('transparent'); + expect(screenshotSelector.copyToClipboard).toBe(false); + expect(screenshotSelector.saveToPc).toBe(true); + expect(screenshotSelector.isActive).toBe(true); + }); + }); + + describe('Combination 3: Clipboard-only Mode (saveToPc: false, clipboard: true)', () => { + beforeEach(() => { + screenshotSelector.saveToPc = false; + screenshotSelector.copyToClipboard = true; + }); + + test('should only perform clipboard operation when clipboard-only mode is enabled', async () => { + const result = await screenshotSelector.captureElement(mockElement); + + expect(result.success).toBe(true); + expect(result.downloadPerformed).toBe(false); + expect(result.clipboardResult).toBeDefined(); + expect(result.clipboardResult.success).toBe(true); + + // Verify download was NOT triggered + expect(mockCreateElement).not.toHaveBeenCalledWith('a'); + + // Verify clipboard operation was attempted + expect(global.navigator.clipboard.write).toHaveBeenCalled(); + }); + + test('should initialize with clipboard-only settings correctly', async () => { + await screenshotSelector.init('black', true, false); + + expect(screenshotSelector.background).toBe('black'); + expect(screenshotSelector.copyToClipboard).toBe(true); + expect(screenshotSelector.saveToPc).toBe(false); + expect(screenshotSelector.isActive).toBe(true); + }); + }); + + describe('Combination 4: Both Disabled (saveToPc: false, clipboard: false)', () => { + beforeEach(() => { + screenshotSelector.saveToPc = false; + screenshotSelector.copyToClipboard = false; + }); + + test('should perform neither operation when both are disabled', async () => { + const result = await screenshotSelector.captureElement(mockElement); + + expect(result.success).toBe(true); + expect(result.downloadPerformed).toBe(false); + expect(result.clipboardResult).toBeNull(); + + // Verify download was NOT triggered + expect(mockCreateElement).not.toHaveBeenCalledWith('a'); + + // Verify clipboard operation was NOT attempted + expect(global.navigator.clipboard.write).not.toHaveBeenCalled(); + }); + + test('should initialize with both disabled settings correctly', async () => { + await screenshotSelector.init('white', false, false); + + expect(screenshotSelector.background).toBe('white'); + expect(screenshotSelector.copyToClipboard).toBe(false); + expect(screenshotSelector.saveToPc).toBe(false); + expect(screenshotSelector.isActive).toBe(true); + }); + + test('should still create canvas even when both outputs are disabled', async () => { + await screenshotSelector.captureElement(mockElement); + + // html2canvas should still be called (needed for potential future operations) + expect(global.html2canvas).toHaveBeenCalledWith(mockElement); + }); + }); +}); + +describe('Popup Validation Logic for All Combinations', () => { + let clipboardToggle; + let saveToPcToggle; + let startBtn; + let validationWarning; + let validateOutputMethods; + + beforeEach(() => { + jest.clearAllMocks(); + + // Mock DOM elements + clipboardToggle = { + checked: true, + addEventListener: jest.fn() + }; + + saveToPcToggle = { + checked: true, + addEventListener: jest.fn() + }; + + startBtn = { + disabled: false, + setAttribute: jest.fn(), + addEventListener: jest.fn() + }; + + validationWarning = { + style: { display: 'none' } + }; + + global.document.getElementById = jest.fn((id) => { + switch (id) { + case 'clipboard-toggle': return clipboardToggle; + case 'save-to-pc-toggle': return saveToPcToggle; + case 'start-selector': return startBtn; + case 'validation-warning': return validationWarning; + default: return null; + } + }); + + // Define validation function + validateOutputMethods = function() { + const hasValidOutput = clipboardToggle.checked || saveToPcToggle.checked; + + if (hasValidOutput) { + validationWarning.style.display = 'none'; + startBtn.disabled = false; + startBtn.setAttribute('aria-describedby', ''); + } else { + validationWarning.style.display = 'block'; + startBtn.disabled = true; + startBtn.setAttribute('aria-describedby', 'validation-warning'); + } + + return hasValidOutput; + }; + }); + + test('should validate successfully when both save to PC and clipboard are enabled', () => { + clipboardToggle.checked = true; + saveToPcToggle.checked = true; + + const result = validateOutputMethods(); + + expect(result).toBe(true); + expect(validationWarning.style.display).toBe('none'); + expect(startBtn.disabled).toBe(false); + expect(startBtn.setAttribute).toHaveBeenCalledWith('aria-describedby', ''); + }); + + test('should validate successfully when only save to PC is enabled', () => { + clipboardToggle.checked = false; + saveToPcToggle.checked = true; + + const result = validateOutputMethods(); + + expect(result).toBe(true); + expect(validationWarning.style.display).toBe('none'); + expect(startBtn.disabled).toBe(false); + expect(startBtn.setAttribute).toHaveBeenCalledWith('aria-describedby', ''); + }); + + test('should validate successfully when only clipboard is enabled', () => { + clipboardToggle.checked = true; + saveToPcToggle.checked = false; + + const result = validateOutputMethods(); + + expect(result).toBe(true); + expect(validationWarning.style.display).toBe('none'); + expect(startBtn.disabled).toBe(false); + expect(startBtn.setAttribute).toHaveBeenCalledWith('aria-describedby', ''); + }); + + test('should fail validation when both save to PC and clipboard are disabled', () => { + clipboardToggle.checked = false; + saveToPcToggle.checked = false; + + const result = validateOutputMethods(); + + expect(result).toBe(false); + expect(validationWarning.style.display).toBe('block'); + expect(startBtn.disabled).toBe(true); + expect(startBtn.setAttribute).toHaveBeenCalledWith('aria-describedby', 'validation-warning'); + }); +}); + +describe('Preference Persistence Across Browser Sessions', () => { + let mockClipboardToggle; + let mockSaveToPcToggle; + + beforeEach(() => { + jest.clearAllMocks(); + + // Mock DOM elements + mockClipboardToggle = { + checked: false, + addEventListener: jest.fn() + }; + + mockSaveToPcToggle = { + checked: false, + addEventListener: jest.fn() + }; + + global.document.getElementById = jest.fn((id) => { + switch (id) { + case 'clipboard-toggle': + return mockClipboardToggle; + case 'save-to-pc-toggle': + return mockSaveToPcToggle; + default: + return null; + } + }); + }); + + test('should default to both enabled for new installations', async () => { + // Mock no saved preferences (new installation) + global.chrome.storage.sync.get.mockResolvedValueOnce({}); + + const popupScript = ` + document.addEventListener('DOMContentLoaded', async () => { + const clipboardToggle = document.getElementById('clipboard-toggle'); + const saveToPcToggle = document.getElementById('save-to-pc-toggle'); + const saved = await chrome.storage.sync.get(['backgroundPreference', 'copyToClipboard', 'saveToPc']); + + clipboardToggle.checked = saved.copyToClipboard !== undefined ? saved.copyToClipboard : true; + saveToPcToggle.checked = saved.saveToPc !== undefined ? saved.saveToPc : true; + }); + `; + + await new Promise(resolve => { + global.document.addEventListener = jest.fn((event, callback) => { + if (event === 'DOMContentLoaded') { + callback(); + resolve(); + } + }); + eval(popupScript); + }); + + expect(mockClipboardToggle.checked).toBe(true); // Default + expect(mockSaveToPcToggle.checked).toBe(true); // Default + }); + + test('should persist save-only preferences across sessions', async () => { + // First session - user sets save-only mode + global.chrome.storage.sync.get.mockResolvedValueOnce({ + copyToClipboard: false, + saveToPc: true + }); + + const popupScript = ` + document.addEventListener('DOMContentLoaded', async () => { + const clipboardToggle = document.getElementById('clipboard-toggle'); + const saveToPcToggle = document.getElementById('save-to-pc-toggle'); + const saved = await chrome.storage.sync.get(['backgroundPreference', 'copyToClipboard', 'saveToPc']); + + clipboardToggle.checked = saved.copyToClipboard !== undefined ? saved.copyToClipboard : true; + saveToPcToggle.checked = saved.saveToPc !== undefined ? saved.saveToPc : true; + }); + `; + + await new Promise(resolve => { + global.document.addEventListener = jest.fn((event, callback) => { + if (event === 'DOMContentLoaded') { + callback(); + resolve(); + } + }); + eval(popupScript); + }); + + expect(mockClipboardToggle.checked).toBe(false); + expect(mockSaveToPcToggle.checked).toBe(true); + }); + + test('should persist clipboard-only preferences across sessions', async () => { + // Load clipboard-only mode + global.chrome.storage.sync.get.mockResolvedValueOnce({ + copyToClipboard: true, + saveToPc: false + }); + + const popupScript = ` + document.addEventListener('DOMContentLoaded', async () => { + const clipboardToggle = document.getElementById('clipboard-toggle'); + const saveToPcToggle = document.getElementById('save-to-pc-toggle'); + const saved = await chrome.storage.sync.get(['backgroundPreference', 'copyToClipboard', 'saveToPc']); + + clipboardToggle.checked = saved.copyToClipboard !== undefined ? saved.copyToClipboard : true; + saveToPcToggle.checked = saved.saveToPc !== undefined ? saved.saveToPc : true; + }); + `; + + await new Promise(resolve => { + global.document.addEventListener = jest.fn((event, callback) => { + if (event === 'DOMContentLoaded') { + callback(); + resolve(); + } + }); + eval(popupScript); + }); + + expect(mockClipboardToggle.checked).toBe(true); + expect(mockSaveToPcToggle.checked).toBe(false); + }); + + test('should persist both disabled preferences across sessions', async () => { + // Load both disabled state + global.chrome.storage.sync.get.mockResolvedValueOnce({ + copyToClipboard: false, + saveToPc: false + }); + + const popupScript = ` + document.addEventListener('DOMContentLoaded', async () => { + const clipboardToggle = document.getElementById('clipboard-toggle'); + const saveToPcToggle = document.getElementById('save-to-pc-toggle'); + const saved = await chrome.storage.sync.get(['backgroundPreference', 'copyToClipboard', 'saveToPc']); + + clipboardToggle.checked = saved.copyToClipboard !== undefined ? saved.copyToClipboard : true; + saveToPcToggle.checked = saved.saveToPc !== undefined ? saved.saveToPc : true; + }); + `; + + await new Promise(resolve => { + global.document.addEventListener = jest.fn((event, callback) => { + if (event === 'DOMContentLoaded') { + callback(); + resolve(); + } + }); + eval(popupScript); + }); + + expect(mockClipboardToggle.checked).toBe(false); + expect(mockSaveToPcToggle.checked).toBe(false); + + // Validation should fail with both disabled + const isValid = mockClipboardToggle.checked || mockSaveToPcToggle.checked; + expect(isValid).toBe(false); + }); + + test('should maintain existing behavior when upgrading from version without saveToPc', async () => { + // Mock existing installation with only clipboard preference + global.chrome.storage.sync.get.mockResolvedValueOnce({ + copyToClipboard: true + // saveToPc is undefined (not set in previous version) + }); + + const popupScript = ` + document.addEventListener('DOMContentLoaded', async () => { + const clipboardToggle = document.getElementById('clipboard-toggle'); + const saveToPcToggle = document.getElementById('save-to-pc-toggle'); + const saved = await chrome.storage.sync.get(['backgroundPreference', 'copyToClipboard', 'saveToPc']); + + clipboardToggle.checked = saved.copyToClipboard !== undefined ? saved.copyToClipboard : true; + saveToPcToggle.checked = saved.saveToPc !== undefined ? saved.saveToPc : true; + }); + `; + + await new Promise(resolve => { + global.document.addEventListener = jest.fn((event, callback) => { + if (event === 'DOMContentLoaded') { + callback(); + resolve(); + } + }); + eval(popupScript); + }); + + expect(mockClipboardToggle.checked).toBe(true); // Existing preference + expect(mockSaveToPcToggle.checked).toBe(true); // Default for backward compatibility + }); +}); \ No newline at end of file diff --git a/tests/setup.js b/tests/setup.js new file mode 100644 index 0000000..e6a0b6e --- /dev/null +++ b/tests/setup.js @@ -0,0 +1,297 @@ +/** + * Jest setup file for clipboard functionality tests + * Provides common mocks and utilities for all test files + */ + +// Keep global and window navigator in sync regardless of direct reassignment +(() => { + try { + let navigatorStore = (typeof global !== 'undefined' && global.navigator) || (typeof window !== 'undefined' && window.navigator) || {}; + if (typeof global !== 'undefined') { + Object.defineProperty(global, 'navigator', { + configurable: true, + get: () => navigatorStore, + set: (val) => { + navigatorStore = val || {}; + if (typeof window !== 'undefined') { + try { window.navigator = navigatorStore; } catch (_) {} + } + } + }); + } + if (typeof window !== 'undefined') { + Object.defineProperty(window, 'navigator', { + configurable: true, + get: () => navigatorStore, + set: (val) => { navigatorStore = val || {}; } + }); + } + } catch (_) {} +})(); + +// 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 = () => { + const existingChrome = global.chrome || {}; + const existingStorage = (existingChrome.storage && existingChrome.storage.sync) || {}; + const existingRuntime = existingChrome.runtime || {}; + const existingTabs = existingChrome.tabs || {}; + + global.chrome = { + storage: { + sync: { + get: existingStorage.get || jest.fn().mockResolvedValue({}), + set: existingStorage.set || jest.fn().mockResolvedValue() + } + }, + runtime: { + sendMessage: existingRuntime.sendMessage || jest.fn().mockResolvedValue(), + onMessage: { + addListener: (existingRuntime.onMessage && existingRuntime.onMessage.addListener) || jest.fn() + } + }, + tabs: { + query: existingTabs.query || jest.fn().mockResolvedValue([{ id: 123, url: 'https://example.com' }]), + sendMessage: existingTabs.sendMessage || jest.fn().mockResolvedValue() + } + }; +}; + +// Mock DOM APIs consistently +global.mockDOMAPIs = () => { + // Document + const existingDocument = global.document || {}; + const existingCreateElement = existingDocument.createElement; + global.document = existingDocument; + if (!global.document.createElement) { + 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: '' + })); + } + if (!global.document.head) global.document.head = { appendChild: jest.fn() }; + if (!global.document.body) global.document.body = { appendChild: jest.fn() }; + if (!global.document.addEventListener) global.document.addEventListener = jest.fn(); + if (!global.document.removeEventListener) global.document.removeEventListener = jest.fn(); + if (!global.document.querySelectorAll) global.document.querySelectorAll = jest.fn(() => []); + if (!global.document.getElementById) global.document.getElementById = jest.fn(() => null); + + // Window + const existingWindow = global.window || {}; + global.window = existingWindow; + // Ensure a shared navigator reference exists early + if (!global.navigator) global.navigator = (existingWindow && existingWindow.navigator) || {}; + if (!global.window.navigator) global.window.navigator = global.navigator; + if (!global.window.getComputedStyle) global.window.getComputedStyle = jest.fn(() => ({})); + if (typeof global.window.devicePixelRatio === 'undefined') global.window.devicePixelRatio = 2; + if (typeof global.window.isSecureContext === 'undefined') global.window.isSecureContext = true; + if (!global.window.CSS) global.window.CSS = { escape: jest.fn(str => str) }; + if (!global.window.setTimeout) { + global.window.setTimeout = jest.fn((fn, delay) => { + if (delay === 0) fn(); + return 1; + }); + } + if (!global.window.clearTimeout) global.window.clearTimeout = jest.fn(); + if (!global.window.close) global.window.close = jest.fn(); +}; + +// Mock clipboard APIs with different scenarios +global.mockClipboardAPI = (scenario = 'supported') => { + switch (scenario) { + case 'supported': { + const existingNavigator = global.navigator || {}; + const existingClipboard = existingNavigator.clipboard || {}; + if (!existingClipboard.write || !jest.isMockFunction?.(existingClipboard.write)) { + existingClipboard.write = jest.fn().mockResolvedValue(); + } + const mergedNavigator = { ...existingNavigator, clipboard: existingClipboard }; + global.navigator = mergedNavigator; + if (!global.window) global.window = {}; + global.window.navigator = mergedNavigator; + if (!global.window) global.window = {}; + if (!global.window.ClipboardItem) global.window.ClipboardItem = jest.fn(); + if (typeof global.window.isSecureContext === 'undefined') global.window.isSecureContext = true; + break; + } + case 'no-clipboard': + global.navigator = {}; + if (!global.window) global.window = {}; + global.window.navigator = global.navigator; + break; + case 'no-clipboarditem': { + const existingNavigator = global.navigator || {}; + const clipboard = existingNavigator.clipboard || { write: jest.fn() }; + const mergedNavigator = { ...existingNavigator, clipboard }; + global.navigator = mergedNavigator; + if (!global.window) global.window = {}; + global.window.navigator = mergedNavigator; + if (!global.window) global.window = {}; + global.window.ClipboardItem = undefined; + break; + } + case 'insecure-context': { + const existingNavigator = global.navigator || {}; + const clipboard = existingNavigator.clipboard || { write: jest.fn() }; + const mergedNavigator = { ...existingNavigator, clipboard }; + global.navigator = mergedNavigator; + if (!global.window) global.window = {}; + global.window.navigator = mergedNavigator; + if (!global.window) global.window = {}; + global.window.ClipboardItem = global.window.ClipboardItem || jest.fn(); + global.window.isSecureContext = false; + break; + } + case 'permission-denied': { + const existingNavigator = global.navigator || {}; + const clipboard = { write: jest.fn().mockRejectedValue(new DOMException('Permission denied', 'NotAllowedError')) }; + const mergedNavigator = { ...existingNavigator, clipboard }; + global.navigator = mergedNavigator; + if (!global.window) global.window = {}; + global.window.navigator = mergedNavigator; + if (!global.window) global.window = {}; + global.window.ClipboardItem = global.window.ClipboardItem || jest.fn(); + global.window.isSecureContext = true; + break; + } + case 'security-error': { + const existingNavigator = global.navigator || {}; + const clipboard = { write: jest.fn().mockRejectedValue(new DOMException('Security error', 'SecurityError')) }; + const mergedNavigator = { ...existingNavigator, clipboard }; + global.navigator = mergedNavigator; + if (!global.window) global.window = {}; + global.window.navigator = mergedNavigator; + if (!global.window) global.window = {}; + global.window.ClipboardItem = 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 diff --git a/tests/validation-logic.test.js b/tests/validation-logic.test.js new file mode 100644 index 0000000..141c9eb --- /dev/null +++ b/tests/validation-logic.test.js @@ -0,0 +1,209 @@ +/** + * Tests for output method validation logic + */ + +// Mock DOM elements and Chrome APIs +const mockChrome = { + storage: { + sync: { + get: jest.fn(), + set: jest.fn() + } + }, + tabs: { + query: jest.fn(), + sendMessage: jest.fn() + }, + runtime: { + onMessage: { + addListener: jest.fn() + } + } +}; + +global.chrome = mockChrome; + +describe('Output Method Validation', () => { + let mockDocument; + let clipboardToggle; + let saveToPcToggle; + let startBtn; + let validationWarning; + let validateOutputMethods; + + beforeEach(() => { + // Reset mocks + jest.clearAllMocks(); + + // Mock DOM elements + clipboardToggle = { + checked: true, + addEventListener: jest.fn() + }; + + saveToPcToggle = { + checked: true, + addEventListener: jest.fn() + }; + + startBtn = { + disabled: false, + setAttribute: jest.fn(), + addEventListener: jest.fn() + }; + + validationWarning = { + style: { display: 'none' } + }; + + // Mock document.getElementById + const mockGetElementById = jest.fn((id) => { + switch (id) { + case 'clipboard-toggle': return clipboardToggle; + case 'save-to-pc-toggle': return saveToPcToggle; + case 'start-selector': return startBtn; + case 'validation-warning': return validationWarning; + default: return null; + } + }); + + mockDocument = { + getElementById: mockGetElementById, + addEventListener: jest.fn() + }; + + global.document = mockDocument; + + // Define validation function (extracted from popup.js logic) + validateOutputMethods = function() { + const hasValidOutput = clipboardToggle.checked || saveToPcToggle.checked; + + if (hasValidOutput) { + validationWarning.style.display = 'none'; + startBtn.disabled = false; + startBtn.setAttribute('aria-describedby', ''); + } else { + validationWarning.style.display = 'block'; + startBtn.disabled = true; + startBtn.setAttribute('aria-describedby', 'validation-warning'); + } + + return hasValidOutput; + }; + }); + + describe('Validation Logic', () => { + test('should return true when both save to PC and clipboard are enabled', () => { + clipboardToggle.checked = true; + saveToPcToggle.checked = true; + + const result = validateOutputMethods(); + + expect(result).toBe(true); + expect(validationWarning.style.display).toBe('none'); + expect(startBtn.disabled).toBe(false); + expect(startBtn.setAttribute).toHaveBeenCalledWith('aria-describedby', ''); + }); + + test('should return true when only save to PC is enabled', () => { + clipboardToggle.checked = false; + saveToPcToggle.checked = true; + + const result = validateOutputMethods(); + + expect(result).toBe(true); + expect(validationWarning.style.display).toBe('none'); + expect(startBtn.disabled).toBe(false); + expect(startBtn.setAttribute).toHaveBeenCalledWith('aria-describedby', ''); + }); + + test('should return true when only clipboard is enabled', () => { + clipboardToggle.checked = true; + saveToPcToggle.checked = false; + + const result = validateOutputMethods(); + + expect(result).toBe(true); + expect(validationWarning.style.display).toBe('none'); + expect(startBtn.disabled).toBe(false); + expect(startBtn.setAttribute).toHaveBeenCalledWith('aria-describedby', ''); + }); + + test('should return false when both save to PC and clipboard are disabled', () => { + clipboardToggle.checked = false; + saveToPcToggle.checked = false; + + const result = validateOutputMethods(); + + expect(result).toBe(false); + expect(validationWarning.style.display).toBe('block'); + expect(startBtn.disabled).toBe(true); + expect(startBtn.setAttribute).toHaveBeenCalledWith('aria-describedby', 'validation-warning'); + }); + }); + + describe('UI State Management', () => { + test('should hide warning and enable button when validation passes', () => { + clipboardToggle.checked = true; + saveToPcToggle.checked = false; + + validateOutputMethods(); + + expect(validationWarning.style.display).toBe('none'); + expect(startBtn.disabled).toBe(false); + expect(startBtn.setAttribute).toHaveBeenCalledWith('aria-describedby', ''); + }); + + test('should show warning and disable button when validation fails', () => { + clipboardToggle.checked = false; + saveToPcToggle.checked = false; + + validateOutputMethods(); + + expect(validationWarning.style.display).toBe('block'); + expect(startBtn.disabled).toBe(true); + expect(startBtn.setAttribute).toHaveBeenCalledWith('aria-describedby', 'validation-warning'); + }); + + test('should update accessibility attributes correctly', () => { + // Test valid state + clipboardToggle.checked = true; + saveToPcToggle.checked = true; + validateOutputMethods(); + expect(startBtn.setAttribute).toHaveBeenCalledWith('aria-describedby', ''); + + // Reset mock + startBtn.setAttribute.mockClear(); + + // Test invalid state + clipboardToggle.checked = false; + saveToPcToggle.checked = false; + validateOutputMethods(); + expect(startBtn.setAttribute).toHaveBeenCalledWith('aria-describedby', 'validation-warning'); + }); + }); + + describe('Edge Cases', () => { + test('should handle undefined checkbox states gracefully', () => { + clipboardToggle.checked = undefined; + saveToPcToggle.checked = true; + + const result = validateOutputMethods(); + + // undefined should be falsy, so only saveToPc (true) should make it valid + expect(result).toBe(true); + }); + + test('should handle null checkbox states gracefully', () => { + clipboardToggle.checked = null; + saveToPcToggle.checked = null; + + const result = validateOutputMethods(); + + // Both null should be falsy, so validation should fail + expect(result).toBeFalsy(); // Use toBeFalsy to handle null/false/undefined + expect(validationWarning.style.display).toBe('block'); + expect(startBtn.disabled).toBe(true); + }); + }); +}); \ No newline at end of file From f4b9af1572650363e4e6d98488bf0c6874bf6621 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Sat, 20 Dec 2025 18:26:20 -0700 Subject: [PATCH 2/2] Better image support --- .gitignore | 3 +- content.js | 178 +-- html-to-image.js | 1389 +++++++++++++++++ html-to-image.js.map | 1 + manifest.json | 2 +- tests/integration-scenarios.test.js | 84 +- .../save-to-pc-clipboard-combinations.test.js | 75 +- tests/setup.js | 22 +- 8 files changed, 1557 insertions(+), 197 deletions(-) create mode 100644 html-to-image.js create mode 100644 html-to-image.js.map diff --git a/.gitignore b/.gitignore index c06c56b..038618d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules/ .kiro/ -.vscode/ \ No newline at end of file +html-to-image/ +.vscode/ diff --git a/content.js b/content.js index 98b61c8..584e32b 100644 --- a/content.js +++ b/content.js @@ -369,7 +369,7 @@ class ScreenshotSelector { try { // Determine loading message based on enabled operations let loadingMessage = 'Capturing screenshot...'; - + if (this.saveToPc && this.copyToClipboard) { // Both operations enabled const availability = this.checkClipboardAPIAvailability(); @@ -411,90 +411,42 @@ class ScreenshotSelector { element.classList.remove('screenshot-highlight'); this.removeScrollableIndicators(); - // Store original styles - const originalStyles = { - overflow: element.style.overflow, - overflowY: element.style.overflowY, - overflowX: element.style.overflowX, - height: element.style.height, - maxHeight: element.style.maxHeight, - position: element.style.position, - zIndex: element.style.zIndex, - }; - - // Temporarily modify element for full capture - element.style.overflow = 'visible'; - element.style.overflowY = 'visible'; - element.style.overflowX = 'visible'; - element.style.height = `${element.scrollHeight}px`; - element.style.maxHeight = 'none'; - // Wait for fonts and external resources to load await this.waitForFontsAndResources(element); - // Enhanced html2canvas configuration - const canvas = await html2canvas(element, { - useCORS: true, - allowTaint: false, - backgroundColor: this.background === 'transparent' ? null : - this.background === 'white' ? '#ffffff' : '#000000', + if (!window.htmlToImage || typeof window.htmlToImage.toCanvas !== 'function') { + throw new Error('Screenshot renderer not available: htmlToImage.toCanvas() missing'); + } - // Improved rendering options - scale: window.devicePixelRatio || 1, // Use device pixel ratio for crisp images - logging: false, // Disable logging for cleaner console + const backgroundColor = + this.background === 'transparent' + ? undefined + : this.background === 'white' + ? '#ffffff' + : '#000000'; - // Better handling of external resources - imageTimeout: 15000, // Wait longer for images to load - - // Font handling and style preservation - onclone: (clonedDoc, clonedElementParam) => { - // Ensure all stylesheets are loaded in cloned document - const originalStyleSheets = Array.from(document.styleSheets); - const clonedHead = clonedDoc.head; - - // Copy all stylesheets to cloned document - originalStyleSheets.forEach(styleSheet => { - try { - if (styleSheet.href) { - // External stylesheet - const link = clonedDoc.createElement('link'); - link.rel = 'stylesheet'; - link.href = styleSheet.href; - link.type = 'text/css'; - clonedHead.appendChild(link); - } else if (styleSheet.ownerNode && styleSheet.ownerNode.tagName === 'STYLE') { - // Inline stylesheet - const style = clonedDoc.createElement('style'); - style.type = 'text/css'; - try { - const cssText = Array.from(styleSheet.cssRules).map(rule => rule.cssText).join('\n'); - style.textContent = cssText; - } catch (e) { - // Fallback to original text content - style.textContent = styleSheet.ownerNode.textContent; - } - clonedHead.appendChild(style); - } - } catch (e) { - // Skip stylesheets that can't be accessed (CORS issues) - console.log('Skipped stylesheet due to CORS:', e); - } - }); - - // Apply computed styles to preserve appearance - const clonedElement = clonedElementParam; - const originalElement = element; // from outer scope - if (clonedElement && originalElement) { - this.preserveComputedStyles(clonedElement, originalElement); - } - - return clonedDoc; - } + // Use html-to-image to render the element to a canvas. + // Important: avoid mutating the live DOM during capture (it can cause reflow/layout drift). + const canvas = await window.htmlToImage.toCanvas(element, { + backgroundColor, + pixelRatio: window.devicePixelRatio || 1, + // Brightspace/D2L and many sites have CORS-protected stylesheets/fonts which cause noisy, + // expected DOMExceptions during CSS rule inspection. Keep logs clean by default. + logLevel: 'silent', + // If the page heavily caches fonts/images, enabling this can help but may slow capture + cacheBust: false, + // Capture full scrollable area without reflowing the live element + width: element.scrollWidth, + height: element.scrollHeight, + style: { + overflow: 'visible', + overflowX: 'visible', + overflowY: 'visible', + height: `${element.scrollHeight}px`, + maxHeight: 'none', + }, }); - // Restore original styles - Object.assign(element.style, originalStyles); - // Conditionally download the image based on saveToPc preference if (this.saveToPc) { const link = document.createElement('a'); @@ -515,15 +467,15 @@ class ScreenshotSelector { // Ensure clipboard errors don't break the workflow - this is a safety net // The copyCanvasToClipboard method should handle all errors internally console.error('Unexpected clipboard error caught in captureElement:', error); - clipboardResult = { - success: false, + clipboardResult = { + success: false, error: 'Unexpected clipboard error occurred', errorType: 'unexpected' }; } } else { - clipboardResult = { - success: false, + clipboardResult = { + success: false, error: availability.reason, errorType: 'api_unavailable' }; @@ -534,7 +486,7 @@ class ScreenshotSelector { let successMessage = 'Screenshot captured!'; let messageColor = 'rgba(39, 174, 96, 0.95)'; // Green for success let messageIcon = '✅'; - + // Determine success message based on enabled operations if (this.saveToPc && this.copyToClipboard) { // Both operations enabled @@ -582,11 +534,11 @@ class ScreenshotSelector { } catch (error) { console.error('Screenshot failed:', error); - + // Provide specific error messages for screenshot failures based on enabled operations let errorMessage = 'Screenshot capture failed. Please try again.'; let operationContext = ''; - + // Add context about what operations were attempted if (this.saveToPc && this.copyToClipboard) { operationContext = ' Neither file download nor clipboard copy could be completed.'; @@ -595,9 +547,9 @@ class ScreenshotSelector { } else if (!this.saveToPc && this.copyToClipboard) { operationContext = ' Clipboard copy could not be completed.'; } - + if (error.message) { - if (error.message.includes('html2canvas')) { + if (error.message.includes('htmlToImage') || error.message.includes('html-to-image')) { errorMessage = `Screenshot rendering failed. Try selecting a different element or refresh the page.${operationContext}`; } else if (error.message.includes('timeout')) { errorMessage = `Screenshot capture timed out. Try selecting a smaller area or simpler element.${operationContext}`; @@ -613,7 +565,7 @@ class ScreenshotSelector { } else { errorMessage = `Screenshot capture failed. Please try again.${operationContext}`; } - + this.overlay.innerHTML = `
@@ -622,10 +574,10 @@ class ScreenshotSelector {
`; - + // Notify popup about the failure chrome.runtime.sendMessage({ action: 'selectorStopped', reason: 'screenshot-failed' }); - + setTimeout(() => this.cleanup(), 3000); } } @@ -678,7 +630,7 @@ class ScreenshotSelector { */ getClipboardSupportFeedback() { const availability = this.checkClipboardAPIAvailability(); - + if (availability.available) { return { supported: true, @@ -688,7 +640,7 @@ class ScreenshotSelector { // Provide user-friendly messages for different scenarios 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')) { @@ -723,39 +675,39 @@ class ScreenshotSelector { 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 is not available in this browser'; - + case 'security_error': return 'clipboard copy was 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'; - + case 'network_error': return 'clipboard copy failed due to network error. Please try again'; - + case 'canvas_conversion': return 'clipboard copy failed: unable to prepare image. Try capturing a different element'; - + case 'clipboard_item_creation': return 'clipboard copy is not supported: your browser doesn\'t support image clipboard operations'; - + case 'invalid_state': return 'clipboard copy failed: browser clipboard is busy. Please try again'; - + case 'data_error': return 'clipboard copy failed: invalid image data. Please try capturing again'; - + case 'unexpected': return 'clipboard copy failed due to unexpected error'; - + default: // For unknown error types, try to extract meaningful info from the error message if (errorMessage.includes('permission') || errorMessage.includes('denied')) { @@ -782,8 +734,8 @@ class ScreenshotSelector { // Check clipboard API availability first const availability = this.checkClipboardAPIAvailability(); if (!availability.available) { - return { - success: false, + return { + success: false, error: availability.reason, errorType: 'api_unavailable' }; @@ -800,7 +752,7 @@ class ScreenshotSelector { } }, 'image/png'); }), - new Promise((_, reject) => + new Promise((_, reject) => setTimeout(() => reject(new Error('Canvas to blob conversion timed out')), 10000) ) ]); @@ -832,7 +784,7 @@ class ScreenshotSelector { // Write to clipboard with timeout await Promise.race([ navigator.clipboard.write([clipboardItem]), - new Promise((_, reject) => + new Promise((_, reject) => setTimeout(() => reject(new Error('Clipboard write operation timed out')), 15000) ) ]); @@ -841,16 +793,16 @@ class ScreenshotSelector { } catch (error) { console.error('Clipboard operation failed:', error); - + // Comprehensive error handling with specific error types and user-friendly messages let errorMessage = error.message || 'Unknown clipboard error'; let errorType = 'unknown'; - + // Permission-related errors if (error.name === 'NotAllowedError') { errorMessage = 'Clipboard access denied. Please allow clipboard permissions in your browser settings.'; errorType = 'permission_denied'; - } + } // API support errors else if (error.name === 'NotSupportedError') { errorMessage = 'Clipboard API not supported in this browser context.'; @@ -897,8 +849,8 @@ class ScreenshotSelector { errorType = 'unknown'; } - return { - success: false, + return { + success: false, error: errorMessage, errorType: errorType }; @@ -963,7 +915,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { case 'checkClipboardSupport': const availability = screenshotSelector.checkClipboardAPIAvailability(); const feedback = screenshotSelector.getClipboardSupportFeedback(); - sendResponse({ + sendResponse({ availability: availability, feedback: feedback }); diff --git a/html-to-image.js b/html-to-image.js new file mode 100644 index 0000000..27f47fb --- /dev/null +++ b/html-to-image.js @@ -0,0 +1,1389 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define(['exports'], factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.htmlToImage = {})); +})(this, (function (exports) { 'use strict'; + + /****************************************************************************** + Copyright (c) Microsoft Corporation. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + PERFORMANCE OF THIS SOFTWARE. + ***************************************************************************** */ + /* global Reflect, Promise, SuppressedError, Symbol, Iterator */ + + + function __awaiter(thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); + } + + function __generator(thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } + } + + typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { + var e = new Error(message); + return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; + }; + + function resolveUrl(url, baseUrl) { + // url is absolute already + if (url.match(/^[a-z]+:\/\//i)) { + return url; + } + // url is absolute already, without protocol + if (url.match(/^\/\//)) { + return window.location.protocol + url; + } + // dataURI, mailto:, tel:, etc. + if (url.match(/^[a-z]+:/i)) { + return url; + } + var doc = document.implementation.createHTMLDocument(); + var base = doc.createElement('base'); + var a = doc.createElement('a'); + doc.head.appendChild(base); + doc.body.appendChild(a); + if (baseUrl) { + base.href = baseUrl; + } + a.href = url; + return a.href; + } + var uuid = (function () { + // generate uuid for className of pseudo elements. + // We should not use GUIDs, otherwise pseudo elements sometimes cannot be captured. + var counter = 0; + // ref: http://stackoverflow.com/a/6248722/2519373 + var random = function () { + // eslint-disable-next-line no-bitwise + return "0000".concat(((Math.random() * Math.pow(36, 4)) << 0).toString(36)).slice(-4); + }; + return function () { + counter += 1; + return "u".concat(random()).concat(counter); + }; + })(); + function toArray(arrayLike) { + var arr = []; + for (var i = 0, l = arrayLike.length; i < l; i++) { + arr.push(arrayLike[i]); + } + return arr; + } + var styleProps = null; + function getStyleProperties(options) { + if (options === void 0) { options = {}; } + if (styleProps) { + return styleProps; + } + if (options.includeStyleProperties) { + styleProps = options.includeStyleProperties; + return styleProps; + } + styleProps = toArray(window.getComputedStyle(document.documentElement)); + return styleProps; + } + function px(node, styleProperty) { + var win = node.ownerDocument.defaultView || window; + var val = win.getComputedStyle(node).getPropertyValue(styleProperty); + return val ? parseFloat(val.replace('px', '')) : 0; + } + function getNodeWidth(node) { + var leftBorder = px(node, 'border-left-width'); + var rightBorder = px(node, 'border-right-width'); + return node.clientWidth + leftBorder + rightBorder; + } + function getNodeHeight(node) { + var topBorder = px(node, 'border-top-width'); + var bottomBorder = px(node, 'border-bottom-width'); + return node.clientHeight + topBorder + bottomBorder; + } + function getImageSize(targetNode, options) { + if (options === void 0) { options = {}; } + var width = options.width || getNodeWidth(targetNode); + var height = options.height || getNodeHeight(targetNode); + return { width: width, height: height }; + } + function getPixelRatio() { + var ratio; + var FINAL_PROCESS; + try { + FINAL_PROCESS = process; + } + catch (e) { + // pass + } + var val = FINAL_PROCESS && FINAL_PROCESS.env + ? FINAL_PROCESS.env.devicePixelRatio + : null; + if (val) { + ratio = parseInt(val, 10); + if (Number.isNaN(ratio)) { + ratio = 1; + } + } + return ratio || window.devicePixelRatio || 1; + } + // @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas#maximum_canvas_size + var canvasDimensionLimit = 16384; + function checkCanvasDimensions(canvas) { + if (canvas.width > canvasDimensionLimit || + canvas.height > canvasDimensionLimit) { + if (canvas.width > canvasDimensionLimit && + canvas.height > canvasDimensionLimit) { + if (canvas.width > canvas.height) { + canvas.height *= canvasDimensionLimit / canvas.width; + canvas.width = canvasDimensionLimit; + } + else { + canvas.width *= canvasDimensionLimit / canvas.height; + canvas.height = canvasDimensionLimit; + } + } + else if (canvas.width > canvasDimensionLimit) { + canvas.height *= canvasDimensionLimit / canvas.width; + canvas.width = canvasDimensionLimit; + } + else { + canvas.width *= canvasDimensionLimit / canvas.height; + canvas.height = canvasDimensionLimit; + } + } + } + function canvasToBlob(canvas, options) { + if (options === void 0) { options = {}; } + if (canvas.toBlob) { + return new Promise(function (resolve) { + canvas.toBlob(resolve, options.type ? options.type : 'image/png', options.quality ? options.quality : 1); + }); + } + return new Promise(function (resolve) { + var binaryString = window.atob(canvas + .toDataURL(options.type ? options.type : undefined, options.quality ? options.quality : undefined) + .split(',')[1]); + var len = binaryString.length; + var binaryArray = new Uint8Array(len); + for (var i = 0; i < len; i += 1) { + binaryArray[i] = binaryString.charCodeAt(i); + } + resolve(new Blob([binaryArray], { + type: options.type ? options.type : 'image/png', + })); + }); + } + function createImage(url) { + return new Promise(function (resolve, reject) { + var img = new Image(); + img.onload = function () { + img.decode().then(function () { + requestAnimationFrame(function () { return resolve(img); }); + }); + }; + img.onerror = reject; + img.crossOrigin = 'anonymous'; + img.decoding = 'async'; + img.src = url; + }); + } + function svgToDataURL(svg) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + return [2 /*return*/, Promise.resolve() + .then(function () { return new XMLSerializer().serializeToString(svg); }) + .then(encodeURIComponent) + .then(function (html) { return "data:image/svg+xml;charset=utf-8,".concat(html); })]; + }); + }); + } + function nodeToDataURL(node, width, height) { + return __awaiter(this, void 0, void 0, function () { + var xmlns, svg, foreignObject; + return __generator(this, function (_a) { + xmlns = 'http://www.w3.org/2000/svg'; + svg = document.createElementNS(xmlns, 'svg'); + foreignObject = document.createElementNS(xmlns, 'foreignObject'); + svg.setAttribute('width', "".concat(width)); + svg.setAttribute('height', "".concat(height)); + svg.setAttribute('viewBox', "0 0 ".concat(width, " ").concat(height)); + foreignObject.setAttribute('width', '100%'); + foreignObject.setAttribute('height', '100%'); + foreignObject.setAttribute('x', '0'); + foreignObject.setAttribute('y', '0'); + foreignObject.setAttribute('externalResourcesRequired', 'true'); + svg.appendChild(foreignObject); + foreignObject.appendChild(node); + return [2 /*return*/, svgToDataURL(svg)]; + }); + }); + } + var isInstanceOfElement = function (node, instance) { + if (node instanceof instance) + return true; + var nodePrototype = Object.getPrototypeOf(node); + if (nodePrototype === null) + return false; + return (nodePrototype.constructor.name === instance.name || + isInstanceOfElement(nodePrototype, instance)); + }; + + function formatCSSText(style) { + var content = style.getPropertyValue('content'); + return "".concat(style.cssText, " content: '").concat(content.replace(/'|"/g, ''), "';"); + } + function formatCSSProperties(style, options) { + return getStyleProperties(options) + .map(function (name) { + var value = style.getPropertyValue(name); + var priority = style.getPropertyPriority(name); + return "".concat(name, ": ").concat(value).concat(priority ? ' !important' : '', ";"); + }) + .join(' '); + } + function getPseudoElementStyle(className, pseudo, style, options) { + var selector = ".".concat(className, ":").concat(pseudo); + var cssText = style.cssText + ? formatCSSText(style) + : formatCSSProperties(style, options); + return document.createTextNode("".concat(selector, "{").concat(cssText, "}")); + } + function clonePseudoElement(nativeNode, clonedNode, pseudo, options) { + var style = window.getComputedStyle(nativeNode, pseudo); + var content = style.getPropertyValue('content'); + if (content === '' || content === 'none') { + return; + } + var className = uuid(); + try { + clonedNode.className = "".concat(clonedNode.className, " ").concat(className); + } + catch (err) { + return; + } + var styleElement = document.createElement('style'); + styleElement.appendChild(getPseudoElementStyle(className, pseudo, style, options)); + clonedNode.appendChild(styleElement); + } + function clonePseudoElements(nativeNode, clonedNode, options) { + clonePseudoElement(nativeNode, clonedNode, ':before', options); + clonePseudoElement(nativeNode, clonedNode, ':after', options); + } + + var WOFF = 'application/font-woff'; + var JPEG = 'image/jpeg'; + var mimes = { + woff: WOFF, + woff2: WOFF, + ttf: 'application/font-truetype', + eot: 'application/vnd.ms-fontobject', + png: 'image/png', + jpg: JPEG, + jpeg: JPEG, + gif: 'image/gif', + tiff: 'image/tiff', + svg: 'image/svg+xml', + webp: 'image/webp', + }; + function getExtension(url) { + var match = /\.([^./]*?)$/g.exec(url); + return match ? match[1] : ''; + } + function getMimeType(url) { + var extension = getExtension(url).toLowerCase(); + return mimes[extension] || ''; + } + + function shouldLog$1(options, level) { + var _a; + var logLevel = (_a = options === null || options === void 0 ? void 0 : options.logLevel) !== null && _a !== void 0 ? _a : 'error'; + if (logLevel === 'silent') + return false; + if (logLevel === 'debug') + return true; + return level === 'error'; + } + function logDebug$1(options) { + var args = []; + for (var _i = 1; _i < arguments.length; _i++) { + args[_i - 1] = arguments[_i]; + } + if (shouldLog$1(options, 'debug')) { + // eslint-disable-next-line no-console + console.debug.apply(console, args); + } + } + function getContentFromDataUrl(dataURL) { + return dataURL.split(/,/)[1]; + } + function isDataUrl(url) { + return url.search(/^(data:)/) !== -1; + } + function makeDataUrl(content, mimeType) { + return "data:".concat(mimeType, ";base64,").concat(content); + } + function fetchAsDataURL(url, init, process) { + return __awaiter(this, void 0, void 0, function () { + var res, blob; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, fetch(url, init)]; + case 1: + res = _a.sent(); + if (res.status === 404) { + throw new Error("Resource \"".concat(res.url, "\" not found")); + } + return [4 /*yield*/, res.blob()]; + case 2: + blob = _a.sent(); + return [2 /*return*/, new Promise(function (resolve, reject) { + var reader = new FileReader(); + reader.onerror = reject; + reader.onloadend = function () { + try { + resolve(process({ res: res, result: reader.result })); + } + catch (error) { + reject(error); + } + }; + reader.readAsDataURL(blob); + })]; + } + }); + }); + } + var cache = {}; + function getCacheKey(url, contentType, includeQueryParams) { + var key = url.replace(/\?.*/, ''); + if (includeQueryParams) { + key = url; + } + // font resource + if (/ttf|otf|eot|woff2?/i.test(key)) { + key = key.replace(/.*\//, ''); + } + return contentType ? "[".concat(contentType, "]").concat(key) : key; + } + function resourceToDataURL(resourceUrl, contentType, options) { + return __awaiter(this, void 0, void 0, function () { + var cacheKey, dataURL, content, error_1, msg; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + cacheKey = getCacheKey(resourceUrl, contentType, options.includeQueryParams); + if (cache[cacheKey] != null) { + return [2 /*return*/, cache[cacheKey]]; + } + // ref: https://developer.mozilla.org/en/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest#Bypassing_the_cache + if (options.cacheBust) { + // eslint-disable-next-line no-param-reassign + resourceUrl += (/\?/.test(resourceUrl) ? '&' : '?') + new Date().getTime(); + } + _a.label = 1; + case 1: + _a.trys.push([1, 3, , 4]); + return [4 /*yield*/, fetchAsDataURL(resourceUrl, options.fetchRequestInit, function (_a) { + var res = _a.res, result = _a.result; + if (!contentType) { + // eslint-disable-next-line no-param-reassign + contentType = res.headers.get('Content-Type') || ''; + } + return getContentFromDataUrl(result); + })]; + case 2: + content = _a.sent(); + dataURL = makeDataUrl(content, contentType); + return [3 /*break*/, 4]; + case 3: + error_1 = _a.sent(); + dataURL = options.imagePlaceholder || ''; + msg = "Failed to fetch resource: ".concat(resourceUrl); + if (error_1) { + msg = typeof error_1 === 'string' ? error_1 : error_1.message; + } + if (msg) { + // Expected on many sites due to CORS; don't spam logs unless debugging. + logDebug$1(options, msg); + } + return [3 /*break*/, 4]; + case 4: + cache[cacheKey] = dataURL; + return [2 /*return*/, dataURL]; + } + }); + }); + } + + function cloneCanvasElement(canvas) { + return __awaiter(this, void 0, void 0, function () { + var dataURL; + return __generator(this, function (_a) { + dataURL = canvas.toDataURL(); + if (dataURL === 'data:,') { + return [2 /*return*/, canvas.cloneNode(false)]; + } + return [2 /*return*/, createImage(dataURL)]; + }); + }); + } + function cloneVideoElement(video, options) { + return __awaiter(this, void 0, void 0, function () { + var canvas, ctx, dataURL_1, poster, contentType, dataURL; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (video.currentSrc) { + canvas = document.createElement('canvas'); + ctx = canvas.getContext('2d'); + canvas.width = video.clientWidth; + canvas.height = video.clientHeight; + ctx === null || ctx === void 0 ? void 0 : ctx.drawImage(video, 0, 0, canvas.width, canvas.height); + dataURL_1 = canvas.toDataURL(); + return [2 /*return*/, createImage(dataURL_1)]; + } + poster = video.poster; + contentType = getMimeType(poster); + return [4 /*yield*/, resourceToDataURL(poster, contentType, options)]; + case 1: + dataURL = _a.sent(); + return [2 /*return*/, createImage(dataURL)]; + } + }); + }); + } + function cloneIFrameElement(iframe, options) { + var _a; + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + _c.trys.push([0, 3, , 4]); + if (!((_a = iframe === null || iframe === void 0 ? void 0 : iframe.contentDocument) === null || _a === void 0 ? void 0 : _a.body)) return [3 /*break*/, 2]; + return [4 /*yield*/, cloneNode(iframe.contentDocument.body, options, true)]; + case 1: return [2 /*return*/, (_c.sent())]; + case 2: return [3 /*break*/, 4]; + case 3: + _c.sent(); + return [3 /*break*/, 4]; + case 4: return [2 /*return*/, iframe.cloneNode(false)]; + } + }); + }); + } + function cloneSingleNode(node, options) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + if (isInstanceOfElement(node, HTMLCanvasElement)) { + return [2 /*return*/, cloneCanvasElement(node)]; + } + if (isInstanceOfElement(node, HTMLVideoElement)) { + return [2 /*return*/, cloneVideoElement(node, options)]; + } + if (isInstanceOfElement(node, HTMLIFrameElement)) { + return [2 /*return*/, cloneIFrameElement(node, options)]; + } + return [2 /*return*/, node.cloneNode(isSVGElement(node))]; + }); + }); + } + var isSlotElement = function (node) { + return node.tagName != null && node.tagName.toUpperCase() === 'SLOT'; + }; + var isSVGElement = function (node) { + return node.tagName != null && node.tagName.toUpperCase() === 'SVG'; + }; + function cloneChildren(nativeNode, clonedNode, options) { + var _a, _b; + return __awaiter(this, void 0, void 0, function () { + var children; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + if (isSVGElement(clonedNode)) { + return [2 /*return*/, clonedNode]; + } + children = []; + if (isSlotElement(nativeNode) && nativeNode.assignedNodes) { + children = toArray(nativeNode.assignedNodes()); + } + else if (isInstanceOfElement(nativeNode, HTMLIFrameElement) && + ((_a = nativeNode.contentDocument) === null || _a === void 0 ? void 0 : _a.body)) { + children = toArray(nativeNode.contentDocument.body.childNodes); + } + else { + children = toArray(((_b = nativeNode.shadowRoot) !== null && _b !== void 0 ? _b : nativeNode).childNodes); + } + if (children.length === 0 || + isInstanceOfElement(nativeNode, HTMLVideoElement)) { + return [2 /*return*/, clonedNode]; + } + return [4 /*yield*/, children.reduce(function (deferred, child) { + return deferred + .then(function () { return cloneNode(child, options); }) + .then(function (clonedChild) { + if (clonedChild) { + clonedNode.appendChild(clonedChild); + } + }); + }, Promise.resolve())]; + case 1: + _c.sent(); + return [2 /*return*/, clonedNode]; + } + }); + }); + } + function cloneCSSStyle(nativeNode, clonedNode, options) { + var targetStyle = clonedNode.style; + if (!targetStyle) { + return; + } + var sourceStyle = window.getComputedStyle(nativeNode); + if (sourceStyle.cssText) { + targetStyle.cssText = sourceStyle.cssText; + targetStyle.transformOrigin = sourceStyle.transformOrigin; + } + else { + getStyleProperties(options).forEach(function (name) { + var value = sourceStyle.getPropertyValue(name); + if (isInstanceOfElement(nativeNode, HTMLIFrameElement) && + name === 'display' && + value === 'inline') { + value = 'block'; + } + if (name === 'd' && clonedNode.getAttribute('d')) { + value = "path(".concat(clonedNode.getAttribute('d'), ")"); + } + targetStyle.setProperty(name, value, sourceStyle.getPropertyPriority(name)); + }); + } + } + function cloneInputValue(nativeNode, clonedNode) { + if (isInstanceOfElement(nativeNode, HTMLTextAreaElement)) { + clonedNode.innerHTML = nativeNode.value; + } + if (isInstanceOfElement(nativeNode, HTMLInputElement)) { + clonedNode.setAttribute('value', nativeNode.value); + } + } + function cloneSelectValue(nativeNode, clonedNode) { + if (isInstanceOfElement(nativeNode, HTMLSelectElement)) { + var clonedSelect = clonedNode; + var selectedOption = Array.from(clonedSelect.children).find(function (child) { return nativeNode.value === child.getAttribute('value'); }); + if (selectedOption) { + selectedOption.setAttribute('selected', ''); + } + } + } + function decorate(nativeNode, clonedNode, options) { + if (isInstanceOfElement(clonedNode, Element)) { + cloneCSSStyle(nativeNode, clonedNode, options); + clonePseudoElements(nativeNode, clonedNode, options); + cloneInputValue(nativeNode, clonedNode); + cloneSelectValue(nativeNode, clonedNode); + } + return clonedNode; + } + function ensureSVGSymbols(clone, options) { + return __awaiter(this, void 0, void 0, function () { + var uses, processedDefs, i, use, id, exist, definition, _a, _b, nodes, ns, svg, defs, i; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + uses = clone.querySelectorAll ? clone.querySelectorAll('use') : []; + if (uses.length === 0) { + return [2 /*return*/, clone]; + } + processedDefs = {}; + i = 0; + _c.label = 1; + case 1: + if (!(i < uses.length)) return [3 /*break*/, 4]; + use = uses[i]; + id = use.getAttribute('xlink:href'); + if (!id) return [3 /*break*/, 3]; + exist = clone.querySelector(id); + definition = document.querySelector(id); + if (!(!exist && definition && !processedDefs[id])) return [3 /*break*/, 3]; + // eslint-disable-next-line no-await-in-loop + _a = processedDefs; + _b = id; + return [4 /*yield*/, cloneNode(definition, options, true)]; + case 2: + // eslint-disable-next-line no-await-in-loop + _a[_b] = (_c.sent()); + _c.label = 3; + case 3: + i++; + return [3 /*break*/, 1]; + case 4: + nodes = Object.values(processedDefs); + if (nodes.length) { + ns = 'http://www.w3.org/1999/xhtml'; + svg = document.createElementNS(ns, 'svg'); + svg.setAttribute('xmlns', ns); + svg.style.position = 'absolute'; + svg.style.width = '0'; + svg.style.height = '0'; + svg.style.overflow = 'hidden'; + svg.style.display = 'none'; + defs = document.createElementNS(ns, 'defs'); + svg.appendChild(defs); + for (i = 0; i < nodes.length; i++) { + defs.appendChild(nodes[i]); + } + clone.appendChild(svg); + } + return [2 /*return*/, clone]; + } + }); + }); + } + function cloneNode(node, options, isRoot) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + if (!isRoot && options.filter && !options.filter(node)) { + return [2 /*return*/, null]; + } + return [2 /*return*/, Promise.resolve(node) + .then(function (clonedNode) { return cloneSingleNode(clonedNode, options); }) + .then(function (clonedNode) { return cloneChildren(node, clonedNode, options); }) + .then(function (clonedNode) { return decorate(node, clonedNode, options); }) + .then(function (clonedNode) { return ensureSVGSymbols(clonedNode, options); })]; + }); + }); + } + + var URL_REGEX = /url\((['"]?)([^'"]+?)\1\)/g; + var URL_WITH_FORMAT_REGEX = /url\([^)]+\)\s*format\((["']?)([^"']+)\1\)/g; + var FONT_SRC_REGEX = /src:\s*(?:url\([^)]+\)\s*format\([^)]+\)[,;]\s*)+/g; + function toRegex(url) { + // eslint-disable-next-line no-useless-escape + var escaped = url.replace(/([.*+?^${}()|\[\]\/\\])/g, '\\$1'); + return new RegExp("(url\\(['\"]?)(".concat(escaped, ")(['\"]?\\))"), 'g'); + } + function parseURLs(cssText) { + var urls = []; + cssText.replace(URL_REGEX, function (raw, quotation, url) { + urls.push(url); + return raw; + }); + return urls.filter(function (url) { return !isDataUrl(url); }); + } + function embed(cssText, resourceURL, baseURL, options, getContentFromUrl) { + return __awaiter(this, void 0, void 0, function () { + var resolvedURL, contentType, dataURL, content; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + _a.trys.push([0, 5, , 6]); + resolvedURL = baseURL ? resolveUrl(resourceURL, baseURL) : resourceURL; + contentType = getMimeType(resourceURL); + dataURL = void 0; + if (!getContentFromUrl) return [3 /*break*/, 2]; + return [4 /*yield*/, getContentFromUrl(resolvedURL)]; + case 1: + content = _a.sent(); + dataURL = makeDataUrl(content, contentType); + return [3 /*break*/, 4]; + case 2: return [4 /*yield*/, resourceToDataURL(resolvedURL, contentType, options)]; + case 3: + dataURL = _a.sent(); + _a.label = 4; + case 4: return [2 /*return*/, cssText.replace(toRegex(resourceURL), "$1".concat(dataURL, "$3"))]; + case 5: + _a.sent(); + return [3 /*break*/, 6]; + case 6: return [2 /*return*/, cssText]; + } + }); + }); + } + function filterPreferredFontFormat(str, _a) { + var preferredFontFormat = _a.preferredFontFormat; + return !preferredFontFormat + ? str + : str.replace(FONT_SRC_REGEX, function (match) { + // eslint-disable-next-line no-constant-condition + while (true) { + var _a = URL_WITH_FORMAT_REGEX.exec(match) || [], src = _a[0], format = _a[2]; + if (!format) { + return ''; + } + if (format === preferredFontFormat) { + return "src: ".concat(src, ";"); + } + } + }); + } + function shouldEmbed(url) { + return url.search(URL_REGEX) !== -1; + } + function embedResources(cssText, baseUrl, options) { + return __awaiter(this, void 0, void 0, function () { + var filteredCSSText, urls; + return __generator(this, function (_a) { + if (!shouldEmbed(cssText)) { + return [2 /*return*/, cssText]; + } + filteredCSSText = filterPreferredFontFormat(cssText, options); + urls = parseURLs(filteredCSSText); + return [2 /*return*/, urls.reduce(function (deferred, url) { + return deferred.then(function (css) { return embed(css, url, baseUrl, options); }); + }, Promise.resolve(filteredCSSText))]; + }); + }); + } + + function embedProp(propName, node, options) { + var _a; + return __awaiter(this, void 0, void 0, function () { + var propValue, cssString; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + propValue = (_a = node.style) === null || _a === void 0 ? void 0 : _a.getPropertyValue(propName); + if (!propValue) return [3 /*break*/, 2]; + return [4 /*yield*/, embedResources(propValue, null, options)]; + case 1: + cssString = _b.sent(); + node.style.setProperty(propName, cssString, node.style.getPropertyPriority(propName)); + return [2 /*return*/, true]; + case 2: return [2 /*return*/, false]; + } + }); + }); + } + function embedBackground(clonedNode, options) { + return __awaiter(this, void 0, void 0, function () { + var _a, _b, _c, _d; + return __generator(this, function (_e) { + switch (_e.label) { + case 0: + return [4 /*yield*/, embedProp('background', clonedNode, options)]; + case 1: + _a = (_e.sent()); + if (_a) return [3 /*break*/, 3]; + return [4 /*yield*/, embedProp('background-image', clonedNode, options)]; + case 2: + _a = (_e.sent()); + _e.label = 3; + case 3: + return [4 /*yield*/, embedProp('mask', clonedNode, options)]; + case 4: + _d = (_e.sent()); + if (_d) return [3 /*break*/, 6]; + return [4 /*yield*/, embedProp('-webkit-mask', clonedNode, options)]; + case 5: + _d = (_e.sent()); + _e.label = 6; + case 6: + _c = _d; + if (_c) return [3 /*break*/, 8]; + return [4 /*yield*/, embedProp('mask-image', clonedNode, options)]; + case 7: + _c = (_e.sent()); + _e.label = 8; + case 8: + _b = _c; + if (_b) return [3 /*break*/, 10]; + return [4 /*yield*/, embedProp('-webkit-mask-image', clonedNode, options)]; + case 9: + _b = (_e.sent()); + _e.label = 10; + case 10: + return [2 /*return*/]; + } + }); + }); + } + function embedImageNode(clonedNode, options) { + return __awaiter(this, void 0, void 0, function () { + var isImageElement, url, dataURL; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + isImageElement = isInstanceOfElement(clonedNode, HTMLImageElement); + if (!(isImageElement && !isDataUrl(clonedNode.src)) && + !(isInstanceOfElement(clonedNode, SVGImageElement) && + !isDataUrl(clonedNode.href.baseVal))) { + return [2 /*return*/]; + } + url = isImageElement ? clonedNode.src : clonedNode.href.baseVal; + return [4 /*yield*/, resourceToDataURL(url, getMimeType(url), options)]; + case 1: + dataURL = _a.sent(); + return [4 /*yield*/, new Promise(function (resolve, reject) { + clonedNode.onload = resolve; + clonedNode.onerror = options.onImageErrorHandler + ? function () { + var attributes = []; + for (var _i = 0; _i < arguments.length; _i++) { + attributes[_i] = arguments[_i]; + } + try { + resolve(options.onImageErrorHandler.apply(options, attributes)); + } + catch (error) { + reject(error); + } + } + : reject; + var image = clonedNode; + if (image.decode) { + image.decode = resolve; + } + if (image.loading === 'lazy') { + image.loading = 'eager'; + } + if (isImageElement) { + clonedNode.srcset = ''; + clonedNode.src = dataURL; + } + else { + clonedNode.href.baseVal = dataURL; + } + })]; + case 2: + _a.sent(); + return [2 /*return*/]; + } + }); + }); + } + function embedChildren(clonedNode, options) { + return __awaiter(this, void 0, void 0, function () { + var children, deferreds; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + children = toArray(clonedNode.childNodes); + deferreds = children.map(function (child) { return embedImages(child, options); }); + return [4 /*yield*/, Promise.all(deferreds).then(function () { return clonedNode; })]; + case 1: + _a.sent(); + return [2 /*return*/]; + } + }); + }); + } + function embedImages(clonedNode, options) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!isInstanceOfElement(clonedNode, Element)) return [3 /*break*/, 4]; + return [4 /*yield*/, embedBackground(clonedNode, options)]; + case 1: + _a.sent(); + return [4 /*yield*/, embedImageNode(clonedNode, options)]; + case 2: + _a.sent(); + return [4 /*yield*/, embedChildren(clonedNode, options)]; + case 3: + _a.sent(); + _a.label = 4; + case 4: return [2 /*return*/]; + } + }); + }); + } + + function applyStyle(node, options) { + var style = node.style; + if (options.backgroundColor) { + style.backgroundColor = options.backgroundColor; + } + if (options.width) { + style.width = "".concat(options.width, "px"); + } + if (options.height) { + style.height = "".concat(options.height, "px"); + } + var manual = options.style; + if (manual != null) { + Object.keys(manual).forEach(function (key) { + style[key] = manual[key]; + }); + } + return node; + } + + function shouldLog(options, level) { + var _a; + var logLevel = (_a = options === null || options === void 0 ? void 0 : options.logLevel) !== null && _a !== void 0 ? _a : 'error'; + if (logLevel === 'silent') + return false; + if (logLevel === 'debug') + return true; + return level === 'error'; + } + function logDebug(options) { + var args = []; + for (var _i = 1; _i < arguments.length; _i++) { + args[_i - 1] = arguments[_i]; + } + if (shouldLog(options, 'debug')) { + // eslint-disable-next-line no-console + console.debug.apply(console, args); + } + } + var cssFetchCache = {}; + function fetchCSS(url) { + return __awaiter(this, void 0, void 0, function () { + var cache, res, cssText; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + cache = cssFetchCache[url]; + if (cache != null) { + return [2 /*return*/, cache]; + } + return [4 /*yield*/, fetch(url)]; + case 1: + res = _a.sent(); + return [4 /*yield*/, res.text()]; + case 2: + cssText = _a.sent(); + cache = { url: url, cssText: cssText }; + cssFetchCache[url] = cache; + return [2 /*return*/, cache]; + } + }); + }); + } + function embedFonts(data, options) { + return __awaiter(this, void 0, void 0, function () { + var cssText, regexUrl, fontLocs, loadFonts; + var _this = this; + return __generator(this, function (_a) { + cssText = data.cssText; + regexUrl = /url\(["']?([^"')]+)["']?\)/g; + fontLocs = cssText.match(/url\([^)]+\)/g) || []; + loadFonts = fontLocs.map(function (loc) { return __awaiter(_this, void 0, void 0, function () { + var url; + return __generator(this, function (_a) { + url = loc.replace(regexUrl, '$1'); + if (!url.startsWith('https://')) { + url = new URL(url, data.url).href; + } + return [2 /*return*/, fetchAsDataURL(url, options.fetchRequestInit, function (_a) { + var result = _a.result; + cssText = cssText.replace(loc, "url(".concat(result, ")")); + return [loc, result]; + })]; + }); + }); }); + return [2 /*return*/, Promise.all(loadFonts).then(function () { return cssText; })]; + }); + }); + } + function parseCSS(source) { + if (source == null) { + return []; + } + var result = []; + var commentsRegex = /(\/\*[\s\S]*?\*\/)/gi; + // strip out comments + var cssText = source.replace(commentsRegex, ''); + // eslint-disable-next-line prefer-regex-literals + var keyframesRegex = new RegExp('((@.*?keyframes [\\s\\S]*?){([\\s\\S]*?}\\s*?)})', 'gi'); + // eslint-disable-next-line no-constant-condition + while (true) { + var matches = keyframesRegex.exec(cssText); + if (matches === null) { + break; + } + result.push(matches[0]); + } + cssText = cssText.replace(keyframesRegex, ''); + var importRegex = /@import[\s\S]*?url\([^)]*\)[\s\S]*?;/gi; + // to match css & media queries together + var combinedCSSRegex = '((\\s*?(?:\\/\\*[\\s\\S]*?\\*\\/)?\\s*?@media[\\s\\S]' + + '*?){([\\s\\S]*?)}\\s*?})|(([\\s\\S]*?){([\\s\\S]*?)})'; + // unified regex + var unifiedRegex = new RegExp(combinedCSSRegex, 'gi'); + // eslint-disable-next-line no-constant-condition + while (true) { + var matches = importRegex.exec(cssText); + if (matches === null) { + matches = unifiedRegex.exec(cssText); + if (matches === null) { + break; + } + else { + importRegex.lastIndex = unifiedRegex.lastIndex; + } + } + else { + unifiedRegex.lastIndex = importRegex.lastIndex; + } + result.push(matches[0]); + } + return result; + } + function getCSSRules(styleSheets, options) { + return __awaiter(this, void 0, void 0, function () { + var ret, deferreds; + return __generator(this, function (_a) { + ret = []; + deferreds = []; + // First loop inlines imports + styleSheets.forEach(function (sheet) { + if ('cssRules' in sheet) { + try { + toArray(sheet.cssRules || []).forEach(function (item, index) { + if (item.type === CSSRule.IMPORT_RULE) { + var importIndex_1 = index + 1; + var url = item.href; + var deferred = fetchCSS(url) + .then(function (metadata) { return embedFonts(metadata, options); }) + .then(function (cssText) { + return parseCSS(cssText).forEach(function (rule) { + try { + sheet.insertRule(rule, rule.startsWith('@import') + ? (importIndex_1 += 1) + : sheet.cssRules.length); + } + catch (error) { + logDebug(options, 'Error inserting rule from remote css', { + rule: rule, + error: error, + }); + } + }); + }) + .catch(function (e) { + var _a, _b; + logDebug(options, 'Error loading remote css', (_b = (_a = e === null || e === void 0 ? void 0 : e.toString) === null || _a === void 0 ? void 0 : _a.call(e)) !== null && _b !== void 0 ? _b : String(e)); + }); + deferreds.push(deferred); + } + }); + } + catch (e) { + var inline_1 = styleSheets.find(function (a) { return a.href == null; }) || document.styleSheets[0]; + if (sheet.href != null) { + deferreds.push(fetchCSS(sheet.href) + .then(function (metadata) { return embedFonts(metadata, options); }) + .then(function (cssText) { + return parseCSS(cssText).forEach(function (rule) { + inline_1.insertRule(rule, inline_1.cssRules.length); + }); + }) + .catch(function (err) { + logDebug(options, 'Error loading remote stylesheet', err); + })); + } + logDebug(options, 'Error inlining remote css file', e); + } + } + }); + return [2 /*return*/, Promise.all(deferreds).then(function () { + // Second loop parses rules + styleSheets.forEach(function (sheet) { + if ('cssRules' in sheet) { + try { + toArray(sheet.cssRules || []).forEach(function (item) { + ret.push(item); + }); + } + catch (e) { + // Usually a CORS-protected stylesheet; not actionable for end users. + logDebug(options, "Error while reading CSS rules from ".concat(sheet.href), e); + } + } + }); + return ret; + })]; + }); + }); + } + function getWebFontRules(cssRules) { + return cssRules + .filter(function (rule) { return rule.type === CSSRule.FONT_FACE_RULE; }) + .filter(function (rule) { return shouldEmbed(rule.style.getPropertyValue('src')); }); + } + function parseWebFontRules(node, options) { + return __awaiter(this, void 0, void 0, function () { + var styleSheets, cssRules; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (node.ownerDocument == null) { + throw new Error('Provided element is not within a Document'); + } + styleSheets = toArray(node.ownerDocument.styleSheets); + return [4 /*yield*/, getCSSRules(styleSheets, options)]; + case 1: + cssRules = _a.sent(); + return [2 /*return*/, getWebFontRules(cssRules)]; + } + }); + }); + } + function normalizeFontFamily(font) { + return font.trim().replace(/["']/g, ''); + } + function getUsedFonts(node) { + var fonts = new Set(); + function traverse(node) { + var fontFamily = node.style.fontFamily || getComputedStyle(node).fontFamily; + fontFamily.split(',').forEach(function (font) { + fonts.add(normalizeFontFamily(font)); + }); + Array.from(node.children).forEach(function (child) { + if (child instanceof HTMLElement) { + traverse(child); + } + }); + } + traverse(node); + return fonts; + } + function getWebFontCSS(node, options) { + return __awaiter(this, void 0, void 0, function () { + var rules, usedFonts, cssTexts; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, parseWebFontRules(node, options)]; + case 1: + rules = _a.sent(); + usedFonts = getUsedFonts(node); + return [4 /*yield*/, Promise.all(rules + .filter(function (rule) { + return usedFonts.has(normalizeFontFamily(rule.style.fontFamily)); + }) + .map(function (rule) { + var baseUrl = rule.parentStyleSheet + ? rule.parentStyleSheet.href + : null; + return embedResources(rule.cssText, baseUrl, options); + }))]; + case 2: + cssTexts = _a.sent(); + return [2 /*return*/, cssTexts.join('\n')]; + } + }); + }); + } + function embedWebFonts(clonedNode, options) { + return __awaiter(this, void 0, void 0, function () { + var cssText, _a, _b, styleNode, sytleContent; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + if (!(options.fontEmbedCSS != null)) return [3 /*break*/, 1]; + _a = options.fontEmbedCSS; + return [3 /*break*/, 5]; + case 1: + if (!options.skipFonts) return [3 /*break*/, 2]; + _b = null; + return [3 /*break*/, 4]; + case 2: return [4 /*yield*/, getWebFontCSS(clonedNode, options)]; + case 3: + _b = _c.sent(); + _c.label = 4; + case 4: + _a = _b; + _c.label = 5; + case 5: + cssText = _a; + if (cssText) { + styleNode = document.createElement('style'); + sytleContent = document.createTextNode(cssText); + styleNode.appendChild(sytleContent); + if (clonedNode.firstChild) { + clonedNode.insertBefore(styleNode, clonedNode.firstChild); + } + else { + clonedNode.appendChild(styleNode); + } + } + return [2 /*return*/]; + } + }); + }); + } + + function toSvg(node, options) { + if (options === void 0) { options = {}; } + return __awaiter(this, void 0, void 0, function () { + var _a, width, height, clonedNode, datauri; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + _a = getImageSize(node, options), width = _a.width, height = _a.height; + return [4 /*yield*/, cloneNode(node, options, true)]; + case 1: + clonedNode = (_b.sent()); + return [4 /*yield*/, embedWebFonts(clonedNode, options)]; + case 2: + _b.sent(); + return [4 /*yield*/, embedImages(clonedNode, options)]; + case 3: + _b.sent(); + applyStyle(clonedNode, options); + return [4 /*yield*/, nodeToDataURL(clonedNode, width, height)]; + case 4: + datauri = _b.sent(); + return [2 /*return*/, datauri]; + } + }); + }); + } + function toCanvas(node, options) { + if (options === void 0) { options = {}; } + return __awaiter(this, void 0, void 0, function () { + var _a, width, height, svg, img, canvas, context, ratio, canvasWidth, canvasHeight; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + _a = getImageSize(node, options), width = _a.width, height = _a.height; + return [4 /*yield*/, toSvg(node, options)]; + case 1: + svg = _b.sent(); + return [4 /*yield*/, createImage(svg)]; + case 2: + img = _b.sent(); + canvas = document.createElement('canvas'); + context = canvas.getContext('2d'); + ratio = options.pixelRatio || getPixelRatio(); + canvasWidth = options.canvasWidth || width; + canvasHeight = options.canvasHeight || height; + canvas.width = canvasWidth * ratio; + canvas.height = canvasHeight * ratio; + if (!options.skipAutoScale) { + checkCanvasDimensions(canvas); + } + canvas.style.width = "".concat(canvasWidth, "px"); + canvas.style.height = "".concat(canvasHeight, "px"); + if (options.backgroundColor) { + context.fillStyle = options.backgroundColor; + context.fillRect(0, 0, canvas.width, canvas.height); + } + context.drawImage(img, 0, 0, canvas.width, canvas.height); + return [2 /*return*/, canvas]; + } + }); + }); + } + function toPixelData(node, options) { + if (options === void 0) { options = {}; } + return __awaiter(this, void 0, void 0, function () { + var _a, width, height, canvas, ctx; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + _a = getImageSize(node, options), width = _a.width, height = _a.height; + return [4 /*yield*/, toCanvas(node, options)]; + case 1: + canvas = _b.sent(); + ctx = canvas.getContext('2d'); + return [2 /*return*/, ctx.getImageData(0, 0, width, height).data]; + } + }); + }); + } + function toPng(node, options) { + if (options === void 0) { options = {}; } + return __awaiter(this, void 0, void 0, function () { + var canvas; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, toCanvas(node, options)]; + case 1: + canvas = _a.sent(); + return [2 /*return*/, canvas.toDataURL()]; + } + }); + }); + } + function toJpeg(node, options) { + if (options === void 0) { options = {}; } + return __awaiter(this, void 0, void 0, function () { + var canvas; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, toCanvas(node, options)]; + case 1: + canvas = _a.sent(); + return [2 /*return*/, canvas.toDataURL('image/jpeg', options.quality || 1)]; + } + }); + }); + } + function toBlob(node, options) { + if (options === void 0) { options = {}; } + return __awaiter(this, void 0, void 0, function () { + var canvas, blob; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, toCanvas(node, options)]; + case 1: + canvas = _a.sent(); + return [4 /*yield*/, canvasToBlob(canvas)]; + case 2: + blob = _a.sent(); + return [2 /*return*/, blob]; + } + }); + }); + } + function getFontEmbedCSS(node, options) { + if (options === void 0) { options = {}; } + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + return [2 /*return*/, getWebFontCSS(node, options)]; + }); + }); + } + + exports.getFontEmbedCSS = getFontEmbedCSS; + exports.toBlob = toBlob; + exports.toCanvas = toCanvas; + exports.toJpeg = toJpeg; + exports.toPixelData = toPixelData; + exports.toPng = toPng; + exports.toSvg = toSvg; + +})); +//# sourceMappingURL=html-to-image.js.map diff --git a/html-to-image.js.map b/html-to-image.js.map new file mode 100644 index 0000000..dd69611 --- /dev/null +++ b/html-to-image.js.map @@ -0,0 +1 @@ +{"version":3,"file":"html-to-image.js","sources":["../node_modules/tslib/tslib.es6.js","../src/util.ts","../src/clone-pseudos.ts","../src/mimes.ts","../src/dataurl.ts","../src/clone-node.ts","../src/embed-resources.ts","../src/embed-images.ts","../src/apply-style.ts","../src/embed-webfonts.ts","../src/index.ts"],"sourcesContent":["/******************************************************************************\r\nCopyright (c) Microsoft Corporation.\r\n\r\nPermission to use, copy, modify, and/or distribute this software for any\r\npurpose with or without fee is hereby granted.\r\n\r\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH\r\nREGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY\r\nAND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,\r\nINDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM\r\nLOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR\r\nOTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR\r\nPERFORMANCE OF THIS SOFTWARE.\r\n***************************************************************************** */\r\n/* global Reflect, Promise, SuppressedError, Symbol, Iterator */\r\n\r\nvar extendStatics = function(d, b) {\r\n extendStatics = Object.setPrototypeOf ||\r\n ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||\r\n function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };\r\n return extendStatics(d, b);\r\n};\r\n\r\nexport function __extends(d, b) {\r\n if (typeof b !== \"function\" && b !== null)\r\n throw new TypeError(\"Class extends value \" + String(b) + \" is not a constructor or null\");\r\n extendStatics(d, b);\r\n function __() { this.constructor = d; }\r\n d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());\r\n}\r\n\r\nexport var __assign = function() {\r\n __assign = Object.assign || function __assign(t) {\r\n for (var s, i = 1, n = arguments.length; i < n; i++) {\r\n s = arguments[i];\r\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];\r\n }\r\n return t;\r\n }\r\n return __assign.apply(this, arguments);\r\n}\r\n\r\nexport function __rest(s, e) {\r\n var t = {};\r\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)\r\n t[p] = s[p];\r\n if (s != null && typeof Object.getOwnPropertySymbols === \"function\")\r\n for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {\r\n if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))\r\n t[p[i]] = s[p[i]];\r\n }\r\n return t;\r\n}\r\n\r\nexport function __decorate(decorators, target, key, desc) {\r\n var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;\r\n if (typeof Reflect === \"object\" && typeof Reflect.decorate === \"function\") r = Reflect.decorate(decorators, target, key, desc);\r\n else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;\r\n return c > 3 && r && Object.defineProperty(target, key, r), r;\r\n}\r\n\r\nexport function __param(paramIndex, decorator) {\r\n return function (target, key) { decorator(target, key, paramIndex); }\r\n}\r\n\r\nexport function __esDecorate(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {\r\n function accept(f) { if (f !== void 0 && typeof f !== \"function\") throw new TypeError(\"Function expected\"); return f; }\r\n var kind = contextIn.kind, key = kind === \"getter\" ? \"get\" : kind === \"setter\" ? \"set\" : \"value\";\r\n var target = !descriptorIn && ctor ? contextIn[\"static\"] ? ctor : ctor.prototype : null;\r\n var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});\r\n var _, done = false;\r\n for (var i = decorators.length - 1; i >= 0; i--) {\r\n var context = {};\r\n for (var p in contextIn) context[p] = p === \"access\" ? {} : contextIn[p];\r\n for (var p in contextIn.access) context.access[p] = contextIn.access[p];\r\n context.addInitializer = function (f) { if (done) throw new TypeError(\"Cannot add initializers after decoration has completed\"); extraInitializers.push(accept(f || null)); };\r\n var result = (0, decorators[i])(kind === \"accessor\" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);\r\n if (kind === \"accessor\") {\r\n if (result === void 0) continue;\r\n if (result === null || typeof result !== \"object\") throw new TypeError(\"Object expected\");\r\n if (_ = accept(result.get)) descriptor.get = _;\r\n if (_ = accept(result.set)) descriptor.set = _;\r\n if (_ = accept(result.init)) initializers.unshift(_);\r\n }\r\n else if (_ = accept(result)) {\r\n if (kind === \"field\") initializers.unshift(_);\r\n else descriptor[key] = _;\r\n }\r\n }\r\n if (target) Object.defineProperty(target, contextIn.name, descriptor);\r\n done = true;\r\n};\r\n\r\nexport function __runInitializers(thisArg, initializers, value) {\r\n var useValue = arguments.length > 2;\r\n for (var i = 0; i < initializers.length; i++) {\r\n value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);\r\n }\r\n return useValue ? value : void 0;\r\n};\r\n\r\nexport function __propKey(x) {\r\n return typeof x === \"symbol\" ? x : \"\".concat(x);\r\n};\r\n\r\nexport function __setFunctionName(f, name, prefix) {\r\n if (typeof name === \"symbol\") name = name.description ? \"[\".concat(name.description, \"]\") : \"\";\r\n return Object.defineProperty(f, \"name\", { configurable: true, value: prefix ? \"\".concat(prefix, \" \", name) : name });\r\n};\r\n\r\nexport function __metadata(metadataKey, metadataValue) {\r\n if (typeof Reflect === \"object\" && typeof Reflect.metadata === \"function\") return Reflect.metadata(metadataKey, metadataValue);\r\n}\r\n\r\nexport function __awaiter(thisArg, _arguments, P, generator) {\r\n function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }\r\n return new (P || (P = Promise))(function (resolve, reject) {\r\n function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }\r\n function rejected(value) { try { step(generator[\"throw\"](value)); } catch (e) { reject(e); } }\r\n function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }\r\n step((generator = generator.apply(thisArg, _arguments || [])).next());\r\n });\r\n}\r\n\r\nexport function __generator(thisArg, body) {\r\n var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === \"function\" ? Iterator : Object).prototype);\r\n return g.next = verb(0), g[\"throw\"] = verb(1), g[\"return\"] = verb(2), typeof Symbol === \"function\" && (g[Symbol.iterator] = function() { return this; }), g;\r\n function verb(n) { return function (v) { return step([n, v]); }; }\r\n function step(op) {\r\n if (f) throw new TypeError(\"Generator is already executing.\");\r\n while (g && (g = 0, op[0] && (_ = 0)), _) try {\r\n if (f = 1, y && (t = op[0] & 2 ? y[\"return\"] : op[0] ? y[\"throw\"] || ((t = y[\"return\"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;\r\n if (y = 0, t) op = [op[0] & 2, t.value];\r\n switch (op[0]) {\r\n case 0: case 1: t = op; break;\r\n case 4: _.label++; return { value: op[1], done: false };\r\n case 5: _.label++; y = op[1]; op = [0]; continue;\r\n case 7: op = _.ops.pop(); _.trys.pop(); continue;\r\n default:\r\n if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }\r\n if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }\r\n if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }\r\n if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }\r\n if (t[2]) _.ops.pop();\r\n _.trys.pop(); continue;\r\n }\r\n op = body.call(thisArg, _);\r\n } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }\r\n if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };\r\n }\r\n}\r\n\r\nexport var __createBinding = Object.create ? (function(o, m, k, k2) {\r\n if (k2 === undefined) k2 = k;\r\n var desc = Object.getOwnPropertyDescriptor(m, k);\r\n if (!desc || (\"get\" in desc ? !m.__esModule : desc.writable || desc.configurable)) {\r\n desc = { enumerable: true, get: function() { return m[k]; } };\r\n }\r\n Object.defineProperty(o, k2, desc);\r\n}) : (function(o, m, k, k2) {\r\n if (k2 === undefined) k2 = k;\r\n o[k2] = m[k];\r\n});\r\n\r\nexport function __exportStar(m, o) {\r\n for (var p in m) if (p !== \"default\" && !Object.prototype.hasOwnProperty.call(o, p)) __createBinding(o, m, p);\r\n}\r\n\r\nexport function __values(o) {\r\n var s = typeof Symbol === \"function\" && Symbol.iterator, m = s && o[s], i = 0;\r\n if (m) return m.call(o);\r\n if (o && typeof o.length === \"number\") return {\r\n next: function () {\r\n if (o && i >= o.length) o = void 0;\r\n return { value: o && o[i++], done: !o };\r\n }\r\n };\r\n throw new TypeError(s ? \"Object is not iterable.\" : \"Symbol.iterator is not defined.\");\r\n}\r\n\r\nexport function __read(o, n) {\r\n var m = typeof Symbol === \"function\" && o[Symbol.iterator];\r\n if (!m) return o;\r\n var i = m.call(o), r, ar = [], e;\r\n try {\r\n while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);\r\n }\r\n catch (error) { e = { error: error }; }\r\n finally {\r\n try {\r\n if (r && !r.done && (m = i[\"return\"])) m.call(i);\r\n }\r\n finally { if (e) throw e.error; }\r\n }\r\n return ar;\r\n}\r\n\r\n/** @deprecated */\r\nexport function __spread() {\r\n for (var ar = [], i = 0; i < arguments.length; i++)\r\n ar = ar.concat(__read(arguments[i]));\r\n return ar;\r\n}\r\n\r\n/** @deprecated */\r\nexport function __spreadArrays() {\r\n for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length;\r\n for (var r = Array(s), k = 0, i = 0; i < il; i++)\r\n for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++)\r\n r[k] = a[j];\r\n return r;\r\n}\r\n\r\nexport function __spreadArray(to, from, pack) {\r\n if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {\r\n if (ar || !(i in from)) {\r\n if (!ar) ar = Array.prototype.slice.call(from, 0, i);\r\n ar[i] = from[i];\r\n }\r\n }\r\n return to.concat(ar || Array.prototype.slice.call(from));\r\n}\r\n\r\nexport function __await(v) {\r\n return this instanceof __await ? (this.v = v, this) : new __await(v);\r\n}\r\n\r\nexport function __asyncGenerator(thisArg, _arguments, generator) {\r\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\r\n var g = generator.apply(thisArg, _arguments || []), i, q = [];\r\n return i = Object.create((typeof AsyncIterator === \"function\" ? AsyncIterator : Object).prototype), verb(\"next\"), verb(\"throw\"), verb(\"return\", awaitReturn), i[Symbol.asyncIterator] = function () { return this; }, i;\r\n function awaitReturn(f) { return function (v) { return Promise.resolve(v).then(f, reject); }; }\r\n function verb(n, f) { if (g[n]) { i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; if (f) i[n] = f(i[n]); } }\r\n function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }\r\n function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }\r\n function fulfill(value) { resume(\"next\", value); }\r\n function reject(value) { resume(\"throw\", value); }\r\n function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }\r\n}\r\n\r\nexport function __asyncDelegator(o) {\r\n var i, p;\r\n return i = {}, verb(\"next\"), verb(\"throw\", function (e) { throw e; }), verb(\"return\"), i[Symbol.iterator] = function () { return this; }, i;\r\n function verb(n, f) { i[n] = o[n] ? function (v) { return (p = !p) ? { value: __await(o[n](v)), done: false } : f ? f(v) : v; } : f; }\r\n}\r\n\r\nexport function __asyncValues(o) {\r\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\r\n var m = o[Symbol.asyncIterator], i;\r\n return m ? m.call(o) : (o = typeof __values === \"function\" ? __values(o) : o[Symbol.iterator](), i = {}, verb(\"next\"), verb(\"throw\"), verb(\"return\"), i[Symbol.asyncIterator] = function () { return this; }, i);\r\n function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }\r\n function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }\r\n}\r\n\r\nexport function __makeTemplateObject(cooked, raw) {\r\n if (Object.defineProperty) { Object.defineProperty(cooked, \"raw\", { value: raw }); } else { cooked.raw = raw; }\r\n return cooked;\r\n};\r\n\r\nvar __setModuleDefault = Object.create ? (function(o, v) {\r\n Object.defineProperty(o, \"default\", { enumerable: true, value: v });\r\n}) : function(o, v) {\r\n o[\"default\"] = v;\r\n};\r\n\r\nvar ownKeys = function(o) {\r\n ownKeys = Object.getOwnPropertyNames || function (o) {\r\n var ar = [];\r\n for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;\r\n return ar;\r\n };\r\n return ownKeys(o);\r\n};\r\n\r\nexport function __importStar(mod) {\r\n if (mod && mod.__esModule) return mod;\r\n var result = {};\r\n if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== \"default\") __createBinding(result, mod, k[i]);\r\n __setModuleDefault(result, mod);\r\n return result;\r\n}\r\n\r\nexport function __importDefault(mod) {\r\n return (mod && mod.__esModule) ? mod : { default: mod };\r\n}\r\n\r\nexport function __classPrivateFieldGet(receiver, state, kind, f) {\r\n if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a getter\");\r\n if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot read private member from an object whose class did not declare it\");\r\n return kind === \"m\" ? f : kind === \"a\" ? f.call(receiver) : f ? f.value : state.get(receiver);\r\n}\r\n\r\nexport function __classPrivateFieldSet(receiver, state, value, kind, f) {\r\n if (kind === \"m\") throw new TypeError(\"Private method is not writable\");\r\n if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a setter\");\r\n if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot write private member to an object whose class did not declare it\");\r\n return (kind === \"a\" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;\r\n}\r\n\r\nexport function __classPrivateFieldIn(state, receiver) {\r\n if (receiver === null || (typeof receiver !== \"object\" && typeof receiver !== \"function\")) throw new TypeError(\"Cannot use 'in' operator on non-object\");\r\n return typeof state === \"function\" ? receiver === state : state.has(receiver);\r\n}\r\n\r\nexport function __addDisposableResource(env, value, async) {\r\n if (value !== null && value !== void 0) {\r\n if (typeof value !== \"object\" && typeof value !== \"function\") throw new TypeError(\"Object expected.\");\r\n var dispose, inner;\r\n if (async) {\r\n if (!Symbol.asyncDispose) throw new TypeError(\"Symbol.asyncDispose is not defined.\");\r\n dispose = value[Symbol.asyncDispose];\r\n }\r\n if (dispose === void 0) {\r\n if (!Symbol.dispose) throw new TypeError(\"Symbol.dispose is not defined.\");\r\n dispose = value[Symbol.dispose];\r\n if (async) inner = dispose;\r\n }\r\n if (typeof dispose !== \"function\") throw new TypeError(\"Object not disposable.\");\r\n if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };\r\n env.stack.push({ value: value, dispose: dispose, async: async });\r\n }\r\n else if (async) {\r\n env.stack.push({ async: true });\r\n }\r\n return value;\r\n\r\n}\r\n\r\nvar _SuppressedError = typeof SuppressedError === \"function\" ? SuppressedError : function (error, suppressed, message) {\r\n var e = new Error(message);\r\n return e.name = \"SuppressedError\", e.error = error, e.suppressed = suppressed, e;\r\n};\r\n\r\nexport function __disposeResources(env) {\r\n function fail(e) {\r\n env.error = env.hasError ? new _SuppressedError(e, env.error, \"An error was suppressed during disposal.\") : e;\r\n env.hasError = true;\r\n }\r\n var r, s = 0;\r\n function next() {\r\n while (r = env.stack.pop()) {\r\n try {\r\n if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);\r\n if (r.dispose) {\r\n var result = r.dispose.call(r.value);\r\n if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });\r\n }\r\n else s |= 1;\r\n }\r\n catch (e) {\r\n fail(e);\r\n }\r\n }\r\n if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();\r\n if (env.hasError) throw env.error;\r\n }\r\n return next();\r\n}\r\n\r\nexport function __rewriteRelativeImportExtension(path, preserveJsx) {\r\n if (typeof path === \"string\" && /^\\.\\.?\\//.test(path)) {\r\n return path.replace(/\\.(tsx)$|((?:\\.d)?)((?:\\.[^./]+?)?)\\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {\r\n return tsx ? preserveJsx ? \".jsx\" : \".js\" : d && (!ext || !cm) ? m : (d + ext + \".\" + cm.toLowerCase() + \"js\");\r\n });\r\n }\r\n return path;\r\n}\r\n\r\nexport default {\r\n __extends: __extends,\r\n __assign: __assign,\r\n __rest: __rest,\r\n __decorate: __decorate,\r\n __param: __param,\r\n __esDecorate: __esDecorate,\r\n __runInitializers: __runInitializers,\r\n __propKey: __propKey,\r\n __setFunctionName: __setFunctionName,\r\n __metadata: __metadata,\r\n __awaiter: __awaiter,\r\n __generator: __generator,\r\n __createBinding: __createBinding,\r\n __exportStar: __exportStar,\r\n __values: __values,\r\n __read: __read,\r\n __spread: __spread,\r\n __spreadArrays: __spreadArrays,\r\n __spreadArray: __spreadArray,\r\n __await: __await,\r\n __asyncGenerator: __asyncGenerator,\r\n __asyncDelegator: __asyncDelegator,\r\n __asyncValues: __asyncValues,\r\n __makeTemplateObject: __makeTemplateObject,\r\n __importStar: __importStar,\r\n __importDefault: __importDefault,\r\n __classPrivateFieldGet: __classPrivateFieldGet,\r\n __classPrivateFieldSet: __classPrivateFieldSet,\r\n __classPrivateFieldIn: __classPrivateFieldIn,\r\n __addDisposableResource: __addDisposableResource,\r\n __disposeResources: __disposeResources,\r\n __rewriteRelativeImportExtension: __rewriteRelativeImportExtension,\r\n};\r\n",null,null,null,null,null,null,null,null,null,null],"names":["shouldLog","logDebug"],"mappings":"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","x_google_ignoreList":[0]} \ No newline at end of file diff --git a/manifest.json b/manifest.json index 96068a5..4ad8d8a 100644 --- a/manifest.json +++ b/manifest.json @@ -24,7 +24,7 @@ "content_scripts": [ { "matches": [""], - "js": ["html2canvas-pro.min.js", "content.js"], + "js": ["html-to-image.js", "content.js"], "run_at": "document_idle" } ], diff --git a/tests/integration-scenarios.test.js b/tests/integration-scenarios.test.js index e32a8c1..d0279e8 100644 --- a/tests/integration-scenarios.test.js +++ b/tests/integration-scenarios.test.js @@ -53,8 +53,10 @@ global.window = { }) }; -// Mock html2canvas -global.html2canvas = jest.fn(); +// Mock html-to-image +global.htmlToImage = { + toCanvas: jest.fn(), +}; describe('Integration Scenarios - Clipboard with Different Backgrounds', () => { let screenshotSelector; @@ -63,7 +65,7 @@ describe('Integration Scenarios - Clipboard with Different Backgrounds', () => { beforeEach(() => { jest.clearAllMocks(); - + // Reset clipboard mock for each test if (!global.navigator) { global.navigator = {}; @@ -75,7 +77,7 @@ describe('Integration Scenarios - Clipboard with Different Backgrounds', () => { return { data, type: 'image/png' }; }); global.window.isSecureContext = true; - + // Mock element to capture mockElement = { classList: { @@ -102,7 +104,7 @@ describe('Integration Scenarios - Clipboard with Different Backgrounds', () => { toDataURL: jest.fn(() => 'data:image/png;base64,mockdata') }; - global.html2canvas = jest.fn().mockResolvedValue(mockCanvas); + global.htmlToImage.toCanvas = jest.fn().mockResolvedValue(mockCanvas); @@ -183,11 +185,17 @@ describe('Integration Scenarios - Clipboard with Different Backgrounds', () => { 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 + const backgroundColor = + this.background === 'transparent' + ? undefined + : this.background === 'white' + ? '#ffffff' + : '#000000'; + + const canvas = await global.htmlToImage.toCanvas(element, { + backgroundColor, + pixelRatio: global.window.devicePixelRatio || 1, + cacheBust: false }); // Always download first @@ -205,7 +213,7 @@ describe('Integration Scenarios - Clipboard with Different Backgrounds', () => { // Update UI based on results (matching new message format) let successMessage = 'Screenshot captured!'; let messageIcon = '✅'; - + if (this.saveToPc && this.copyToClipboard) { // Both operations enabled if (clipboardResult && clipboardResult.success) { @@ -235,7 +243,7 @@ describe('Integration Scenarios - Clipboard with Different Backgrounds', () => { } this.overlay.innerHTML = `
${successMessage}
`; - + return { success: true, downloadSuccess: true, @@ -258,13 +266,11 @@ describe('Integration Scenarios - Clipboard with Different Backgrounds', () => { 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(global.htmlToImage.toCanvas).toHaveBeenCalledWith(mockElement, expect.objectContaining({ + backgroundColor: undefined, // Transparent background })); expect(result.success).toBe(true); @@ -275,13 +281,11 @@ describe('Integration Scenarios - Clipboard with Different Backgrounds', () => { 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({ + expect(global.htmlToImage.toCanvas).toHaveBeenCalledWith(mockElement, expect.objectContaining({ backgroundColor: '#000000', // Black background - useCORS: true, - allowTaint: false })); expect(result.success).toBe(true); @@ -291,13 +295,11 @@ describe('Integration Scenarios - Clipboard with Different Backgrounds', () => { 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({ + expect(global.htmlToImage.toCanvas).toHaveBeenCalledWith(mockElement, expect.objectContaining({ backgroundColor: '#ffffff', // White background - useCORS: true, - allowTaint: false })); expect(result.success).toBe(true); @@ -307,7 +309,7 @@ describe('Integration Scenarios - Clipboard with Different Backgrounds', () => { test('should capture without clipboard when disabled', async () => { await screenshotSelector.init('black', false, true); // saveToPc = true, copyToClipboard = false - + const result = await screenshotSelector.captureElement(mockElement); expect(result.success).toBe(true); @@ -324,9 +326,9 @@ describe('Integration Scenarios - Clipboard with Different Backgrounds', () => { global.navigator.clipboard = { write: jest.fn().mockRejectedValue(new DOMException('Permission denied', 'NotAllowedError')) }; - + await screenshotSelector.init('black', true); - + const result = await screenshotSelector.captureElement(mockElement); expect(result.success).toBe(true); @@ -340,9 +342,9 @@ describe('Integration Scenarios - Clipboard with Different Backgrounds', () => { 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); @@ -359,7 +361,7 @@ describe('Integration Scenarios - Complete Workflow', () => { beforeEach(() => { jest.clearAllMocks(); - + // Mock popup elements mockPopupElements = { clipboardToggle: { checked: true, addEventListener: jest.fn() }, @@ -399,18 +401,18 @@ describe('Integration Scenarios - Complete Workflow', () => { 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, @@ -466,12 +468,12 @@ describe('Integration Scenarios - Complete Workflow', () => { 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 }); }); @@ -499,9 +501,9 @@ describe('Integration Scenarios - Complete Workflow', () => { test('should handle incompatible pages gracefully', async () => { // Mock incompatible page - global.chrome.tabs.query.mockResolvedValue([{ - id: 123, - url: 'chrome://settings/' + global.chrome.tabs.query.mockResolvedValue([{ + id: 123, + url: 'chrome://settings/' }]); let startButtonCallback; @@ -514,7 +516,7 @@ describe('Integration Scenarios - Complete Workflow', () => { 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 }); @@ -527,7 +529,7 @@ describe('Integration Scenarios - Complete Workflow', () => { status.style.display = 'block'; return; } - + // Normal flow would continue here }); }); diff --git a/tests/save-to-pc-clipboard-combinations.test.js b/tests/save-to-pc-clipboard-combinations.test.js index 4506c35..58f9f49 100644 --- a/tests/save-to-pc-clipboard-combinations.test.js +++ b/tests/save-to-pc-clipboard-combinations.test.js @@ -53,8 +53,10 @@ global.window = { CSS: { escape: jest.fn(str => str) } }; -// Mock html2canvas -global.html2canvas = jest.fn(); +// Mock html-to-image +global.htmlToImage = { + toCanvas: jest.fn(), +}; // Mock ScreenshotSelector class for testing class MockScreenshotSelector { @@ -126,10 +128,18 @@ class MockScreenshotSelector { toDataURL: jest.fn(() => 'data:image/png;base64,mockdata') }; - // Mock html2canvas - global.html2canvas.mockResolvedValue(mockCanvas); + // Mock html-to-image + global.htmlToImage.toCanvas.mockResolvedValue(mockCanvas); - const canvas = await global.html2canvas(element); + const canvas = await global.htmlToImage.toCanvas(element, { + backgroundColor: this.background === 'transparent' + ? undefined + : this.background === 'white' + ? '#ffffff' + : '#000000', + pixelRatio: global.window.devicePixelRatio || 1, + cacheBust: false + }); // Conditionally download based on saveToPc setting if (this.saveToPc) { @@ -159,7 +169,7 @@ describe('Save to PC and Clipboard Combinations', () => { beforeEach(() => { jest.clearAllMocks(); - + // Reset mocks mockCreateElement.mockClear(); mockCreateElement.mockReturnValue(mockLink); @@ -168,7 +178,7 @@ describe('Save to PC and Clipboard Combinations', () => { global.document.createElement = mockCreateElement; } mockLink.click.mockClear(); - + // Setup clipboard API mocks global.navigator = { clipboard: { @@ -207,7 +217,7 @@ describe('Save to PC and Clipboard Combinations', () => { // Verify download was triggered expect(mockCreateElement).toHaveBeenCalledWith('a'); expect(mockLink.click).toHaveBeenCalled(); - + // Verify clipboard operation was attempted expect(global.navigator.clipboard.write).toHaveBeenCalled(); }); @@ -256,7 +266,7 @@ describe('Save to PC and Clipboard Combinations', () => { // Verify download was triggered expect(mockCreateElement).toHaveBeenCalledWith('a'); expect(mockLink.click).toHaveBeenCalled(); - + // Verify clipboard operation was NOT attempted expect(global.navigator.clipboard.write).not.toHaveBeenCalled(); }); @@ -287,7 +297,7 @@ describe('Save to PC and Clipboard Combinations', () => { // Verify download was NOT triggered expect(mockCreateElement).not.toHaveBeenCalledWith('a'); - + // Verify clipboard operation was attempted expect(global.navigator.clipboard.write).toHaveBeenCalled(); }); @@ -317,7 +327,7 @@ describe('Save to PC and Clipboard Combinations', () => { // Verify download was NOT triggered expect(mockCreateElement).not.toHaveBeenCalledWith('a'); - + // Verify clipboard operation was NOT attempted expect(global.navigator.clipboard.write).not.toHaveBeenCalled(); }); @@ -334,8 +344,11 @@ describe('Save to PC and Clipboard Combinations', () => { test('should still create canvas even when both outputs are disabled', async () => { await screenshotSelector.captureElement(mockElement); - // html2canvas should still be called (needed for potential future operations) - expect(global.html2canvas).toHaveBeenCalledWith(mockElement); + // html-to-image should still be called (needed for potential future operations) + expect(global.htmlToImage.toCanvas).toHaveBeenCalledWith( + mockElement, + expect.any(Object) + ); }); }); }); @@ -349,24 +362,24 @@ describe('Popup Validation Logic for All Combinations', () => { beforeEach(() => { jest.clearAllMocks(); - + // Mock DOM elements clipboardToggle = { checked: true, addEventListener: jest.fn() }; - + saveToPcToggle = { checked: true, addEventListener: jest.fn() }; - + startBtn = { disabled: false, setAttribute: jest.fn(), addEventListener: jest.fn() }; - + validationWarning = { style: { display: 'none' } }; @@ -384,7 +397,7 @@ describe('Popup Validation Logic for All Combinations', () => { // Define validation function validateOutputMethods = function() { const hasValidOutput = clipboardToggle.checked || saveToPcToggle.checked; - + if (hasValidOutput) { validationWarning.style.display = 'none'; startBtn.disabled = false; @@ -394,7 +407,7 @@ describe('Popup Validation Logic for All Combinations', () => { startBtn.disabled = true; startBtn.setAttribute('aria-describedby', 'validation-warning'); } - + return hasValidOutput; }; }); @@ -454,13 +467,13 @@ describe('Preference Persistence Across Browser Sessions', () => { beforeEach(() => { jest.clearAllMocks(); - + // Mock DOM elements mockClipboardToggle = { checked: false, addEventListener: jest.fn() }; - + mockSaveToPcToggle = { checked: false, addEventListener: jest.fn() @@ -481,13 +494,13 @@ describe('Preference Persistence Across Browser Sessions', () => { test('should default to both enabled for new installations', async () => { // Mock no saved preferences (new installation) global.chrome.storage.sync.get.mockResolvedValueOnce({}); - + const popupScript = ` document.addEventListener('DOMContentLoaded', async () => { const clipboardToggle = document.getElementById('clipboard-toggle'); const saveToPcToggle = document.getElementById('save-to-pc-toggle'); const saved = await chrome.storage.sync.get(['backgroundPreference', 'copyToClipboard', 'saveToPc']); - + clipboardToggle.checked = saved.copyToClipboard !== undefined ? saved.copyToClipboard : true; saveToPcToggle.checked = saved.saveToPc !== undefined ? saved.saveToPc : true; }); @@ -513,13 +526,13 @@ describe('Preference Persistence Across Browser Sessions', () => { copyToClipboard: false, saveToPc: true }); - + const popupScript = ` document.addEventListener('DOMContentLoaded', async () => { const clipboardToggle = document.getElementById('clipboard-toggle'); const saveToPcToggle = document.getElementById('save-to-pc-toggle'); const saved = await chrome.storage.sync.get(['backgroundPreference', 'copyToClipboard', 'saveToPc']); - + clipboardToggle.checked = saved.copyToClipboard !== undefined ? saved.copyToClipboard : true; saveToPcToggle.checked = saved.saveToPc !== undefined ? saved.saveToPc : true; }); @@ -545,13 +558,13 @@ describe('Preference Persistence Across Browser Sessions', () => { copyToClipboard: true, saveToPc: false }); - + const popupScript = ` document.addEventListener('DOMContentLoaded', async () => { const clipboardToggle = document.getElementById('clipboard-toggle'); const saveToPcToggle = document.getElementById('save-to-pc-toggle'); const saved = await chrome.storage.sync.get(['backgroundPreference', 'copyToClipboard', 'saveToPc']); - + clipboardToggle.checked = saved.copyToClipboard !== undefined ? saved.copyToClipboard : true; saveToPcToggle.checked = saved.saveToPc !== undefined ? saved.saveToPc : true; }); @@ -583,7 +596,7 @@ describe('Preference Persistence Across Browser Sessions', () => { const clipboardToggle = document.getElementById('clipboard-toggle'); const saveToPcToggle = document.getElementById('save-to-pc-toggle'); const saved = await chrome.storage.sync.get(['backgroundPreference', 'copyToClipboard', 'saveToPc']); - + clipboardToggle.checked = saved.copyToClipboard !== undefined ? saved.copyToClipboard : true; saveToPcToggle.checked = saved.saveToPc !== undefined ? saved.saveToPc : true; }); @@ -601,7 +614,7 @@ describe('Preference Persistence Across Browser Sessions', () => { expect(mockClipboardToggle.checked).toBe(false); expect(mockSaveToPcToggle.checked).toBe(false); - + // Validation should fail with both disabled const isValid = mockClipboardToggle.checked || mockSaveToPcToggle.checked; expect(isValid).toBe(false); @@ -613,13 +626,13 @@ describe('Preference Persistence Across Browser Sessions', () => { copyToClipboard: true // saveToPc is undefined (not set in previous version) }); - + const popupScript = ` document.addEventListener('DOMContentLoaded', async () => { const clipboardToggle = document.getElementById('clipboard-toggle'); const saveToPcToggle = document.getElementById('save-to-pc-toggle'); const saved = await chrome.storage.sync.get(['backgroundPreference', 'copyToClipboard', 'saveToPc']); - + clipboardToggle.checked = saved.copyToClipboard !== undefined ? saved.copyToClipboard : true; saveToPcToggle.checked = saved.saveToPc !== undefined ? saved.saveToPc : true; }); diff --git a/tests/setup.js b/tests/setup.js index e6a0b6e..ef9df98 100644 --- a/tests/setup.js +++ b/tests/setup.js @@ -233,13 +233,15 @@ global.mockClipboardAPI = (scenario = 'supported') => { beforeEach(() => { // Clear all mocks first jest.clearAllMocks(); - + global.mockChromeAPIs(); global.mockDOMAPIs(); global.mockClipboardAPI('supported'); - - // Mock html2canvas - global.html2canvas = jest.fn().mockResolvedValue(global.createMockCanvas()); + + // Mock html-to-image + global.htmlToImage = { + toCanvas: jest.fn().mockResolvedValue(global.createMockCanvas()), + }; }); // Cleanup after each test @@ -251,13 +253,13 @@ afterEach(() => { expect.extend({ toHaveBeenCalledWithClipboardMessage(received, expectedBackground, expectedClipboard) { const calls = received.mock.calls; - const matchingCall = calls.find(call => - call[1] && + 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`, @@ -270,7 +272,7 @@ expect.extend({ }; } }, - + toHaveClipboardResult(received, expectedSuccess, expectedErrorType) { if (!received.clipboardResult) { return { @@ -278,10 +280,10 @@ expect.extend({ 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`,