Add copy to clipboard and save to pc toggle.

* escaped node modules from vcs

* feat: added copy to clipboard feature

* feat: added tests

* feat: added feature spec

* feat: added toggle for save to pc

* feat: added tests

* refactor: change toggle from '?' button to the 'switch' component

* Fix failing tests

* Update gitignore and remove unnecessary files

* Remove tooltips because they are unnecessary

---------

Co-authored-by: Joey Yakimowich-Payne <jyapayne@gmail.com>
This commit is contained in:
Karthikeyan N 2025-08-14 00:59:57 +05:30 • committed by Joey
commit 9dd4d706fa
12 changed files with 7780 additions and 22 deletions

View file

@ -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.');
});
});

View file

@ -0,0 +1,557 @@
/**
* Integration test suite for clipboard functionality across different scenarios
* Tests end-to-end workflows combining clipboard operations with various settings
*/
// Mock Chrome APIs
global.chrome = {
storage: {
sync: {
get: jest.fn(),
set: jest.fn()
}
},
runtime: {
sendMessage: jest.fn()
},
tabs: {
query: jest.fn(),
sendMessage: jest.fn()
}
};
// Mock DOM APIs
global.document = {
createElement: jest.fn(() => ({
style: {},
classList: { add: jest.fn(), remove: jest.fn() },
appendChild: jest.fn(),
click: jest.fn(),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
innerHTML: '',
textContent: '',
id: '',
className: ''
})),
head: { appendChild: jest.fn() },
body: { appendChild: jest.fn() },
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
querySelectorAll: jest.fn(() => []),
getElementById: jest.fn()
};
global.window = {
getComputedStyle: jest.fn(() => ({})),
devicePixelRatio: 2,
isSecureContext: true,
CSS: { escape: jest.fn(str => str) },
setTimeout: jest.fn((fn, delay) => {
if (delay === 0) fn();
return 1;
})
};
// Mock html2canvas
global.html2canvas = jest.fn();
describe('Integration Scenarios - Clipboard with Different Backgrounds', () => {
let screenshotSelector;
let mockElement;
let mockCanvas;
beforeEach(() => {
jest.clearAllMocks();
// Reset clipboard mock for each test
if (!global.navigator) {
global.navigator = {};
}
global.navigator.clipboard = {
write: jest.fn().mockResolvedValue()
};
global.window.ClipboardItem = jest.fn().mockImplementation((data) => {
return { data, type: 'image/png' };
});
global.window.isSecureContext = true;
// Mock element to capture
mockElement = {
classList: {
add: jest.fn(),
remove: jest.fn()
},
style: {},
getBoundingClientRect: () => ({ left: 0, top: 0, width: 100, height: 100 }),
scrollHeight: 100,
clientHeight: 100,
scrollWidth: 100,
clientWidth: 100,
children: [],
parentNode: { children: [] }
};
// Mock canvas
mockCanvas = {
toBlob: jest.fn((callback, format) => {
const mockBlob = new Blob(['mock image data'], { type: format });
Object.defineProperty(mockBlob, 'size', { value: 1024 }); // 1KB
callback(mockBlob);
}),
toDataURL: jest.fn(() => 'data:image/png;base64,mockdata')
};
global.html2canvas = jest.fn().mockResolvedValue(mockCanvas);
// Import ScreenshotSelector (simplified for testing)
const ScreenshotSelector = class {
constructor() {
this.isActive = false;
this.background = 'black';
this.copyToClipboard = true;
this.saveToPc = true;
this.overlay = null;
}
async init(background = 'black', copyToClipboard = true, saveToPc = true) {
this.background = background;
this.copyToClipboard = copyToClipboard;
this.saveToPc = saveToPc;
this.isActive = true;
this.createUI();
}
createUI() {
this.overlay = global.document.createElement('div');
global.document.body.appendChild(this.overlay);
}
checkClipboardAPIAvailability() {
if (!global.navigator.clipboard) {
return { available: false, reason: 'Clipboard API not available in this browser' };
}
if (!global.window.ClipboardItem) {
return { available: false, reason: 'ClipboardItem not supported in this browser' };
}
if (!global.window.isSecureContext) {
return { available: false, reason: 'Clipboard API requires a secure context (HTTPS)' };
}
if (!global.navigator.clipboard.write) {
return { available: false, reason: 'Clipboard write functionality not available' };
}
return { available: true };
}
async copyCanvasToClipboard(canvas) {
try {
const availability = this.checkClipboardAPIAvailability();
if (!availability.available) {
return { success: false, error: availability.reason, errorType: 'api_unavailable' };
}
const blob = await new Promise((resolve, reject) => {
canvas.toBlob((blob) => {
if (blob) resolve(blob);
else reject(new Error('Canvas to blob conversion returned null'));
}, 'image/png');
});
const maxBlobSize = 50 * 1024 * 1024;
if (blob.size > maxBlobSize) {
return {
success: false,
error: `Image too large for clipboard (${Math.round(blob.size / 1024 / 1024)}MB)`,
errorType: 'size_limit'
};
}
const clipboardItem = new global.window.ClipboardItem({ 'image/png': blob });
await global.navigator.clipboard.write([clipboardItem]);
return { success: true };
} catch (error) {
if (error.name === 'NotAllowedError') {
return { success: false, error: 'Permission denied', errorType: 'permission_denied' };
}
return { success: false, error: error.message, errorType: 'unexpected' };
}
}
async captureElement(element) {
try {
// Generate canvas with background-specific settings
const canvas = await global.html2canvas(element, {
backgroundColor: this.background === 'transparent' ? null :
this.background === 'white' ? '#ffffff' : '#000000',
useCORS: true,
allowTaint: false
});
// Always download first
const link = global.document.createElement('a');
link.href = canvas.toDataURL('image/png');
link.download = `screenshot-${new Date().toISOString().slice(0, 19).replace(/[:.]/g, '-')}.png`;
link.click();
// Handle clipboard if enabled
let clipboardResult = null;
if (this.copyToClipboard) {
clipboardResult = await this.copyCanvasToClipboard(canvas);
}
// Update UI based on results (matching new message format)
let successMessage = 'Screenshot captured!';
let messageIcon = '✅';
if (this.saveToPc && this.copyToClipboard) {
// Both operations enabled
if (clipboardResult && clipboardResult.success) {
successMessage = 'Screenshot saved and copied to clipboard!';
messageIcon = '✅';
} else {
// Save succeeded but clipboard failed
const errorMessage = clipboardResult ? clipboardResult.error.toLowerCase() : 'clipboard copy failed';
successMessage = `Screenshot saved successfully! However, ${errorMessage}`;
messageIcon = '⚠️';
}
} else if (this.saveToPc && !this.copyToClipboard) {
// Only save to PC enabled
successMessage = 'Screenshot saved to downloads folder!';
messageIcon = '💾';
} else if (!this.saveToPc && this.copyToClipboard) {
// Only clipboard enabled
if (clipboardResult && clipboardResult.success) {
successMessage = 'Screenshot copied to clipboard!';
messageIcon = '📋';
} else {
// Clipboard failed - this is a complete failure since no other output method is enabled
const errorMessage = clipboardResult ? clipboardResult.error : 'clipboard copy failed';
successMessage = `Screenshot capture failed: ${errorMessage}`;
messageIcon = '❌';
}
}
this.overlay.innerHTML = `<div>${successMessage}</div>`;
return {
success: true,
downloadSuccess: true,
clipboardResult: clipboardResult,
message: successMessage
};
} catch (error) {
this.overlay.innerHTML = `<div>Screenshot failed: ${error.message}</div>`;
return {
success: false,
error: error.message
};
}
}
};
screenshotSelector = new ScreenshotSelector();
});
test('should capture with transparent background and copy to clipboard', async () => {
await screenshotSelector.init('transparent', true);
const result = await screenshotSelector.captureElement(mockElement);
expect(global.html2canvas).toHaveBeenCalledWith(mockElement, expect.objectContaining({
backgroundColor: null, // Transparent background
useCORS: true,
allowTaint: false
}));
expect(result.success).toBe(true);
expect(result.downloadSuccess).toBe(true);
expect(result.clipboardResult.success).toBe(true);
expect(result.message).toBe('Screenshot saved and copied to clipboard!');
});
test('should capture with black background and copy to clipboard', async () => {
await screenshotSelector.init('black', true);
const result = await screenshotSelector.captureElement(mockElement);
expect(global.html2canvas).toHaveBeenCalledWith(mockElement, expect.objectContaining({
backgroundColor: '#000000', // Black background
useCORS: true,
allowTaint: false
}));
expect(result.success).toBe(true);
expect(result.clipboardResult.success).toBe(true);
expect(result.message).toBe('Screenshot saved and copied to clipboard!');
});
test('should capture with white background and copy to clipboard', async () => {
await screenshotSelector.init('white', true);
const result = await screenshotSelector.captureElement(mockElement);
expect(global.html2canvas).toHaveBeenCalledWith(mockElement, expect.objectContaining({
backgroundColor: '#ffffff', // White background
useCORS: true,
allowTaint: false
}));
expect(result.success).toBe(true);
expect(result.clipboardResult.success).toBe(true);
expect(result.message).toBe('Screenshot saved and copied to clipboard!');
});
test('should capture without clipboard when disabled', async () => {
await screenshotSelector.init('black', false, true); // saveToPc = true, copyToClipboard = false
const result = await screenshotSelector.captureElement(mockElement);
expect(result.success).toBe(true);
expect(result.downloadSuccess).toBe(true);
expect(result.clipboardResult).toBe(null); // No clipboard operation attempted
expect(result.message).toBe('Screenshot saved to downloads folder!');
if (global.navigator.clipboard?.write) {
expect(global.navigator.clipboard.write).not.toHaveBeenCalled();
}
});
test('should handle clipboard failure gracefully while maintaining download', async () => {
// Reset and mock clipboard failure
global.navigator.clipboard = {
write: jest.fn().mockRejectedValue(new DOMException('Permission denied', 'NotAllowedError'))
};
await screenshotSelector.init('black', true);
const result = await screenshotSelector.captureElement(mockElement);
expect(result.success).toBe(true);
expect(result.downloadSuccess).toBe(true);
expect(result.clipboardResult.success).toBe(false);
expect(result.clipboardResult.errorType).toBe('permission_denied');
expect(result.message).toContain('Screenshot saved successfully!');
expect(result.message).toContain('permission denied');
});
test('should handle clipboard API unavailable scenario', async () => {
// Mock clipboard API not available
global.navigator.clipboard = undefined;
await screenshotSelector.init('transparent', true);
const result = await screenshotSelector.captureElement(mockElement);
expect(result.success).toBe(true);
expect(result.downloadSuccess).toBe(true);
expect(result.clipboardResult.success).toBe(false);
expect(result.clipboardResult.errorType).toBe('api_unavailable');
expect(result.message).toContain('Screenshot saved successfully!');
expect(result.message).toContain('clipboard');
});
});
describe('Integration Scenarios - Complete Workflow', () => {
let mockPopupElements;
beforeEach(() => {
jest.clearAllMocks();
// Mock popup elements
mockPopupElements = {
clipboardToggle: { checked: true, addEventListener: jest.fn() },
backgroundSelect: { value: 'black', addEventListener: jest.fn() },
startButton: { addEventListener: jest.fn(), style: { display: 'block' } },
status: { textContent: '', className: '', style: { display: 'none' } }
};
global.document.getElementById = jest.fn((id) => {
switch (id) {
case 'clipboard-toggle': return mockPopupElements.clipboardToggle;
case 'background-select': return mockPopupElements.backgroundSelect;
case 'start-selector': return mockPopupElements.startButton;
case 'status': return mockPopupElements.status;
default: return null;
}
});
// Mock Chrome APIs
global.chrome.storage.sync.get.mockResolvedValue({
backgroundPreference: 'black',
copyToClipboard: true
});
global.chrome.tabs.query.mockResolvedValue([{ id: 123, url: 'https://example.com' }]);
global.chrome.tabs.sendMessage.mockResolvedValue();
});
test('should complete full workflow from popup to content script', async () => {
let startButtonCallback;
mockPopupElements.startButton.addEventListener = jest.fn((event, callback) => {
if (event === 'click') startButtonCallback = callback;
});
// Simulate popup initialization
const popupScript = `
document.addEventListener('DOMContentLoaded', async () => {
const backgroundSelect = document.getElementById('background-select');
const clipboardToggle = document.getElementById('clipboard-toggle');
const startBtn = document.getElementById('start-selector');
// Load preferences
const saved = await chrome.storage.sync.get(['backgroundPreference', 'copyToClipboard']);
if (saved.backgroundPreference) {
backgroundSelect.value = saved.backgroundPreference;
}
clipboardToggle.checked = saved.copyToClipboard !== undefined ? saved.copyToClipboard : true;
// Start selector
startBtn.addEventListener('click', async () => {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
await chrome.tabs.sendMessage(tab.id, {
action: 'startSelector',
background: backgroundSelect.value,
copyToClipboard: clipboardToggle.checked
});
});
});
`;
// Execute popup initialization
await new Promise(resolve => {
global.document.addEventListener = jest.fn((event, callback) => {
if (event === 'DOMContentLoaded') {
callback();
resolve();
}
});
eval(popupScript);
});
// Verify preferences were loaded
expect(mockPopupElements.backgroundSelect.value).toBe('black');
expect(mockPopupElements.clipboardToggle.checked).toBe(true);
// Simulate start button click
await startButtonCallback();
// Verify message was sent to content script with correct parameters
expect(global.chrome.tabs.sendMessage).toHaveBeenCalledWith(123, {
action: 'startSelector',
background: 'black',
copyToClipboard: true
});
});
test('should handle preference changes and persist them', async () => {
let clipboardChangeCallback;
let backgroundChangeCallback;
mockPopupElements.clipboardToggle.addEventListener = jest.fn((event, callback) => {
if (event === 'change') clipboardChangeCallback = callback;
});
mockPopupElements.backgroundSelect.addEventListener = jest.fn((event, callback) => {
if (event === 'change') backgroundChangeCallback = callback;
});
// Initialize popup
await new Promise(resolve => {
global.document.addEventListener = jest.fn((event, callback) => {
if (event === 'DOMContentLoaded') {
callback();
resolve();
}
});
const popupScript = `
document.addEventListener('DOMContentLoaded', async () => {
const backgroundSelect = document.getElementById('background-select');
const clipboardToggle = document.getElementById('clipboard-toggle');
backgroundSelect.addEventListener('change', () => {
chrome.storage.sync.set({ backgroundPreference: backgroundSelect.value });
});
clipboardToggle.addEventListener('change', () => {
chrome.storage.sync.set({ copyToClipboard: clipboardToggle.checked });
});
});
`;
eval(popupScript);
});
// Change background preference
mockPopupElements.backgroundSelect.value = 'transparent';
backgroundChangeCallback();
expect(global.chrome.storage.sync.set).toHaveBeenCalledWith({ backgroundPreference: 'transparent' });
// Change clipboard preference
mockPopupElements.clipboardToggle.checked = false;
clipboardChangeCallback();
expect(global.chrome.storage.sync.set).toHaveBeenCalledWith({ copyToClipboard: false });
});
test('should handle incompatible pages gracefully', async () => {
// Mock incompatible page
global.chrome.tabs.query.mockResolvedValue([{
id: 123,
url: 'chrome://settings/'
}]);
let startButtonCallback;
mockPopupElements.startButton.addEventListener = jest.fn((event, callback) => {
if (event === 'click') startButtonCallback = callback;
});
// Initialize popup with incompatible page handling
const popupScript = `
document.addEventListener('DOMContentLoaded', async () => {
const startBtn = document.getElementById('start-selector');
const status = document.getElementById('status');
startBtn.addEventListener('click', async () => {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (tab.url.startsWith('chrome://') ||
tab.url.startsWith('chrome-extension://') ||
tab.url.startsWith('edge://') ||
tab.url.startsWith('about:')) {
status.textContent = 'Error: Cannot capture screenshots on this page type.';
status.className = 'status error';
status.style.display = 'block';
return;
}
// Normal flow would continue here
});
});
`;
await new Promise(resolve => {
global.document.addEventListener = jest.fn((event, callback) => {
if (event === 'DOMContentLoaded') {
callback();
resolve();
}
});
eval(popupScript);
});
// Simulate start button click on incompatible page
await startButtonCallback();
// Verify error message was shown
expect(mockPopupElements.status.textContent).toBe('Error: Cannot capture screenshots on this page type.');
expect(mockPopupElements.status.className).toBe('status error');
expect(mockPopupElements.status.style.display).toBe('block');
// Verify no message was sent to content script
expect(global.chrome.tabs.sendMessage).not.toHaveBeenCalled();
});
});

View file

@ -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
});
});
});

View file

@ -0,0 +1,641 @@
/**
* Comprehensive test suite for all combinations of save to PC and clipboard settings
* Tests all four combinations: both enabled, save-only, clipboard-only, both disabled
* Includes preference persistence testing across browser sessions
*/
// Mock Chrome APIs
global.chrome = {
storage: {
sync: {
get: jest.fn(),
set: jest.fn()
}
},
runtime: {
sendMessage: jest.fn(),
onMessage: {
addListener: jest.fn()
}
},
tabs: {
query: jest.fn(),
sendMessage: jest.fn()
}
};
// Mock DOM APIs
const mockCreateElement = jest.fn();
const mockLink = {
href: '',
download: '',
click: jest.fn(),
style: {},
classList: { add: jest.fn(), remove: jest.fn() }
};
global.document = {
createElement: mockCreateElement,
head: { appendChild: jest.fn() },
body: { appendChild: jest.fn() },
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
querySelectorAll: jest.fn(() => []),
getElementById: jest.fn()
};
global.window = {
close: jest.fn(),
setTimeout: jest.fn((fn) => fn()),
getComputedStyle: jest.fn(() => ({})),
devicePixelRatio: 2,
isSecureContext: true,
CSS: { escape: jest.fn(str => str) }
};
// Mock html2canvas
global.html2canvas = jest.fn();
// Mock ScreenshotSelector class for testing
class MockScreenshotSelector {
constructor() {
this.copyToClipboard = true;
this.saveToPc = true;
this.background = 'black';
this.isActive = false;
}
async init(background = 'black', copyToClipboard = true, saveToPc = true) {
this.background = background;
this.copyToClipboard = copyToClipboard;
this.saveToPc = saveToPc;
this.isActive = true;
}
checkClipboardAPIAvailability() {
if (!global.navigator.clipboard) {
return { available: false, reason: 'Clipboard API not available in this browser' };
}
if (!global.window.ClipboardItem) {
return { available: false, reason: 'ClipboardItem not supported in this browser' };
}
if (!global.window.isSecureContext) {
return { available: false, reason: 'Clipboard API requires a secure context (HTTPS)' };
}
if (!global.navigator.clipboard.write) {
return { available: false, reason: 'Clipboard write functionality not available' };
}
return { available: true };
}
async copyCanvasToClipboard(canvas) {
try {
const availability = this.checkClipboardAPIAvailability();
if (!availability.available) {
return { success: false, error: availability.reason, errorType: 'api_unavailable' };
}
const blob = await new Promise((resolve, reject) => {
canvas.toBlob((blob) => {
if (blob) resolve(blob);
else reject(new Error('Canvas to blob conversion returned null'));
}, 'image/png');
});
const clipboardItem = new global.window.ClipboardItem({ 'image/png': blob });
await global.navigator.clipboard.write([clipboardItem]);
return { success: true };
} catch (error) {
if (error.name === 'NotAllowedError') {
return { success: false, error: 'Permission denied', errorType: 'permission_denied' };
} else if (error.name === 'SecurityError') {
return { success: false, error: 'Security error', errorType: 'security_error' };
}
return { success: false, error: error.message, errorType: 'unexpected' };
}
}
async captureElement(element) {
// Mock canvas creation
const mockCanvas = {
toBlob: jest.fn((callback) => {
const mockBlob = new Blob(['mock data'], { type: 'image/png' });
callback(mockBlob);
}),
toDataURL: jest.fn(() => 'data:image/png;base64,mockdata')
};
// Mock html2canvas
global.html2canvas.mockResolvedValue(mockCanvas);
const canvas = await global.html2canvas(element);
// Conditionally download based on saveToPc setting
if (this.saveToPc) {
const link = global.document.createElement('a');
link.href = canvas.toDataURL('image/png');
link.download = `screenshot-${new Date().toISOString().slice(0, 19).replace(/[:.]/g, '-')}.png`;
link.click();
}
// Handle clipboard operations if enabled
let clipboardResult = null;
if (this.copyToClipboard) {
clipboardResult = await this.copyCanvasToClipboard(canvas);
}
return {
success: true,
downloadPerformed: this.saveToPc,
clipboardResult: clipboardResult
};
}
}
describe('Save to PC and Clipboard Combinations', () => {
let screenshotSelector;
let mockElement;
beforeEach(() => {
jest.clearAllMocks();
// Reset mocks
mockCreateElement.mockClear();
mockCreateElement.mockReturnValue(mockLink);
// Ensure our mock is used even if the test environment adjusted document
if (global.document) {
global.document.createElement = mockCreateElement;
}
mockLink.click.mockClear();
// Setup clipboard API mocks
global.navigator = {
clipboard: {
write: jest.fn().mockResolvedValue()
}
};
global.window.ClipboardItem = jest.fn();
global.window.isSecureContext = true;
screenshotSelector = new MockScreenshotSelector();
mockElement = {
classList: { remove: jest.fn() },
style: {},
getBoundingClientRect: () => ({ left: 0, top: 0, width: 100, height: 100 }),
scrollHeight: 100,
clientHeight: 100,
scrollWidth: 100,
clientWidth: 100
};
});
describe('Combination 1: Both Save to PC and Clipboard Enabled (saveToPc: true, clipboard: true)', () => {
beforeEach(() => {
screenshotSelector.saveToPc = true;
screenshotSelector.copyToClipboard = true;
});
test('should perform both download and clipboard operations when both are enabled', async () => {
const result = await screenshotSelector.captureElement(mockElement);
expect(result.success).toBe(true);
expect(result.downloadPerformed).toBe(true);
expect(result.clipboardResult).toBeDefined();
expect(result.clipboardResult.success).toBe(true);
// Verify download was triggered
expect(mockCreateElement).toHaveBeenCalledWith('a');
expect(mockLink.click).toHaveBeenCalled();
// Verify clipboard operation was attempted
expect(global.navigator.clipboard.write).toHaveBeenCalled();
});
test('should succeed with download even if clipboard fails when both are enabled', async () => {
// Mock clipboard failure
if (global.navigator.clipboard && global.navigator.clipboard.write) {
global.navigator.clipboard.write.mockRejectedValue(new DOMException('Permission denied', 'NotAllowedError'));
}
const result = await screenshotSelector.captureElement(mockElement);
expect(result.success).toBe(true);
expect(result.downloadPerformed).toBe(true);
expect(result.clipboardResult.success).toBe(false);
expect(result.clipboardResult.errorType).toBe('permission_denied');
// Download should still work
expect(mockCreateElement).toHaveBeenCalledWith('a');
expect(mockLink.click).toHaveBeenCalled();
});
test('should initialize with both settings correctly', async () => {
await screenshotSelector.init('white', true, true);
expect(screenshotSelector.background).toBe('white');
expect(screenshotSelector.copyToClipboard).toBe(true);
expect(screenshotSelector.saveToPc).toBe(true);
expect(screenshotSelector.isActive).toBe(true);
});
});
describe('Combination 2: Save-only Mode (saveToPc: true, clipboard: false)', () => {
beforeEach(() => {
screenshotSelector.saveToPc = true;
screenshotSelector.copyToClipboard = false;
});
test('should only perform download when save-only mode is enabled', async () => {
const result = await screenshotSelector.captureElement(mockElement);
expect(result.success).toBe(true);
expect(result.downloadPerformed).toBe(true);
expect(result.clipboardResult).toBeNull();
// Verify download was triggered
expect(mockCreateElement).toHaveBeenCalledWith('a');
expect(mockLink.click).toHaveBeenCalled();
// Verify clipboard operation was NOT attempted
expect(global.navigator.clipboard.write).not.toHaveBeenCalled();
});
test('should initialize with save-only settings correctly', async () => {
await screenshotSelector.init('transparent', false, true);
expect(screenshotSelector.background).toBe('transparent');
expect(screenshotSelector.copyToClipboard).toBe(false);
expect(screenshotSelector.saveToPc).toBe(true);
expect(screenshotSelector.isActive).toBe(true);
});
});
describe('Combination 3: Clipboard-only Mode (saveToPc: false, clipboard: true)', () => {
beforeEach(() => {
screenshotSelector.saveToPc = false;
screenshotSelector.copyToClipboard = true;
});
test('should only perform clipboard operation when clipboard-only mode is enabled', async () => {
const result = await screenshotSelector.captureElement(mockElement);
expect(result.success).toBe(true);
expect(result.downloadPerformed).toBe(false);
expect(result.clipboardResult).toBeDefined();
expect(result.clipboardResult.success).toBe(true);
// Verify download was NOT triggered
expect(mockCreateElement).not.toHaveBeenCalledWith('a');
// Verify clipboard operation was attempted
expect(global.navigator.clipboard.write).toHaveBeenCalled();
});
test('should initialize with clipboard-only settings correctly', async () => {
await screenshotSelector.init('black', true, false);
expect(screenshotSelector.background).toBe('black');
expect(screenshotSelector.copyToClipboard).toBe(true);
expect(screenshotSelector.saveToPc).toBe(false);
expect(screenshotSelector.isActive).toBe(true);
});
});
describe('Combination 4: Both Disabled (saveToPc: false, clipboard: false)', () => {
beforeEach(() => {
screenshotSelector.saveToPc = false;
screenshotSelector.copyToClipboard = false;
});
test('should perform neither operation when both are disabled', async () => {
const result = await screenshotSelector.captureElement(mockElement);
expect(result.success).toBe(true);
expect(result.downloadPerformed).toBe(false);
expect(result.clipboardResult).toBeNull();
// Verify download was NOT triggered
expect(mockCreateElement).not.toHaveBeenCalledWith('a');
// Verify clipboard operation was NOT attempted
expect(global.navigator.clipboard.write).not.toHaveBeenCalled();
});
test('should initialize with both disabled settings correctly', async () => {
await screenshotSelector.init('white', false, false);
expect(screenshotSelector.background).toBe('white');
expect(screenshotSelector.copyToClipboard).toBe(false);
expect(screenshotSelector.saveToPc).toBe(false);
expect(screenshotSelector.isActive).toBe(true);
});
test('should still create canvas even when both outputs are disabled', async () => {
await screenshotSelector.captureElement(mockElement);
// html2canvas should still be called (needed for potential future operations)
expect(global.html2canvas).toHaveBeenCalledWith(mockElement);
});
});
});
describe('Popup Validation Logic for All Combinations', () => {
let clipboardToggle;
let saveToPcToggle;
let startBtn;
let validationWarning;
let validateOutputMethods;
beforeEach(() => {
jest.clearAllMocks();
// Mock DOM elements
clipboardToggle = {
checked: true,
addEventListener: jest.fn()
};
saveToPcToggle = {
checked: true,
addEventListener: jest.fn()
};
startBtn = {
disabled: false,
setAttribute: jest.fn(),
addEventListener: jest.fn()
};
validationWarning = {
style: { display: 'none' }
};
global.document.getElementById = jest.fn((id) => {
switch (id) {
case 'clipboard-toggle': return clipboardToggle;
case 'save-to-pc-toggle': return saveToPcToggle;
case 'start-selector': return startBtn;
case 'validation-warning': return validationWarning;
default: return null;
}
});
// Define validation function
validateOutputMethods = function() {
const hasValidOutput = clipboardToggle.checked || saveToPcToggle.checked;
if (hasValidOutput) {
validationWarning.style.display = 'none';
startBtn.disabled = false;
startBtn.setAttribute('aria-describedby', '');
} else {
validationWarning.style.display = 'block';
startBtn.disabled = true;
startBtn.setAttribute('aria-describedby', 'validation-warning');
}
return hasValidOutput;
};
});
test('should validate successfully when both save to PC and clipboard are enabled', () => {
clipboardToggle.checked = true;
saveToPcToggle.checked = true;
const result = validateOutputMethods();
expect(result).toBe(true);
expect(validationWarning.style.display).toBe('none');
expect(startBtn.disabled).toBe(false);
expect(startBtn.setAttribute).toHaveBeenCalledWith('aria-describedby', '');
});
test('should validate successfully when only save to PC is enabled', () => {
clipboardToggle.checked = false;
saveToPcToggle.checked = true;
const result = validateOutputMethods();
expect(result).toBe(true);
expect(validationWarning.style.display).toBe('none');
expect(startBtn.disabled).toBe(false);
expect(startBtn.setAttribute).toHaveBeenCalledWith('aria-describedby', '');
});
test('should validate successfully when only clipboard is enabled', () => {
clipboardToggle.checked = true;
saveToPcToggle.checked = false;
const result = validateOutputMethods();
expect(result).toBe(true);
expect(validationWarning.style.display).toBe('none');
expect(startBtn.disabled).toBe(false);
expect(startBtn.setAttribute).toHaveBeenCalledWith('aria-describedby', '');
});
test('should fail validation when both save to PC and clipboard are disabled', () => {
clipboardToggle.checked = false;
saveToPcToggle.checked = false;
const result = validateOutputMethods();
expect(result).toBe(false);
expect(validationWarning.style.display).toBe('block');
expect(startBtn.disabled).toBe(true);
expect(startBtn.setAttribute).toHaveBeenCalledWith('aria-describedby', 'validation-warning');
});
});
describe('Preference Persistence Across Browser Sessions', () => {
let mockClipboardToggle;
let mockSaveToPcToggle;
beforeEach(() => {
jest.clearAllMocks();
// Mock DOM elements
mockClipboardToggle = {
checked: false,
addEventListener: jest.fn()
};
mockSaveToPcToggle = {
checked: false,
addEventListener: jest.fn()
};
global.document.getElementById = jest.fn((id) => {
switch (id) {
case 'clipboard-toggle':
return mockClipboardToggle;
case 'save-to-pc-toggle':
return mockSaveToPcToggle;
default:
return null;
}
});
});
test('should default to both enabled for new installations', async () => {
// Mock no saved preferences (new installation)
global.chrome.storage.sync.get.mockResolvedValueOnce({});
const popupScript = `
document.addEventListener('DOMContentLoaded', async () => {
const clipboardToggle = document.getElementById('clipboard-toggle');
const saveToPcToggle = document.getElementById('save-to-pc-toggle');
const saved = await chrome.storage.sync.get(['backgroundPreference', 'copyToClipboard', 'saveToPc']);
clipboardToggle.checked = saved.copyToClipboard !== undefined ? saved.copyToClipboard : true;
saveToPcToggle.checked = saved.saveToPc !== undefined ? saved.saveToPc : true;
});
`;
await new Promise(resolve => {
global.document.addEventListener = jest.fn((event, callback) => {
if (event === 'DOMContentLoaded') {
callback();
resolve();
}
});
eval(popupScript);
});
expect(mockClipboardToggle.checked).toBe(true); // Default
expect(mockSaveToPcToggle.checked).toBe(true); // Default
});
test('should persist save-only preferences across sessions', async () => {
// First session - user sets save-only mode
global.chrome.storage.sync.get.mockResolvedValueOnce({
copyToClipboard: false,
saveToPc: true
});
const popupScript = `
document.addEventListener('DOMContentLoaded', async () => {
const clipboardToggle = document.getElementById('clipboard-toggle');
const saveToPcToggle = document.getElementById('save-to-pc-toggle');
const saved = await chrome.storage.sync.get(['backgroundPreference', 'copyToClipboard', 'saveToPc']);
clipboardToggle.checked = saved.copyToClipboard !== undefined ? saved.copyToClipboard : true;
saveToPcToggle.checked = saved.saveToPc !== undefined ? saved.saveToPc : true;
});
`;
await new Promise(resolve => {
global.document.addEventListener = jest.fn((event, callback) => {
if (event === 'DOMContentLoaded') {
callback();
resolve();
}
});
eval(popupScript);
});
expect(mockClipboardToggle.checked).toBe(false);
expect(mockSaveToPcToggle.checked).toBe(true);
});
test('should persist clipboard-only preferences across sessions', async () => {
// Load clipboard-only mode
global.chrome.storage.sync.get.mockResolvedValueOnce({
copyToClipboard: true,
saveToPc: false
});
const popupScript = `
document.addEventListener('DOMContentLoaded', async () => {
const clipboardToggle = document.getElementById('clipboard-toggle');
const saveToPcToggle = document.getElementById('save-to-pc-toggle');
const saved = await chrome.storage.sync.get(['backgroundPreference', 'copyToClipboard', 'saveToPc']);
clipboardToggle.checked = saved.copyToClipboard !== undefined ? saved.copyToClipboard : true;
saveToPcToggle.checked = saved.saveToPc !== undefined ? saved.saveToPc : true;
});
`;
await new Promise(resolve => {
global.document.addEventListener = jest.fn((event, callback) => {
if (event === 'DOMContentLoaded') {
callback();
resolve();
}
});
eval(popupScript);
});
expect(mockClipboardToggle.checked).toBe(true);
expect(mockSaveToPcToggle.checked).toBe(false);
});
test('should persist both disabled preferences across sessions', async () => {
// Load both disabled state
global.chrome.storage.sync.get.mockResolvedValueOnce({
copyToClipboard: false,
saveToPc: false
});
const popupScript = `
document.addEventListener('DOMContentLoaded', async () => {
const clipboardToggle = document.getElementById('clipboard-toggle');
const saveToPcToggle = document.getElementById('save-to-pc-toggle');
const saved = await chrome.storage.sync.get(['backgroundPreference', 'copyToClipboard', 'saveToPc']);
clipboardToggle.checked = saved.copyToClipboard !== undefined ? saved.copyToClipboard : true;
saveToPcToggle.checked = saved.saveToPc !== undefined ? saved.saveToPc : true;
});
`;
await new Promise(resolve => {
global.document.addEventListener = jest.fn((event, callback) => {
if (event === 'DOMContentLoaded') {
callback();
resolve();
}
});
eval(popupScript);
});
expect(mockClipboardToggle.checked).toBe(false);
expect(mockSaveToPcToggle.checked).toBe(false);
// Validation should fail with both disabled
const isValid = mockClipboardToggle.checked || mockSaveToPcToggle.checked;
expect(isValid).toBe(false);
});
test('should maintain existing behavior when upgrading from version without saveToPc', async () => {
// Mock existing installation with only clipboard preference
global.chrome.storage.sync.get.mockResolvedValueOnce({
copyToClipboard: true
// saveToPc is undefined (not set in previous version)
});
const popupScript = `
document.addEventListener('DOMContentLoaded', async () => {
const clipboardToggle = document.getElementById('clipboard-toggle');
const saveToPcToggle = document.getElementById('save-to-pc-toggle');
const saved = await chrome.storage.sync.get(['backgroundPreference', 'copyToClipboard', 'saveToPc']);
clipboardToggle.checked = saved.copyToClipboard !== undefined ? saved.copyToClipboard : true;
saveToPcToggle.checked = saved.saveToPc !== undefined ? saved.saveToPc : true;
});
`;
await new Promise(resolve => {
global.document.addEventListener = jest.fn((event, callback) => {
if (event === 'DOMContentLoaded') {
callback();
resolve();
}
});
eval(popupScript);
});
expect(mockClipboardToggle.checked).toBe(true); // Existing preference
expect(mockSaveToPcToggle.checked).toBe(true); // Default for backward compatibility
});
});

297
tests/setup.js Normal file
View file

@ -0,0 +1,297 @@
/**
* Jest setup file for clipboard functionality tests
* Provides common mocks and utilities for all test files
*/
// Keep global and window navigator in sync regardless of direct reassignment
(() => {
try {
let navigatorStore = (typeof global !== 'undefined' && global.navigator) || (typeof window !== 'undefined' && window.navigator) || {};
if (typeof global !== 'undefined') {
Object.defineProperty(global, 'navigator', {
configurable: true,
get: () => navigatorStore,
set: (val) => {
navigatorStore = val || {};
if (typeof window !== 'undefined') {
try { window.navigator = navigatorStore; } catch (_) {}
}
}
});
}
if (typeof window !== 'undefined') {
Object.defineProperty(window, 'navigator', {
configurable: true,
get: () => navigatorStore,
set: (val) => { navigatorStore = val || {}; }
});
}
} catch (_) {}
})();
// Global test utilities
global.createMockBlob = (data = 'mock data', type = 'image/png', size = 1024) => {
const blob = new Blob([data], { type });
Object.defineProperty(blob, 'size', { value: size });
return blob;
};
global.createMockCanvas = (width = 100, height = 100) => {
return {
width,
height,
toBlob: jest.fn((callback, format = 'image/png') => {
const blob = global.createMockBlob('mock canvas data', format);
callback(blob);
}),
toDataURL: jest.fn((format = 'image/png') => `data:${format};base64,mockcanvasdata`),
getContext: jest.fn(() => ({
drawImage: jest.fn(),
getImageData: jest.fn(),
putImageData: jest.fn()
}))
};
};
global.createMockElement = (options = {}) => {
return {
classList: {
add: jest.fn(),
remove: jest.fn(),
contains: jest.fn(() => false)
},
style: {},
getBoundingClientRect: () => ({
left: options.left || 0,
top: options.top || 0,
width: options.width || 100,
height: options.height || 100
}),
scrollHeight: options.scrollHeight || 100,
clientHeight: options.clientHeight || 100,
scrollWidth: options.scrollWidth || 100,
clientWidth: options.clientWidth || 100,
children: options.children || [],
parentNode: { children: options.siblings || [] },
tagName: options.tagName || 'DIV',
id: options.id || '',
addEventListener: jest.fn(),
removeEventListener: jest.fn()
};
};
// Mock Chrome extension APIs consistently
global.mockChromeAPIs = () => {
const existingChrome = global.chrome || {};
const existingStorage = (existingChrome.storage && existingChrome.storage.sync) || {};
const existingRuntime = existingChrome.runtime || {};
const existingTabs = existingChrome.tabs || {};
global.chrome = {
storage: {
sync: {
get: existingStorage.get || jest.fn().mockResolvedValue({}),
set: existingStorage.set || jest.fn().mockResolvedValue()
}
},
runtime: {
sendMessage: existingRuntime.sendMessage || jest.fn().mockResolvedValue(),
onMessage: {
addListener: (existingRuntime.onMessage && existingRuntime.onMessage.addListener) || jest.fn()
}
},
tabs: {
query: existingTabs.query || jest.fn().mockResolvedValue([{ id: 123, url: 'https://example.com' }]),
sendMessage: existingTabs.sendMessage || jest.fn().mockResolvedValue()
}
};
};
// Mock DOM APIs consistently
global.mockDOMAPIs = () => {
// Document
const existingDocument = global.document || {};
const existingCreateElement = existingDocument.createElement;
global.document = existingDocument;
if (!global.document.createElement) {
global.document.createElement = jest.fn(() => ({
style: {},
classList: { add: jest.fn(), remove: jest.fn() },
appendChild: jest.fn(),
click: jest.fn(),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
innerHTML: '',
textContent: '',
id: '',
className: '',
checked: false,
value: ''
}));
}
if (!global.document.head) global.document.head = { appendChild: jest.fn() };
if (!global.document.body) global.document.body = { appendChild: jest.fn() };
if (!global.document.addEventListener) global.document.addEventListener = jest.fn();
if (!global.document.removeEventListener) global.document.removeEventListener = jest.fn();
if (!global.document.querySelectorAll) global.document.querySelectorAll = jest.fn(() => []);
if (!global.document.getElementById) global.document.getElementById = jest.fn(() => null);
// Window
const existingWindow = global.window || {};
global.window = existingWindow;
// Ensure a shared navigator reference exists early
if (!global.navigator) global.navigator = (existingWindow && existingWindow.navigator) || {};
if (!global.window.navigator) global.window.navigator = global.navigator;
if (!global.window.getComputedStyle) global.window.getComputedStyle = jest.fn(() => ({}));
if (typeof global.window.devicePixelRatio === 'undefined') global.window.devicePixelRatio = 2;
if (typeof global.window.isSecureContext === 'undefined') global.window.isSecureContext = true;
if (!global.window.CSS) global.window.CSS = { escape: jest.fn(str => str) };
if (!global.window.setTimeout) {
global.window.setTimeout = jest.fn((fn, delay) => {
if (delay === 0) fn();
return 1;
});
}
if (!global.window.clearTimeout) global.window.clearTimeout = jest.fn();
if (!global.window.close) global.window.close = jest.fn();
};
// Mock clipboard APIs with different scenarios
global.mockClipboardAPI = (scenario = 'supported') => {
switch (scenario) {
case 'supported': {
const existingNavigator = global.navigator || {};
const existingClipboard = existingNavigator.clipboard || {};
if (!existingClipboard.write || !jest.isMockFunction?.(existingClipboard.write)) {
existingClipboard.write = jest.fn().mockResolvedValue();
}
const mergedNavigator = { ...existingNavigator, clipboard: existingClipboard };
global.navigator = mergedNavigator;
if (!global.window) global.window = {};
global.window.navigator = mergedNavigator;
if (!global.window) global.window = {};
if (!global.window.ClipboardItem) global.window.ClipboardItem = jest.fn();
if (typeof global.window.isSecureContext === 'undefined') global.window.isSecureContext = true;
break;
}
case 'no-clipboard':
global.navigator = {};
if (!global.window) global.window = {};
global.window.navigator = global.navigator;
break;
case 'no-clipboarditem': {
const existingNavigator = global.navigator || {};
const clipboard = existingNavigator.clipboard || { write: jest.fn() };
const mergedNavigator = { ...existingNavigator, clipboard };
global.navigator = mergedNavigator;
if (!global.window) global.window = {};
global.window.navigator = mergedNavigator;
if (!global.window) global.window = {};
global.window.ClipboardItem = undefined;
break;
}
case 'insecure-context': {
const existingNavigator = global.navigator || {};
const clipboard = existingNavigator.clipboard || { write: jest.fn() };
const mergedNavigator = { ...existingNavigator, clipboard };
global.navigator = mergedNavigator;
if (!global.window) global.window = {};
global.window.navigator = mergedNavigator;
if (!global.window) global.window = {};
global.window.ClipboardItem = global.window.ClipboardItem || jest.fn();
global.window.isSecureContext = false;
break;
}
case 'permission-denied': {
const existingNavigator = global.navigator || {};
const clipboard = { write: jest.fn().mockRejectedValue(new DOMException('Permission denied', 'NotAllowedError')) };
const mergedNavigator = { ...existingNavigator, clipboard };
global.navigator = mergedNavigator;
if (!global.window) global.window = {};
global.window.navigator = mergedNavigator;
if (!global.window) global.window = {};
global.window.ClipboardItem = global.window.ClipboardItem || jest.fn();
global.window.isSecureContext = true;
break;
}
case 'security-error': {
const existingNavigator = global.navigator || {};
const clipboard = { write: jest.fn().mockRejectedValue(new DOMException('Security error', 'SecurityError')) };
const mergedNavigator = { ...existingNavigator, clipboard };
global.navigator = mergedNavigator;
if (!global.window) global.window = {};
global.window.navigator = mergedNavigator;
if (!global.window) global.window = {};
global.window.ClipboardItem = global.window.ClipboardItem || jest.fn();
global.window.isSecureContext = true;
break;
}
}
};
// Setup default mocks before each test
beforeEach(() => {
// Clear all mocks first
jest.clearAllMocks();
global.mockChromeAPIs();
global.mockDOMAPIs();
global.mockClipboardAPI('supported');
// Mock html2canvas
global.html2canvas = jest.fn().mockResolvedValue(global.createMockCanvas());
});
// Cleanup after each test
afterEach(() => {
jest.restoreAllMocks();
});
// Custom matchers for better test assertions
expect.extend({
toHaveBeenCalledWithClipboardMessage(received, expectedBackground, expectedClipboard) {
const calls = received.mock.calls;
const matchingCall = calls.find(call =>
call[1] &&
call[1].action === 'startSelector' &&
call[1].background === expectedBackground &&
call[1].copyToClipboard === expectedClipboard
);
if (matchingCall) {
return {
message: () => `Expected not to be called with clipboard message`,
pass: true
};
} else {
return {
message: () => `Expected to be called with clipboard message (background: ${expectedBackground}, clipboard: ${expectedClipboard})`,
pass: false
};
}
},
toHaveClipboardResult(received, expectedSuccess, expectedErrorType) {
if (!received.clipboardResult) {
return {
message: () => `Expected clipboard result to exist`,
pass: false
};
}
const success = received.clipboardResult.success === expectedSuccess;
const errorType = !expectedErrorType || received.clipboardResult.errorType === expectedErrorType;
if (success && errorType) {
return {
message: () => `Expected clipboard result not to match`,
pass: true
};
} else {
return {
message: () => `Expected clipboard result (success: ${expectedSuccess}, errorType: ${expectedErrorType}), got (success: ${received.clipboardResult.success}, errorType: ${received.clipboardResult.errorType})`,
pass: false
};
}
}
});

View file

@ -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);
});
});
});