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