diff --git a/tests/integration-scenarios.test.js b/tests/integration-scenarios.test.js index 2221bf7..e32a8c1 100644 --- a/tests/integration-scenarios.test.js +++ b/tests/integration-scenarios.test.js @@ -64,6 +64,18 @@ describe('Integration Scenarios - Clipboard with Different Backgrounds', () => { 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: { @@ -92,13 +104,7 @@ describe('Integration Scenarios - Clipboard with Different Backgrounds', () => { 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 { @@ -106,12 +112,14 @@ describe('Integration Scenarios - Clipboard with Different Backgrounds', () => { this.isActive = false; this.background = 'black'; this.copyToClipboard = true; + this.saveToPc = true; this.overlay = null; } - async init(background = 'black', copyToClipboard = true) { + async init(background = 'black', copyToClipboard = true, saveToPc = true) { this.background = background; this.copyToClipboard = copyToClipboard; + this.saveToPc = saveToPc; this.isActive = true; this.createUI(); } @@ -194,12 +202,36 @@ describe('Integration Scenarios - Clipboard with Different Backgrounds', () => { 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}`; + // 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}
`; @@ -238,7 +270,7 @@ describe('Integration Scenarios - Clipboard with Different Backgrounds', () => { 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!'); + expect(result.message).toBe('Screenshot saved and copied to clipboard!'); }); test('should capture with black background and copy to clipboard', async () => { @@ -254,7 +286,7 @@ describe('Integration Scenarios - Clipboard with Different Backgrounds', () => { expect(result.success).toBe(true); expect(result.clipboardResult.success).toBe(true); - expect(result.message).toBe('Screenshot downloaded and copied to clipboard!'); + expect(result.message).toBe('Screenshot saved and copied to clipboard!'); }); test('should capture with white background and copy to clipboard', async () => { @@ -270,24 +302,28 @@ describe('Integration Scenarios - Clipboard with Different Backgrounds', () => { expect(result.success).toBe(true); expect(result.clipboardResult.success).toBe(true); - expect(result.message).toBe('Screenshot downloaded and copied to clipboard!'); + expect(result.message).toBe('Screenshot saved and copied to clipboard!'); }); test('should capture without clipboard when disabled', async () => { - await screenshotSelector.init('black', false); + 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 downloaded successfully!'); - expect(global.navigator.clipboard.write).not.toHaveBeenCalled(); + 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 () => { - // Mock clipboard failure - global.navigator.clipboard.write.mockRejectedValue(new DOMException('Permission denied', 'NotAllowedError')); + // Reset and mock clipboard failure + global.navigator.clipboard = { + write: jest.fn().mockRejectedValue(new DOMException('Permission denied', 'NotAllowedError')) + }; await screenshotSelector.init('black', true); @@ -297,8 +333,8 @@ describe('Integration Scenarios - Clipboard with Different Backgrounds', () => { 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'); + expect(result.message).toContain('Screenshot saved successfully!'); + expect(result.message).toContain('permission denied'); }); test('should handle clipboard API unavailable scenario', async () => { @@ -313,8 +349,8 @@ describe('Integration Scenarios - Clipboard with Different Backgrounds', () => { 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'); + expect(result.message).toContain('Screenshot saved successfully!'); + expect(result.message).toContain('clipboard'); }); }); 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..7791096 --- /dev/null +++ b/tests/save-to-pc-clipboard-combinations.test.js @@ -0,0 +1,637 @@ +/** + * 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); + 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/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