From dd466188e1d745a3bdf618532e1976f4a5d09e11 Mon Sep 17 00:00:00 2001 From: Francisco Moreno Date: Wed, 28 Oct 2026 09:15:19 +0100 Subject: [PATCH 1/3] chore: add ESLint, Prettier and git hooks (Husky, lint-staged) Pre-commit runs lint-staged and Jest, plus release:check when manifest.json is staged. Pre-push runs the headless E2E suite when runtime files changed. A Claude Code PreToolUse hook stops agents from bypassing the hooks. Co-Authored-By: [PERSON_NAME] Sonnet 5.5 <[EMAIL]> --- .claude/settings.json | 15 +- .husky/pre-commit | 5 + .husky/pre-push | 1 + .prettierignore | 9 + .prettierrc.json | 7 + HTMLReport/modules/reportEvents.js | 148 ++-- background.js | 1114 ++++++++++++++------------ eslint.config.js | 69 ++ js/popup.js | 673 +++++++++------- package-lock.json | 1039 +++++++++++++++++++++++- package.json | 30 +- playwright.config.js | 11 +- scripts/claude-guard.js | 26 + scripts/claude-guard.test.js | 26 + scripts/pre-push.js | 40 + scripts/release-check.js | 103 +++ test/e2e/annotation-editor.spec.js | 35 +- test/e2e/basic-functionality.spec.js | 18 +- test/spec/ExportSessionCSV.test.js | 302 +++---- test/spec/Session.test.js | 490 ++++++----- 20 files changed, 2827 insertions(+), 1334 deletions(-) create mode 100644 .husky/pre-commit create mode 100644 .husky/pre-push create mode 100644 .prettierignore create mode 100644 .prettierrc.json create mode 100644 eslint.config.js create mode 100644 scripts/claude-guard.js create mode 100644 scripts/claude-guard.test.js create mode 100644 scripts/pre-push.js create mode 100644 scripts/release-check.js diff --git a/.claude/settings.json b/.claude/settings.json index 5266dfd..001ec9d 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -1,7 +1,18 @@ { "permissions": { - "allow": [ - "Bash(npx playwright:*)" + "allow": ["Bash(npx playwright:*)"] + }, + "hooks": { + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "node \"$CLAUDE_PROJECT_DIR/scripts/claude-guard.js\"" + } + ] + } ] } } diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..8528ac3 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,5 @@ +npx lint-staged +npm test +if git diff --cached --name-only | grep -qx "manifest.json"; then + npm run release:check +fi diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100644 index 0000000..5de9c79 --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1 @@ +node scripts/pre-push.js diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..8b1fa99 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,9 @@ +lib/ +node_modules/ +playwright-report/ +test-results/ +coverage/ +*.zip +*.min.js +HTMLReport/TableFilter/ +package-lock.json diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..c93aaff --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,7 @@ +{ + "singleQuote": true, + "semi": true, + "printWidth": 100, + "trailingComma": "es5", + "endOfLine": "auto" +} diff --git a/HTMLReport/modules/reportEvents.js b/HTMLReport/modules/reportEvents.js index fef17a0..415ac2f 100644 --- a/HTMLReport/modules/reportEvents.js +++ b/HTMLReport/modules/reportEvents.js @@ -9,121 +9,121 @@ let currentFilter = 'all'; * Returns the current active filter type. */ export function getCurrentFilter() { - return currentFilter; + return currentFilter; } /** * Sets up all interactive event listeners for the report. */ export function setupAllListeners(session) { - setupFilterListeners(session); - setupDeleteListeners(); - setupImagePreviewListeners(); - setupDownloadListener(session); + setupFilterListeners(session); + setupDeleteListeners(); + setupImagePreviewListeners(); + setupDownloadListener(session); } /** * Re-binds row-level listeners after table re-render. */ export function rebindTableListeners() { - setupImagePreviewListeners(); + setupImagePreviewListeners(); } function setupFilterListeners(session) { - document.querySelectorAll('.filter-pill').forEach(button => { - button.addEventListener('click', function () { - document.querySelectorAll('.filter-pill').forEach(btn => btn.classList.remove('active')); - this.classList.add('active'); - currentFilter = this.dataset.type; - displayAnnotationsTable(session, currentFilter); - rebindTableListeners(); - }); + document.querySelectorAll('.filter-pill').forEach((button) => { + button.addEventListener('click', function () { + document.querySelectorAll('.filter-pill').forEach((btn) => btn.classList.remove('active')); + this.classList.add('active'); + currentFilter = this.dataset.type; + displayAnnotationsTable(session, currentFilter); + rebindTableListeners(); }); + }); } function setupDeleteListeners() { - // Delegate click on delete buttons - document.getElementById('annotationsTableBody').addEventListener('click', (e) => { - const btn = e.target.closest('.delete-btn'); - if (!btn) return; - annotationToDelete = parseInt(btn.dataset.index); - document.getElementById('divOverlay').style.display = 'block'; - }); - - document.getElementById('cancelDelete').addEventListener('click', () => { - document.getElementById('divOverlay').style.display = 'none'; - annotationToDelete = null; - }); - - document.getElementById('deleteYes').addEventListener('click', async () => { - if (annotationToDelete === null) return; - const response = await deleteAnnotation(annotationToDelete); - document.getElementById('divOverlay').style.display = 'none'; - annotationToDelete = null; - // Reload to resync regardless of response - location.reload(); - }); + // Delegate click on delete buttons + document.getElementById('annotationsTableBody').addEventListener('click', (e) => { + const btn = e.target.closest('.delete-btn'); + if (!btn) return; + annotationToDelete = parseInt(btn.dataset.index); + document.getElementById('divOverlay').style.display = 'block'; + }); + + document.getElementById('cancelDelete').addEventListener('click', () => { + document.getElementById('divOverlay').style.display = 'none'; + annotationToDelete = null; + }); + + document.getElementById('deleteYes').addEventListener('click', async () => { + if (annotationToDelete === null) return; + await deleteAnnotation(annotationToDelete); + document.getElementById('divOverlay').style.display = 'none'; + annotationToDelete = null; + // Reload to resync regardless of response + location.reload(); + }); } function setupImagePreviewListeners() { - document.querySelectorAll('.preview-image').forEach(img => { - img.addEventListener('click', () => showImagePreview(img.dataset.preview)); - img.addEventListener('mouseenter', (e) => showHoverPreview(img.dataset.preview, e)); - img.addEventListener('mousemove', updateHoverPosition); - img.addEventListener('mouseleave', hideHoverPreview); - }); + document.querySelectorAll('.preview-image').forEach((img) => { + img.addEventListener('click', () => showImagePreview(img.dataset.preview)); + img.addEventListener('mouseenter', (e) => showHoverPreview(img.dataset.preview, e)); + img.addEventListener('mousemove', updateHoverPosition); + img.addEventListener('mouseleave', hideHoverPreview); + }); } function setupDownloadListener(session) { - document.getElementById('downloadReportBtn').addEventListener('click', () => { - downloadCompleteReport(session); - }); + document.getElementById('downloadReportBtn').addEventListener('click', () => { + downloadCompleteReport(session); + }); - document.getElementById('downloadImagesBtn').addEventListener('click', () => { - downloadAllImages(session); - }); + document.getElementById('downloadImagesBtn').addEventListener('click', () => { + downloadAllImages(session); + }); } // --- Image Preview --- function showImagePreview(src) { - const preview = document.getElementById('imagePreview'); - const previewImg = preview.querySelector('img'); - previewImg.src = src; - preview.classList.add('active'); - - const closePreview = () => { - preview.classList.remove('active'); - preview.removeEventListener('click', closePreview); - }; - preview.addEventListener('click', closePreview); - previewImg.addEventListener('click', (e) => e.stopPropagation()); + const preview = document.getElementById('imagePreview'); + const previewImg = preview.querySelector('img'); + previewImg.src = src; + preview.classList.add('active'); + + const closePreview = () => { + preview.classList.remove('active'); + preview.removeEventListener('click', closePreview); + }; + preview.addEventListener('click', closePreview); + previewImg.addEventListener('click', (e) => e.stopPropagation()); } function showHoverPreview(src, event) { - const preview = document.getElementById('imageHoverPreview'); - preview.querySelector('img').src = src; - preview.classList.add('active'); - updateHoverPosition(event); + const preview = document.getElementById('imageHoverPreview'); + preview.querySelector('img').src = src; + preview.classList.add('active'); + updateHoverPosition(event); } function updateHoverPosition(event) { - const preview = document.getElementById('imageHoverPreview'); - if (!preview.classList.contains('active')) return; + const preview = document.getElementById('imageHoverPreview'); + if (!preview.classList.contains('active')) return; - const offset = 15; - const pw = preview.offsetWidth; - const ph = preview.offsetHeight; - let left = event.clientX + offset; - let top = event.clientY + offset; + const offset = 15; + const pw = preview.offsetWidth; + const ph = preview.offsetHeight; + let left = event.clientX + offset; + let top = event.clientY + offset; - if (left + pw > window.innerWidth) left = event.clientX - pw - offset; - if (top + ph > window.innerHeight) top = event.clientY - ph - offset; + if (left + pw > window.innerWidth) left = event.clientX - pw - offset; + if (top + ph > window.innerHeight) top = event.clientY - ph - offset; - preview.style.left = left + 'px'; - preview.style.top = top + 'px'; + preview.style.left = left + 'px'; + preview.style.top = top + 'px'; } function hideHoverPreview() { - document.getElementById('imageHoverPreview').classList.remove('active'); + document.getElementById('imageHoverPreview').classList.remove('active'); } diff --git a/background.js b/background.js index 7cc7b40..4694bda 100644 --- a/background.js +++ b/background.js @@ -12,580 +12,676 @@ let session = new Session(); // Función para guardar la sesión en el storage async function saveSession() { - try { - await chrome.storage.local.set({ 'session': session }); - } catch (error) { - if (error.name === 'QUOTA_BYTES' || (chrome.runtime.lastError && chrome.runtime.lastError.message && chrome.runtime.lastError.message.includes('QUOTA_BYTES'))) { - console.error('Error saving session due to quota limit:', error); - - // Create a deep copy of the session object - const sessionCopy = JSON.parse(JSON.stringify(session)); - - // Find the oldest annotation with a non-null imageURL - let oldestAnnotationIndex = -1; - for (let i = 0; i < sessionCopy.annotations.length; i++) { - if (sessionCopy.annotations[i].imageURL) { - oldestAnnotationIndex = i; - break; - } - } - - if (oldestAnnotationIndex !== -1) { - // const originalImageURL = sessionCopy.annotations[oldestAnnotationIndex].imageURL; // Optional: for logging - sessionCopy.annotations[oldestAnnotationIndex].imageURL = "IMAGE_REMOVED_DUE_TO_STORAGE_LIMIT"; - - try { - await chrome.storage.local.set({ 'session': sessionCopy }); - // Notify user about successful save after removing screenshot - const notifId = 'sessionSavedAfterQuota-' + Date.now(); - chrome.notifications.create(notifId, { - type: 'basic', - iconUrl: 'icons/iconbig.png', - title: 'Session Saved with Adjustment', - message: 'The oldest screenshot was removed to save the session due to storage limits.' - }); - setTimeout(() => { chrome.notifications.clear(notifId); }, 7000); - // Update the main session object to reflect the change if the save was successful. - session.annotations[oldestAnnotationIndex].imageURL = "IMAGE_REMOVED_DUE_TO_STORAGE_LIMIT"; - - } catch (secondError) { - console.error('Error saving session even after removing screenshot:', secondError); - // Notify user about failed save even after removing screenshot - const notifId = 'sessionSaveFailedAfterQuota-' + Date.now(); - chrome.notifications.create(notifId, { - type: 'basic', - iconUrl: 'icons/iconbig.png', - title: 'Session Save Failed', - message: 'Failed to save session. Insufficient storage even after removing the oldest screenshot.' - }); - setTimeout(() => { chrome.notifications.clear(notifId); }, 7000); - } - } else { - // No annotation with imageURL found - console.error('Failed to save session. No screenshots to remove for quota.'); - const notifId = 'sessionSaveFailedNoScreenshot-' + Date.now(); - chrome.notifications.create(notifId, { - type: 'basic', - iconUrl: 'icons/iconbig.png', - title: 'Session Save Failed', - message: 'Failed to save session. Insufficient storage and no screenshots to remove.' - }); - setTimeout(() => { chrome.notifications.clear(notifId); }, 7000); - } - } else { - // Not a quota error, re-throw or handle as appropriate - console.error('Error saving session:', error); - throw error; + try { + await chrome.storage.local.set({ session: session }); + } catch (error) { + if ( + error.name === 'QUOTA_BYTES' || + (chrome.runtime.lastError && + chrome.runtime.lastError.message && + chrome.runtime.lastError.message.includes('QUOTA_BYTES')) + ) { + console.error('Error saving session due to quota limit:', error); + + // Create a deep copy of the session object + const sessionCopy = JSON.parse(JSON.stringify(session)); + + // Find the oldest annotation with a non-null imageURL + let oldestAnnotationIndex = -1; + for (let i = 0; i < sessionCopy.annotations.length; i++) { + if (sessionCopy.annotations[i].imageURL) { + oldestAnnotationIndex = i; + break; + } + } + + if (oldestAnnotationIndex !== -1) { + // const originalImageURL = sessionCopy.annotations[oldestAnnotationIndex].imageURL; // Optional: for logging + sessionCopy.annotations[oldestAnnotationIndex].imageURL = + 'IMAGE_REMOVED_DUE_TO_STORAGE_LIMIT'; + + try { + await chrome.storage.local.set({ session: sessionCopy }); + // Notify user about successful save after removing screenshot + const notifId = 'sessionSavedAfterQuota-' + Date.now(); + chrome.notifications.create(notifId, { + type: 'basic', + iconUrl: 'icons/iconbig.png', + title: 'Session Saved with Adjustment', + message: 'The oldest screenshot was removed to save the session due to storage limits.', + }); + setTimeout(() => { + chrome.notifications.clear(notifId); + }, 7000); + // Update the main session object to reflect the change if the save was successful. + session.annotations[oldestAnnotationIndex].imageURL = + 'IMAGE_REMOVED_DUE_TO_STORAGE_LIMIT'; + } catch (secondError) { + console.error('Error saving session even after removing screenshot:', secondError); + // Notify user about failed save even after removing screenshot + const notifId = 'sessionSaveFailedAfterQuota-' + Date.now(); + chrome.notifications.create(notifId, { + type: 'basic', + iconUrl: 'icons/iconbig.png', + title: 'Session Save Failed', + message: + 'Failed to save session. Insufficient storage even after removing the oldest screenshot.', + }); + setTimeout(() => { + chrome.notifications.clear(notifId); + }, 7000); } + } else { + // No annotation with imageURL found + console.error('Failed to save session. No screenshots to remove for quota.'); + const notifId = 'sessionSaveFailedNoScreenshot-' + Date.now(); + chrome.notifications.create(notifId, { + type: 'basic', + iconUrl: 'icons/iconbig.png', + title: 'Session Save Failed', + message: 'Failed to save session. Insufficient storage and no screenshots to remove.', + }); + setTimeout(() => { + chrome.notifications.clear(notifId); + }, 7000); + } + } else { + // Not a quota error, re-throw or handle as appropriate + console.error('Error saving session:', error); + throw error; } + } } // Función para cargar la sesión desde el storage async function loadSession() { - const data = await chrome.storage.local.get('session'); - if (data.session) { - // Reconstruir el objeto Session con sus métodos - const loadedSession = data.session; - session = new Session(loadedSession.startDateTime, loadedSession.browserInfo); - - // Reconstruir las anotaciones - loadedSession.annotations.forEach(annotation => { - let newAnnotation; - switch (annotation.type) { - case "Bug": - newAnnotation = new Bug(annotation.name, annotation.url, annotation.timestamp, annotation.imageURL); - session.addBug(newAnnotation); - break; - case "Note": - newAnnotation = new Note(annotation.name, annotation.url, annotation.timestamp, annotation.imageURL); - session.addNote(newAnnotation); - break; - case "Idea": - newAnnotation = new Idea(annotation.name, annotation.url, annotation.timestamp, annotation.imageURL); - session.addIdea(newAnnotation); - break; - case "Question": - newAnnotation = new Question(annotation.name, annotation.url, annotation.timestamp, annotation.imageURL); - session.addQuestion(newAnnotation); - break; - } - }); - } + const data = await chrome.storage.local.get('session'); + if (data.session) { + // Reconstruir el objeto Session con sus métodos + const loadedSession = data.session; + session = new Session(loadedSession.startDateTime, loadedSession.browserInfo); + + // Reconstruir las anotaciones + loadedSession.annotations.forEach((annotation) => { + let newAnnotation; + switch (annotation.type) { + case 'Bug': + newAnnotation = new Bug( + annotation.name, + annotation.url, + annotation.timestamp, + annotation.imageURL + ); + session.addBug(newAnnotation); + break; + case 'Note': + newAnnotation = new Note( + annotation.name, + annotation.url, + annotation.timestamp, + annotation.imageURL + ); + session.addNote(newAnnotation); + break; + case 'Idea': + newAnnotation = new Idea( + annotation.name, + annotation.url, + annotation.timestamp, + annotation.imageURL + ); + session.addIdea(newAnnotation); + break; + case 'Question': + newAnnotation = new Question( + annotation.name, + annotation.url, + annotation.timestamp, + annotation.imageURL + ); + session.addQuestion(newAnnotation); + break; + } + }); + } } // Cargar la sesión al iniciar loadSession(); // Helper function for notifications of processing errors (before addAnnotation is called) -function notifyProcessingError(annotationType, descriptionName, errorMessage = "") { - const typeStr = annotationType || "Annotation"; - const nameStr = descriptionName || ""; - const notifId = 'annotationProcessingError-' + Date.now(); - chrome.notifications.create(notifId, { - type: 'basic', - iconUrl: 'icons/iconbig.png', - title: `${typeStr} Processing Failed`, - message: `Could not process ${typeStr.toLowerCase()} "${nameStr}" for screenshot. Error: ${errorMessage}` - }); - setTimeout(() => { chrome.notifications.clear(notifId); }, 7000); +// eslint-disable-next-line no-unused-vars +function notifyProcessingError(annotationType, descriptionName, errorMessage = '') { + const typeStr = annotationType || 'Annotation'; + const nameStr = descriptionName || ''; + const notifId = 'annotationProcessingError-' + Date.now(); + chrome.notifications.create(notifId, { + type: 'basic', + iconUrl: 'icons/iconbig.png', + title: `${typeStr} Processing Failed`, + message: `Could not process ${typeStr.toLowerCase()} "${nameStr}" for screenshot. Error: ${errorMessage}`, + }); + setTimeout(() => { + chrome.notifications.clear(notifId); + }, 7000); } - // Escuchar mensajes del popup chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { - // Keep 'return true' if any path in this listener might call sendResponse asynchronously. - // For "csToBgCropData", we are not sending a response back to content script currently. - // For other existing cases, they do use sendResponse. - let isAsync = false; - - switch (request.type) { - case "initiateCropSelection": - // Forward the crop selection request to the content script - chrome.tabs.query({ active: true, currentWindow: true }, async function(tabs) { - if (tabs && tabs[0] && tabs[0].id != null) { - const tab = tabs[0]; - - // Check if the page allows content scripts - if (tab.url.startsWith('chrome://') || tab.url.startsWith('edge://') || - tab.url.startsWith('chrome-extension://') || tab.url.startsWith('about:')) { - const notifId = 'cropSelectionError-' + Date.now(); - chrome.notifications.create(notifId, { - type: 'basic', - iconUrl: 'icons/iconbig.png', - title: 'Selection Not Available', - message: 'Screen selection cannot be used on this type of page. Please try on a regular webpage.' - }); - setTimeout(() => { chrome.notifications.clear(notifId); }, 5000); - return; - } - - try { - // Try to inject content script if it's not already loaded - await chrome.scripting.executeScript({ - target: { tabId: tab.id }, - files: ['js/content_script.js'] - }).catch(() => { - // Content script might already be loaded, ignore error - console.log("Background: Content script might already be loaded"); - }); - - // Small delay to ensure content script is ready - setTimeout(() => { - chrome.tabs.sendMessage(tab.id, { - type: "startSelection", - annotationType: request.annotationType, - description: request.description - }, function(response) { - if (chrome.runtime.lastError) { - console.error("Background: Error sending startSelection to content script:", chrome.runtime.lastError.message); - const notifId = 'cropSelectionError-' + Date.now(); - chrome.notifications.create(notifId, { - type: 'basic', - iconUrl: 'icons/iconbig.png', - title: 'Selection Failed', - message: 'Could not start screen selection. Please try again.' - }); - setTimeout(() => { chrome.notifications.clear(notifId); }, 5000); - } else { - console.log("Background: Successfully initiated crop selection on content script"); - } - }); - }, 100); - - } catch (error) { - console.error("Background: Error injecting content script:", error); - const notifId = 'cropSelectionError-' + Date.now(); - chrome.notifications.create(notifId, { - type: 'basic', - iconUrl: 'icons/iconbig.png', - title: 'Selection Failed', - message: 'Could not start screen selection. Error: ' + error.message - }); - setTimeout(() => { chrome.notifications.clear(notifId); }, 5000); - } - } else { - console.error("Background: No active tab found for crop selection"); - } + // Keep 'return true' if any path in this listener might call sendResponse asynchronously. + // For "csToBgCropData", we are not sending a response back to content script currently. + // For other existing cases, they do use sendResponse. + let isAsync = false; + + switch (request.type) { + case 'initiateCropSelection': + // Forward the crop selection request to the content script + chrome.tabs.query({ active: true, currentWindow: true }, async function (tabs) { + if (tabs && tabs[0] && tabs[0].id != null) { + const tab = tabs[0]; + + // Check if the page allows content scripts + if ( + tab.url.startsWith('chrome://') || + tab.url.startsWith('edge://') || + tab.url.startsWith('chrome-extension://') || + tab.url.startsWith('about:') + ) { + const notifId = 'cropSelectionError-' + Date.now(); + chrome.notifications.create(notifId, { + type: 'basic', + iconUrl: 'icons/iconbig.png', + title: 'Selection Not Available', + message: + 'Screen selection cannot be used on this type of page. Please try on a regular webpage.', }); - break; - case "addBug": - console.log("Background: Received message", request.type, ". Name:", request.name, ". imageURL (first 100 chars):", request.imageURL ? request.imageURL.substring(0, 100) : "null"); - addAnnotation("Bug", request.name, request.imageURL) - .then(() => sendResponse({ status: "ok" })) - .catch(error => sendResponse({ status: "error", error: error.message })); - isAsync = true; - break; - case "addIdea": - console.log("Background: Received message", request.type, ". Name:", request.name, ". imageURL (first 100 chars):", request.imageURL ? request.imageURL.substring(0, 100) : "null"); - addAnnotation("Idea", request.name, request.imageURL) - .then(() => sendResponse({ status: "ok" })) - .catch(error => sendResponse({ status: "error", error: error.message })); - isAsync = true; - break; - case "addNote": - console.log("Background: Received message", request.type, ". Name:", request.name, ". imageURL (first 100 chars):", request.imageURL ? request.imageURL.substring(0, 100) : "null"); - addAnnotation("Note", request.name, request.imageURL) - .then(() => sendResponse({ status: "ok" })) - .catch(error => sendResponse({ status: "error", error: error.message })); - isAsync = true; - break; - case "addQuestion": - console.log("Background: Received message", request.type, ". Name:", request.name, ". imageURL (first 100 chars):", request.imageURL ? request.imageURL.substring(0, 100) : "null"); - addAnnotation("Question", request.name, request.imageURL) - .then(() => sendResponse({ status: "ok" })) - .catch(error => sendResponse({ status: "error", error: error.message })); - isAsync = true; - break; - case "requestCropScreenshot": - console.log("Background: Received requestCropScreenshot", request); - handleCropScreenshotRequest(request) - .then((croppedImageData) => { - sendResponse({ croppedImageData: croppedImageData }); - }) - .catch((error) => { - console.error("Background: Failed to capture crop screenshot:", error); - sendResponse({ croppedImageData: null }); - }); - isAsync = true; - break; - case "csToBgCropData": - console.log("Background: Received csToBgCropData", request); - handleProcessAnnotatedCrop(request) - .then(() => { - console.log("Background: Annotated crop processed successfully"); - }) - .catch((error) => { - console.error("Background: Failed to process annotated crop:", error); - }); - break; - case "updateAnnotationName": - var AnnotationID = request.annotationID; - var newName = request.newName; - var annotations = session.getAnnotations(); - var annotation = annotations[AnnotationID]; - annotation.setName(newName); - saveSession().then(() => sendResponse({ status: "ok" })); - break; - case "deleteAnnotation": - session.deleteAnnotation(request.annotationID); - saveSession().then(() => sendResponse({ status: "ok" })); - break; - case "exportSessionCSV": - if (!exportSessionCSV()) { - sendResponse({ status: "nothing to export" }); - } else { - sendResponse({ status: "ok" }); - } - break; - case "exportSessionJSon": - if (!exportSessionJSon()) { - sendResponse({ status: "nothing to export" }); - } else { - sendResponse({ status: "ok" }); - } - break; - case "importSessionJSon": - var fileData = request.jSonSession; - if (!importSessionJSon(fileData)) { - sendResponse({ status: "nothing to import" }); - } else { - sendResponse({ status: "ok" }); - } - break; - case "clearSession": - clearSession().then(() => sendResponse({ status: "ok" })); - break; - case "getSessionData": - sendResponse({ - bugs: session.getBugs().length, - notes: session.getNotes().length, - ideas: session.getIdeas().length, - questions: session.getQuestions().length, - annotationsCount: session.getAnnotations().length - }); - break; - case "getFullSession": - if (!session) { - sendResponse(null); - return true; - } - sendResponse({ - startDateTime: session.StartDateTime, - browserInfo: { - browser: session.BrowserInfo.browser || "Chrome", - browserVersion: session.BrowserInfo.browserVersion || chrome.runtime.getManifest().version, - os: session.BrowserInfo.os || navigator.platform, - osVersion: session.BrowserInfo.osVersion || navigator.userAgent, - cookies: session.BrowserInfo.cookies || navigator.cookieEnabled, - flashVersion: session.BrowserInfo.flashVersion || "N/A" + setTimeout(() => { + chrome.notifications.clear(notifId); + }, 5000); + return; + } + + try { + // Try to inject content script if it's not already loaded + await chrome.scripting + .executeScript({ + target: { tabId: tab.id }, + files: ['js/content_script.js'], + }) + .catch(() => { + // Content script might already be loaded, ignore error + console.log('Background: Content script might already be loaded'); + }); + + // Small delay to ensure content script is ready + setTimeout(() => { + chrome.tabs.sendMessage( + tab.id, + { + type: 'startSelection', + annotationType: request.annotationType, + description: request.description, }, - annotations: session.annotations.map(annotation => ({ - type: annotation.constructor.name, - name: annotation.name, - url: annotation.url, - timestamp: annotation.timestamp, - imageURL: annotation.imageURL - })) + function (response) { + if (chrome.runtime.lastError) { + console.error( + 'Background: Error sending startSelection to content script:', + chrome.runtime.lastError.message + ); + const notifId = 'cropSelectionError-' + Date.now(); + chrome.notifications.create(notifId, { + type: 'basic', + iconUrl: 'icons/iconbig.png', + title: 'Selection Failed', + message: 'Could not start screen selection. Please try again.', + }); + setTimeout(() => { + chrome.notifications.clear(notifId); + }, 5000); + } else { + console.log( + 'Background: Successfully initiated crop selection on content script' + ); + } + } + ); + }, 100); + } catch (error) { + console.error('Background: Error injecting content script:', error); + const notifId = 'cropSelectionError-' + Date.now(); + chrome.notifications.create(notifId, { + type: 'basic', + iconUrl: 'icons/iconbig.png', + title: 'Selection Failed', + message: 'Could not start screen selection. Error: ' + error.message, }); - break; - } - return isAsync; // Return true only if sendResponse is used asynchronously in any of the handled cases. + setTimeout(() => { + chrome.notifications.clear(notifId); + }, 5000); + } + } else { + console.error('Background: No active tab found for crop selection'); + } + }); + break; + case 'addBug': + console.log( + 'Background: Received message', + request.type, + '. Name:', + request.name, + '. imageURL (first 100 chars):', + request.imageURL ? request.imageURL.substring(0, 100) : 'null' + ); + addAnnotation('Bug', request.name, request.imageURL) + .then(() => sendResponse({ status: 'ok' })) + .catch((error) => sendResponse({ status: 'error', error: error.message })); + isAsync = true; + break; + case 'addIdea': + console.log( + 'Background: Received message', + request.type, + '. Name:', + request.name, + '. imageURL (first 100 chars):', + request.imageURL ? request.imageURL.substring(0, 100) : 'null' + ); + addAnnotation('Idea', request.name, request.imageURL) + .then(() => sendResponse({ status: 'ok' })) + .catch((error) => sendResponse({ status: 'error', error: error.message })); + isAsync = true; + break; + case 'addNote': + console.log( + 'Background: Received message', + request.type, + '. Name:', + request.name, + '. imageURL (first 100 chars):', + request.imageURL ? request.imageURL.substring(0, 100) : 'null' + ); + addAnnotation('Note', request.name, request.imageURL) + .then(() => sendResponse({ status: 'ok' })) + .catch((error) => sendResponse({ status: 'error', error: error.message })); + isAsync = true; + break; + case 'addQuestion': + console.log( + 'Background: Received message', + request.type, + '. Name:', + request.name, + '. imageURL (first 100 chars):', + request.imageURL ? request.imageURL.substring(0, 100) : 'null' + ); + addAnnotation('Question', request.name, request.imageURL) + .then(() => sendResponse({ status: 'ok' })) + .catch((error) => sendResponse({ status: 'error', error: error.message })); + isAsync = true; + break; + case 'requestCropScreenshot': + console.log('Background: Received requestCropScreenshot', request); + handleCropScreenshotRequest(request) + .then((croppedImageData) => { + sendResponse({ croppedImageData: croppedImageData }); + }) + .catch((error) => { + console.error('Background: Failed to capture crop screenshot:', error); + sendResponse({ croppedImageData: null }); + }); + isAsync = true; + break; + case 'csToBgCropData': + console.log('Background: Received csToBgCropData', request); + handleProcessAnnotatedCrop(request) + .then(() => { + console.log('Background: Annotated crop processed successfully'); + }) + .catch((error) => { + console.error('Background: Failed to process annotated crop:', error); + }); + break; + case 'updateAnnotationName': + var AnnotationID = request.annotationID; + var newName = request.newName; + var annotations = session.getAnnotations(); + var annotation = annotations[AnnotationID]; + annotation.setName(newName); + saveSession().then(() => sendResponse({ status: 'ok' })); + break; + case 'deleteAnnotation': + session.deleteAnnotation(request.annotationID); + saveSession().then(() => sendResponse({ status: 'ok' })); + break; + case 'exportSessionCSV': + if (!exportSessionCSV()) { + sendResponse({ status: 'nothing to export' }); + } else { + sendResponse({ status: 'ok' }); + } + break; + case 'exportSessionJSon': + if (!exportSessionJSon()) { + sendResponse({ status: 'nothing to export' }); + } else { + sendResponse({ status: 'ok' }); + } + break; + case 'importSessionJSon': + var fileData = request.jSonSession; + if (!importSessionJSon(fileData)) { + sendResponse({ status: 'nothing to import' }); + } else { + sendResponse({ status: 'ok' }); + } + break; + case 'clearSession': + clearSession().then(() => sendResponse({ status: 'ok' })); + break; + case 'getSessionData': + sendResponse({ + bugs: session.getBugs().length, + notes: session.getNotes().length, + ideas: session.getIdeas().length, + questions: session.getQuestions().length, + annotationsCount: session.getAnnotations().length, + }); + break; + case 'getFullSession': + if (!session) { + sendResponse(null); + return true; + } + sendResponse({ + startDateTime: session.StartDateTime, + browserInfo: { + browser: session.BrowserInfo.browser || 'Chrome', + browserVersion: + session.BrowserInfo.browserVersion || chrome.runtime.getManifest().version, + os: session.BrowserInfo.os || navigator.platform, + osVersion: session.BrowserInfo.osVersion || navigator.userAgent, + cookies: session.BrowserInfo.cookies || navigator.cookieEnabled, + flashVersion: session.BrowserInfo.flashVersion || 'N/A', + }, + annotations: session.annotations.map((annotation) => ({ + type: annotation.constructor.name, + name: annotation.name, + url: annotation.url, + timestamp: annotation.timestamp, + imageURL: annotation.imageURL, + })), + }); + break; + } + return isAsync; // Return true only if sendResponse is used asynchronously in any of the handled cases. }); - // Handle crop screenshot request - capture and crop, then return to content script async function handleCropScreenshotRequest(request) { - try { - console.log("Background: Processing crop screenshot request", request); - - // Capture the visible tab - const dataUrl = await chrome.tabs.captureVisibleTab(null, { format: "png" }); - if (!dataUrl) { - throw new Error("Failed to capture screenshot"); - } - - // Convert dataUrl to Blob - const response = await fetch(dataUrl); - const blob = await response.blob(); - - // Create bitmap from image - const bitmap = await createImageBitmap(blob); + try { + console.log('Background: Processing crop screenshot request', request); - // Create offscreen canvas with crop dimensions - const canvas = new OffscreenCanvas( - request.coordinates.width, - request.coordinates.height - ); - const ctx = canvas.getContext('2d'); - - // Draw the selected portion - ctx.drawImage( - bitmap, - request.coordinates.x, - request.coordinates.y, - request.coordinates.width, - request.coordinates.height, - 0, - 0, - request.coordinates.width, - request.coordinates.height - ); - - // Convert canvas to blob - const croppedBlob = await canvas.convertToBlob({ type: 'image/png' }); + // Capture the visible tab + const dataUrl = await chrome.tabs.captureVisibleTab(null, { format: 'png' }); + if (!dataUrl) { + throw new Error('Failed to capture screenshot'); + } - // Convert blob to dataUrl - const croppedDataUrl = await new Promise((resolve) => { - const reader = new FileReader(); - reader.onloadend = () => resolve(reader.result); - reader.readAsDataURL(croppedBlob); - }); + // Convert dataUrl to Blob + const response = await fetch(dataUrl); + const blob = await response.blob(); + + // Create bitmap from image + const bitmap = await createImageBitmap(blob); + + // Create offscreen canvas with crop dimensions + const canvas = new OffscreenCanvas(request.coordinates.width, request.coordinates.height); + const ctx = canvas.getContext('2d'); + + // Draw the selected portion + ctx.drawImage( + bitmap, + request.coordinates.x, + request.coordinates.y, + request.coordinates.width, + request.coordinates.height, + 0, + 0, + request.coordinates.width, + request.coordinates.height + ); + + // Convert canvas to blob + const croppedBlob = await canvas.convertToBlob({ type: 'image/png' }); + + // Convert blob to dataUrl + const croppedDataUrl = await new Promise((resolve) => { + const reader = new FileReader(); + reader.onloadend = () => resolve(reader.result); + reader.readAsDataURL(croppedBlob); + }); - console.log("Background: Successfully created cropped screenshot"); - return croppedDataUrl; - } catch (error) { - console.error("Background: Error in handleCropScreenshotRequest:", error); - throw error; - } + console.log('Background: Successfully created cropped screenshot'); + return croppedDataUrl; + } catch (error) { + console.error('Background: Error in handleCropScreenshotRequest:', error); + throw error; + } } // Handle annotated crop data - save the final annotated image async function handleProcessAnnotatedCrop(request) { - try { - console.log("Background: Processing annotated crop", request); - - // Create annotation with the annotated image - await addAnnotation( - request.annotationType.charAt(0).toUpperCase() + request.annotationType.slice(1), - request.description, - request.annotatedImageData - ); - - console.log("Background: Successfully processed annotated crop"); - } catch (error) { - console.error("Background: Error in handleProcessAnnotatedCrop:", error); - throw error; - } + try { + console.log('Background: Processing annotated crop', request); + + // Create annotation with the annotated image + await addAnnotation( + request.annotationType.charAt(0).toUpperCase() + request.annotationType.slice(1), + request.description, + request.annotatedImageData + ); + + console.log('Background: Successfully processed annotated crop'); + } catch (error) { + console.error('Background: Error in handleProcessAnnotatedCrop:', error); + throw error; + } } - async function addAnnotation(type, name, imageURL) { - console.log("Background: addAnnotation called. Type:", type, ". Name:", name, ". Image URL (first 100 chars):", imageURL ? imageURL.substring(0, 100) : "No image"); - if (session.getAnnotations().length == 0) { - await startSession(); - } - - return new Promise((resolve, reject) => { - chrome.tabs.query({ currentWindow: true, active: true }, function (tabs) { - try { - const currentUrl = tabs[0] ? tabs[0].url : "N/A"; // Handle missing tab - const now = new Date().getTime(); - - let newAnnotation; - let annotationSimpleType = ""; // For user-friendly notification - - switch (type) { - case "Bug": - newAnnotation = new Bug(name, currentUrl, now, imageURL); - session.addBug(newAnnotation); - annotationSimpleType = "Bug"; - break; - case "Note": - newAnnotation = new Note(name, currentUrl, now, imageURL); - session.addNote(newAnnotation); - annotationSimpleType = "Note"; - break; - case "Idea": - newAnnotation = new Idea(name, currentUrl, now, imageURL); - session.addIdea(newAnnotation); - annotationSimpleType = "Idea"; - break; - case "Question": - newAnnotation = new Question(name, currentUrl, now, imageURL); - session.addQuestion(newAnnotation); - annotationSimpleType = "Question"; - break; - default: // Should not happen - return reject(new Error("Unknown annotation type")); - } + console.log( + 'Background: addAnnotation called. Type:', + type, + '. Name:', + name, + '. Image URL (first 100 chars):', + imageURL ? imageURL.substring(0, 100) : 'No image' + ); + if (session.getAnnotations().length == 0) { + await startSession(); + } + + return new Promise((resolve, reject) => { + chrome.tabs.query({ currentWindow: true, active: true }, function (tabs) { + try { + const currentUrl = tabs[0] ? tabs[0].url : 'N/A'; // Handle missing tab + const now = new Date().getTime(); + + let newAnnotation; + let annotationSimpleType = ''; // For user-friendly notification + + switch (type) { + case 'Bug': + newAnnotation = new Bug(name, currentUrl, now, imageURL); + session.addBug(newAnnotation); + annotationSimpleType = 'Bug'; + break; + case 'Note': + newAnnotation = new Note(name, currentUrl, now, imageURL); + session.addNote(newAnnotation); + annotationSimpleType = 'Note'; + break; + case 'Idea': + newAnnotation = new Idea(name, currentUrl, now, imageURL); + session.addIdea(newAnnotation); + annotationSimpleType = 'Idea'; + break; + case 'Question': + newAnnotation = new Question(name, currentUrl, now, imageURL); + session.addQuestion(newAnnotation); + annotationSimpleType = 'Question'; + break; + default: // Should not happen + return reject(new Error('Unknown annotation type')); + } - console.log("Background: Attempting to save session for annotation Type:", type, "Name:", name); - saveSession().then(() => { - // --- Create Notification --- - const notifId = 'annotationSaved-' + Date.now(); - const notifOptions = { - type: 'basic', - iconUrl: 'icons/iconbig.png', // Ensure this icon path is correct - title: `${annotationSimpleType} Saved!`, - message: `Your ${annotationSimpleType.toLowerCase()} "${name}" has been successfully saved.` - }; - chrome.notifications.create(notifId, notifOptions); - // Optional: Clear notification after a few seconds - setTimeout(() => { - chrome.notifications.clear(notifId); - }, 5000); // Clear after 5 seconds - // --- End Notification --- - - resolve(); // Resolve the main promise - }).catch(error => { - console.error("Background: Error during saveSession for", type, name, ":", error); - // Optionally, show an error notification here too - const errorNotifId = 'annotationError-' + Date.now(); - chrome.notifications.create(errorNotifId, { - type: 'basic', - iconUrl: 'icons/iconbig.png', - title: `${annotationSimpleType || type} Save Failed`, - message: `Could not save ${annotationSimpleType.toLowerCase() || type.toLowerCase()} "${name}". Error: ${error.message}` - }); - setTimeout(() => { - chrome.notifications.clear(errorNotifId); - }, 7000); // Keep error notifications slightly longer - - reject(error); - }); - - } catch (error) { // Catch synchronous errors in the promise executor - console.error("Background: Error in addAnnotation sync part:", error); - // Send a notification for this synchronous error as well - const syncErrorNotifId = 'annotationSyncError-' + Date.now(); - chrome.notifications.create(syncErrorNotifId, { - type: 'basic', - iconUrl: 'icons/iconbig.png', - title: `${type || 'Annotation'} Setup Failed`, - message: `Failed to initiate saving for "${name}". Error: ${error.message}` - }); - setTimeout(() => { - chrome.notifications.clear(syncErrorNotifId); - }, 7000); - reject(error); - } + console.log( + 'Background: Attempting to save session for annotation Type:', + type, + 'Name:', + name + ); + saveSession() + .then(() => { + // --- Create Notification --- + const notifId = 'annotationSaved-' + Date.now(); + const notifOptions = { + type: 'basic', + iconUrl: 'icons/iconbig.png', // Ensure this icon path is correct + title: `${annotationSimpleType} Saved!`, + message: `Your ${annotationSimpleType.toLowerCase()} "${name}" has been successfully saved.`, + }; + chrome.notifications.create(notifId, notifOptions); + // Optional: Clear notification after a few seconds + setTimeout(() => { + chrome.notifications.clear(notifId); + }, 5000); // Clear after 5 seconds + // --- End Notification --- + + resolve(); // Resolve the main promise + }) + .catch((error) => { + console.error('Background: Error during saveSession for', type, name, ':', error); + // Optionally, show an error notification here too + const errorNotifId = 'annotationError-' + Date.now(); + chrome.notifications.create(errorNotifId, { + type: 'basic', + iconUrl: 'icons/iconbig.png', + title: `${annotationSimpleType || type} Save Failed`, + message: `Could not save ${annotationSimpleType.toLowerCase() || type.toLowerCase()} "${name}". Error: ${error.message}`, + }); + setTimeout(() => { + chrome.notifications.clear(errorNotifId); + }, 7000); // Keep error notifications slightly longer + + reject(error); + }); + } catch (error) { + // Catch synchronous errors in the promise executor + console.error('Background: Error in addAnnotation sync part:', error); + // Send a notification for this synchronous error as well + const syncErrorNotifId = 'annotationSyncError-' + Date.now(); + chrome.notifications.create(syncErrorNotifId, { + type: 'basic', + iconUrl: 'icons/iconbig.png', + title: `${type || 'Annotation'} Setup Failed`, + message: `Failed to initiate saving for "${name}". Error: ${error.message}`, }); + setTimeout(() => { + chrome.notifications.clear(syncErrorNotifId); + }, 7000); + reject(error); + } }); + }); } async function startSession() { - var systemInfo = getSystemInfo(); - session = new Session(Date.now(), systemInfo); - await saveSession(); + var systemInfo = getSystemInfo(); + session = new Session(Date.now(), systemInfo); + await saveSession(); } async function clearSession() { - session.clearAnnotations(); - await saveSession(); + session.clearAnnotations(); + await saveSession(); } function exportSessionCSV() { - if (session.getAnnotations().length == 0) return false; + if (session.getAnnotations().length == 0) return false; - var exportService = new ExportSessionCSV(session); - var csvData = exportService.getCSVData(); + var exportService = new ExportSessionCSV(session); + var csvData = exportService.getCSVData(); - var browserInfo = session.getBrowserInfo(); - var browserInfoString = browserInfo.browser + "_" + browserInfo.browserVersion; + var browserInfo = session.getBrowserInfo(); + var browserInfoString = browserInfo.browser + '_' + browserInfo.browserVersion; - // Formatear la fecha correctamente - const date = new Date(session.getStartDateTime()); - const startDateTime = date.getFullYear() + - ('0' + (date.getMonth() + 1)).slice(-2) + - ('0' + date.getDate()).slice(-2) + '_' + - ('0' + date.getHours()).slice(-2) + - ('0' + date.getMinutes()).slice(-2); + // Formatear la fecha correctamente + const date = new Date(session.getStartDateTime()); + const startDateTime = + date.getFullYear() + + ('0' + (date.getMonth() + 1)).slice(-2) + + ('0' + date.getDate()).slice(-2) + + '_' + + ('0' + date.getHours()).slice(-2) + + ('0' + date.getMinutes()).slice(-2); - var fileName = "ExploratorySession_" + browserInfoString + "_" + startDateTime + ".csv"; + var fileName = 'ExploratorySession_' + browserInfoString + '_' + startDateTime + '.csv'; - // Crear data URL - const dataUrl = 'data:text/csv;charset=utf-8;base64,' + btoa(csvData); + // Crear data URL + const dataUrl = 'data:text/csv;charset=utf-8;base64,' + btoa(csvData); - chrome.downloads.download({ - url: dataUrl, - filename: fileName, - saveAs: true - }); + chrome.downloads.download({ + url: dataUrl, + filename: fileName, + saveAs: true, + }); - return true; + return true; } function exportSessionJSon() { - if (session.getAnnotations().length == 0) return false; + if (session.getAnnotations().length == 0) return false; - var exportJSonService = new JSonSessionService(); - var jsonData = exportJSonService.getJSon(session); + var exportJSonService = new JSonSessionService(); + var jsonData = exportJSonService.getJSon(session); - var browserInfo = session.getBrowserInfo(); - var browserInfoString = browserInfo.browser + "_" + browserInfo.browserVersion; + var browserInfo = session.getBrowserInfo(); + var browserInfoString = browserInfo.browser + '_' + browserInfo.browserVersion; - // Formatear la fecha correctamente - const date = new Date(session.getStartDateTime()); - const startDateTime = date.getFullYear() + - ('0' + (date.getMonth() + 1)).slice(-2) + - ('0' + date.getDate()).slice(-2) + '_' + - ('0' + date.getHours()).slice(-2) + - ('0' + date.getMinutes()).slice(-2); + // Formatear la fecha correctamente + const date = new Date(session.getStartDateTime()); + const startDateTime = + date.getFullYear() + + ('0' + (date.getMonth() + 1)).slice(-2) + + ('0' + date.getDate()).slice(-2) + + '_' + + ('0' + date.getHours()).slice(-2) + + ('0' + date.getMinutes()).slice(-2); - var fileName = "ExploratorySession_" + browserInfoString + "_" + startDateTime + ".json"; + var fileName = 'ExploratorySession_' + browserInfoString + '_' + startDateTime + '.json'; - // Crear data URL - const dataUrl = 'data:application/json;base64,' + btoa(jsonData); + // Crear data URL + const dataUrl = 'data:application/json;base64,' + btoa(jsonData); - chrome.downloads.download({ - url: dataUrl, - filename: fileName, - saveAs: true - }); + chrome.downloads.download({ + url: dataUrl, + filename: fileName, + saveAs: true, + }); - return true; + return true; } function importSessionJSon(JSonSessionData) { - debugger; - var exportJSonService = new JSonSessionService(); - var importedSession = exportJSonService.getSession(JSonSessionData); + var exportJSonService = new JSonSessionService(); + var importedSession = exportJSonService.getSession(JSonSessionData); - if (importedSession == null) - return false; + if (importedSession == null) return false; - clearSession(); - session = importedSession; + clearSession(); + session = importedSession; - return true; -} \ No newline at end of file + return true; +} diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..d1b6b72 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,69 @@ +const js = require('@eslint/js'); +const globals = require('globals'); +const prettier = require('eslint-config-prettier'); + +module.exports = [ + { + ignores: [ + 'lib/**', + 'node_modules/**', + 'playwright-report/**', + 'test-results/**', + 'coverage/**', + '**/*.min.js', + 'HTMLReport/TableFilter/**', + ], + }, + js.configs.recommended, + { + // Extension runtime: ES modules running in the browser or service worker + files: ['src/**/*.js', 'js/**/*.js', 'background.js', 'HTMLReport/**/*.js'], + languageOptions: { + sourceType: 'module', + globals: { + ...globals.browser, + ...globals.serviceworker, + chrome: 'readonly', + $: 'readonly', + jQuery: 'readonly', + // vendored libraries loaded via