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