Compare commits

..

1 commit

Author SHA1 Message Date
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
8 changed files with 197 additions and 1557 deletions

1
.gitignore vendored
View file

@ -1,4 +1,3 @@
node_modules/
.kiro/
html-to-image/
.vscode/

View file

@ -411,42 +411,90 @@ class ScreenshotSelector {
element.classList.remove('screenshot-highlight');
this.removeScrollableIndicators();
// Store original styles
const originalStyles = {
overflow: element.style.overflow,
overflowY: element.style.overflowY,
overflowX: element.style.overflowX,
height: element.style.height,
maxHeight: element.style.maxHeight,
position: element.style.position,
zIndex: element.style.zIndex,
};
// Temporarily modify element for full capture
element.style.overflow = 'visible';
element.style.overflowY = 'visible';
element.style.overflowX = 'visible';
element.style.height = `${element.scrollHeight}px`;
element.style.maxHeight = 'none';
// Wait for fonts and external resources to load
await this.waitForFontsAndResources(element);
if (!window.htmlToImage || typeof window.htmlToImage.toCanvas !== 'function') {
throw new Error('Screenshot renderer not available: htmlToImage.toCanvas() missing');
}
// Enhanced html2canvas configuration
const canvas = await html2canvas(element, {
useCORS: true,
allowTaint: false,
backgroundColor: this.background === 'transparent' ? null :
this.background === 'white' ? '#ffffff' : '#000000',
const backgroundColor =
this.background === 'transparent'
? undefined
: this.background === 'white'
? '#ffffff'
: '#000000';
// Improved rendering options
scale: window.devicePixelRatio || 1, // Use device pixel ratio for crisp images
logging: false, // Disable logging for cleaner console
// Use html-to-image to render the element to a canvas.
// Important: avoid mutating the live DOM during capture (it can cause reflow/layout drift).
const canvas = await window.htmlToImage.toCanvas(element, {
backgroundColor,
pixelRatio: window.devicePixelRatio || 1,
// Brightspace/D2L and many sites have CORS-protected stylesheets/fonts which cause noisy,
// expected DOMExceptions during CSS rule inspection. Keep logs clean by default.
logLevel: 'silent',
// If the page heavily caches fonts/images, enabling this can help but may slow capture
cacheBust: false,
// Capture full scrollable area without reflowing the live element
width: element.scrollWidth,
height: element.scrollHeight,
style: {
overflow: 'visible',
overflowX: 'visible',
overflowY: 'visible',
height: `${element.scrollHeight}px`,
maxHeight: 'none',
},
// Better handling of external resources
imageTimeout: 15000, // Wait longer for images to load
// Font handling and style preservation
onclone: (clonedDoc, clonedElementParam) => {
// Ensure all stylesheets are loaded in cloned document
const originalStyleSheets = Array.from(document.styleSheets);
const clonedHead = clonedDoc.head;
// Copy all stylesheets to cloned document
originalStyleSheets.forEach(styleSheet => {
try {
if (styleSheet.href) {
// External stylesheet
const link = clonedDoc.createElement('link');
link.rel = 'stylesheet';
link.href = styleSheet.href;
link.type = 'text/css';
clonedHead.appendChild(link);
} else if (styleSheet.ownerNode && styleSheet.ownerNode.tagName === 'STYLE') {
// Inline stylesheet
const style = clonedDoc.createElement('style');
style.type = 'text/css';
try {
const cssText = Array.from(styleSheet.cssRules).map(rule => rule.cssText).join('\n');
style.textContent = cssText;
} catch (e) {
// Fallback to original text content
style.textContent = styleSheet.ownerNode.textContent;
}
clonedHead.appendChild(style);
}
} catch (e) {
// Skip stylesheets that can't be accessed (CORS issues)
console.log('Skipped stylesheet due to CORS:', e);
}
});
// Apply computed styles to preserve appearance
const clonedElement = clonedElementParam;
const originalElement = element; // from outer scope
if (clonedElement && originalElement) {
this.preserveComputedStyles(clonedElement, originalElement);
}
return clonedDoc;
}
});
// Restore original styles
Object.assign(element.style, originalStyles);
// Conditionally download the image based on saveToPc preference
if (this.saveToPc) {
const link = document.createElement('a');
@ -549,7 +597,7 @@ class ScreenshotSelector {
}
if (error.message) {
if (error.message.includes('htmlToImage') || error.message.includes('html-to-image')) {
if (error.message.includes('html2canvas')) {
errorMessage = `Screenshot rendering failed. Try selecting a different element or refresh the page.${operationContext}`;
} else if (error.message.includes('timeout')) {
errorMessage = `Screenshot capture timed out. Try selecting a smaller area or simpler element.${operationContext}`;

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -24,7 +24,7 @@
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["html-to-image.js", "content.js"],
"js": ["html2canvas-pro.min.js", "content.js"],
"run_at": "document_idle"
}
],

View file

@ -53,10 +53,8 @@ global.window = {
})
};
// Mock html-to-image
global.htmlToImage = {
toCanvas: jest.fn(),
};
// Mock html2canvas
global.html2canvas = jest.fn();
describe('Integration Scenarios - Clipboard with Different Backgrounds', () => {
let screenshotSelector;
@ -104,7 +102,7 @@ describe('Integration Scenarios - Clipboard with Different Backgrounds', () => {
toDataURL: jest.fn(() => 'data:image/png;base64,mockdata')
};
global.htmlToImage.toCanvas = jest.fn().mockResolvedValue(mockCanvas);
global.html2canvas = jest.fn().mockResolvedValue(mockCanvas);
@ -185,17 +183,11 @@ describe('Integration Scenarios - Clipboard with Different Backgrounds', () => {
async captureElement(element) {
try {
// Generate canvas with background-specific settings
const backgroundColor =
this.background === 'transparent'
? undefined
: this.background === 'white'
? '#ffffff'
: '#000000';
const canvas = await global.htmlToImage.toCanvas(element, {
backgroundColor,
pixelRatio: global.window.devicePixelRatio || 1,
cacheBust: false
const canvas = await global.html2canvas(element, {
backgroundColor: this.background === 'transparent' ? null :
this.background === 'white' ? '#ffffff' : '#000000',
useCORS: true,
allowTaint: false
});
// Always download first
@ -269,8 +261,10 @@ describe('Integration Scenarios - Clipboard with Different Backgrounds', () => {
const result = await screenshotSelector.captureElement(mockElement);
expect(global.htmlToImage.toCanvas).toHaveBeenCalledWith(mockElement, expect.objectContaining({
backgroundColor: undefined, // Transparent background
expect(global.html2canvas).toHaveBeenCalledWith(mockElement, expect.objectContaining({
backgroundColor: null, // Transparent background
useCORS: true,
allowTaint: false
}));
expect(result.success).toBe(true);
@ -284,8 +278,10 @@ describe('Integration Scenarios - Clipboard with Different Backgrounds', () => {
const result = await screenshotSelector.captureElement(mockElement);
expect(global.htmlToImage.toCanvas).toHaveBeenCalledWith(mockElement, expect.objectContaining({
expect(global.html2canvas).toHaveBeenCalledWith(mockElement, expect.objectContaining({
backgroundColor: '#000000', // Black background
useCORS: true,
allowTaint: false
}));
expect(result.success).toBe(true);
@ -298,8 +294,10 @@ describe('Integration Scenarios - Clipboard with Different Backgrounds', () => {
const result = await screenshotSelector.captureElement(mockElement);
expect(global.htmlToImage.toCanvas).toHaveBeenCalledWith(mockElement, expect.objectContaining({
expect(global.html2canvas).toHaveBeenCalledWith(mockElement, expect.objectContaining({
backgroundColor: '#ffffff', // White background
useCORS: true,
allowTaint: false
}));
expect(result.success).toBe(true);

View file

@ -53,10 +53,8 @@ global.window = {
CSS: { escape: jest.fn(str => str) }
};
// Mock html-to-image
global.htmlToImage = {
toCanvas: jest.fn(),
};
// Mock html2canvas
global.html2canvas = jest.fn();
// Mock ScreenshotSelector class for testing
class MockScreenshotSelector {
@ -128,18 +126,10 @@ class MockScreenshotSelector {
toDataURL: jest.fn(() => 'data:image/png;base64,mockdata')
};
// Mock html-to-image
global.htmlToImage.toCanvas.mockResolvedValue(mockCanvas);
// Mock html2canvas
global.html2canvas.mockResolvedValue(mockCanvas);
const canvas = await global.htmlToImage.toCanvas(element, {
backgroundColor: this.background === 'transparent'
? undefined
: this.background === 'white'
? '#ffffff'
: '#000000',
pixelRatio: global.window.devicePixelRatio || 1,
cacheBust: false
});
const canvas = await global.html2canvas(element);
// Conditionally download based on saveToPc setting
if (this.saveToPc) {
@ -344,11 +334,8 @@ describe('Save to PC and Clipboard Combinations', () => {
test('should still create canvas even when both outputs are disabled', async () => {
await screenshotSelector.captureElement(mockElement);
// html-to-image should still be called (needed for potential future operations)
expect(global.htmlToImage.toCanvas).toHaveBeenCalledWith(
mockElement,
expect.any(Object)
);
// html2canvas should still be called (needed for potential future operations)
expect(global.html2canvas).toHaveBeenCalledWith(mockElement);
});
});
});

View file

@ -238,10 +238,8 @@ beforeEach(() => {
global.mockDOMAPIs();
global.mockClipboardAPI('supported');
// Mock html-to-image
global.htmlToImage = {
toCanvas: jest.fn().mockResolvedValue(global.createMockCanvas()),
};
// Mock html2canvas
global.html2canvas = jest.fn().mockResolvedValue(global.createMockCanvas());
});
// Cleanup after each test