screenshot-chrome-extension/tests/clipboard-functionality.test.js
Karthikeyan N a018c36d67
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>
2025-08-13 13:29:57 -06:00

480 lines
No EOL
16 KiB
JavaScript

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