From 9ecf73e046cd4ee9a71790166127cdc92698f0d5 Mon Sep 17 00:00:00 2001 From: KarthiDreamr Date: Wed, 13 Aug 2025 20:58:59 +0530 Subject: [PATCH 01/10] escaped node modules from vcs --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..40b878d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/ \ No newline at end of file -- 2.49.1 From 7f591382dbf51434d242145484850375dd6770aa Mon Sep 17 00:00:00 2001 From: KarthiDreamr Date: Wed, 13 Aug 2025 21:00:55 +0530 Subject: [PATCH 02/10] feat: added copy to clipboard feature --- .../specs/copy-to-clipboard-toggle/design.md | 165 + .../copy-to-clipboard-toggle/requirements.md | 51 + .kiro/specs/copy-to-clipboard-toggle/tasks.md | 64 + .vscode/settings.json | 3 + content.js | 380 +- coverage/base.css | 224 + coverage/block-navigation.js | 87 + coverage/content.js.html | 2893 +++++++++++ coverage/favicon.png | Bin 0 -> 445 bytes coverage/index.html | 131 + coverage/lcov-report/base.css | 224 + coverage/lcov-report/block-navigation.js | 87 + coverage/lcov-report/content.js.html | 2893 +++++++++++ coverage/lcov-report/favicon.png | Bin 0 -> 445 bytes coverage/lcov-report/index.html | 131 + coverage/lcov-report/popup.js.html | 643 +++ coverage/lcov-report/prettify.css | 1 + coverage/lcov-report/prettify.js | 2 + coverage/lcov-report/sort-arrow-sprite.png | Bin 0 -> 138 bytes coverage/lcov-report/sorter.js | 196 + coverage/lcov.info | 848 ++++ coverage/popup.js.html | 643 +++ coverage/prettify.css | 1 + coverage/prettify.js | 2 + coverage/sort-arrow-sprite.png | Bin 0 -> 138 bytes coverage/sorter.js | 196 + package-lock.json | 4466 +++++++++++++++++ package.json | 37 + popup.html | 167 + popup.js | 60 +- 30 files changed, 14583 insertions(+), 12 deletions(-) create mode 100644 .kiro/specs/copy-to-clipboard-toggle/design.md create mode 100644 .kiro/specs/copy-to-clipboard-toggle/requirements.md create mode 100644 .kiro/specs/copy-to-clipboard-toggle/tasks.md create mode 100644 .vscode/settings.json create mode 100644 coverage/base.css create mode 100644 coverage/block-navigation.js create mode 100644 coverage/content.js.html create mode 100644 coverage/favicon.png create mode 100644 coverage/index.html create mode 100644 coverage/lcov-report/base.css create mode 100644 coverage/lcov-report/block-navigation.js create mode 100644 coverage/lcov-report/content.js.html create mode 100644 coverage/lcov-report/favicon.png create mode 100644 coverage/lcov-report/index.html create mode 100644 coverage/lcov-report/popup.js.html create mode 100644 coverage/lcov-report/prettify.css create mode 100644 coverage/lcov-report/prettify.js create mode 100644 coverage/lcov-report/sort-arrow-sprite.png create mode 100644 coverage/lcov-report/sorter.js create mode 100644 coverage/lcov.info create mode 100644 coverage/popup.js.html create mode 100644 coverage/prettify.css create mode 100644 coverage/prettify.js create mode 100644 coverage/sort-arrow-sprite.png create mode 100644 coverage/sorter.js create mode 100644 package-lock.json create mode 100644 package.json diff --git a/.kiro/specs/copy-to-clipboard-toggle/design.md b/.kiro/specs/copy-to-clipboard-toggle/design.md new file mode 100644 index 0000000..7d4af9a --- /dev/null +++ b/.kiro/specs/copy-to-clipboard-toggle/design.md @@ -0,0 +1,165 @@ +# Design Document + +## Overview + +The copy to clipboard toggle feature will be implemented as an additional setting in the extension popup, similar to the existing background color selector. The feature will use the modern Clipboard API with appropriate fallbacks for older browsers. The implementation will integrate seamlessly with the existing screenshot capture workflow, adding clipboard functionality without disrupting the current download behavior. + +## Architecture + +The feature will be implemented across three main components: + +1. **Popup UI Enhancement**: Add toggle control and persistence logic +2. **Content Script Integration**: Modify screenshot capture to include clipboard operations +3. **Storage Management**: Extend existing preference storage system + +The clipboard functionality will be implemented as an optional enhancement to the existing screenshot workflow, ensuring backward compatibility and graceful degradation. + +## Components and Interfaces + +### Popup Interface (`popup.html` & `popup.js`) + +**New UI Elements:** +- Toggle switch control for "Copy to Clipboard" option +- Positioned between background selector and start button +- Consistent styling with existing controls + +**Storage Integration:** +- Extend existing `chrome.storage.sync` usage to include `copyToClipboard` preference +- Default value: `true` (enabled by default for better user experience) +- Load and save preferences alongside existing background preference + +**Message Passing:** +- Include `copyToClipboard` setting in the `startSelector` message to content script +- No changes needed to existing message structure, just additional property + +### Content Script Enhancement (`content.js`) + +**ScreenshotSelector Class Modifications:** +- Add `copyToClipboard` property to constructor +- Modify `init()` method to accept and store clipboard preference +- Enhance `captureElement()` method to include clipboard operations + +**Clipboard Implementation:** +```javascript +async copyCanvasToClipboard(canvas) { + try { + // Convert canvas to blob + const blob = await new Promise(resolve => + canvas.toBlob(resolve, 'image/png') + ); + + // Use Clipboard API + await navigator.clipboard.write([ + new ClipboardItem({ 'image/png': blob }) + ]); + + return { success: true }; + } catch (error) { + return { success: false, error: error.message }; + } +} +``` + +**Error Handling:** +- Graceful fallback when Clipboard API is unavailable +- User-friendly error messages for clipboard failures +- Ensure screenshot download continues even if clipboard fails + +### User Feedback System + +**Success States:** +- "Screenshot saved and copied to clipboard!" - when both operations succeed +- "Screenshot saved! (Clipboard copy failed: [reason])" - when download succeeds but clipboard fails + +**Loading States:** +- Update existing loading message to indicate clipboard operation when enabled +- "Capturing screenshot and preparing clipboard..." vs "Capturing screenshot..." + +## Data Models + +### Settings Storage Schema +```javascript +{ + backgroundPreference: 'black' | 'transparent' | 'white', + copyToClipboard: boolean // New addition +} +``` + +### Message Interface Extension +```javascript +// startSelector message +{ + action: 'startSelector', + background: string, + copyToClipboard: boolean // New addition +} +``` + +## Error Handling + +### Clipboard API Availability +- Check for `navigator.clipboard` and `ClipboardItem` support +- Provide clear error messages when API is unavailable +- Continue with download-only functionality as fallback + +### Permission Handling +- Clipboard write operations may require user gesture +- Handle permission denied scenarios gracefully +- Provide user guidance for enabling clipboard permissions if needed + +### Browser Compatibility +- Primary support: Chrome/Edge 76+, Firefox 87+, Safari 13.1+ +- Fallback behavior for older browsers: disable clipboard feature with notification +- No impact on core screenshot functionality + +## Testing Strategy + +### Unit Testing Approach +- Mock Clipboard API for testing clipboard operations +- Test preference storage and retrieval +- Verify message passing between popup and content script +- Test error handling scenarios + +### Integration Testing +- Test complete workflow: toggle setting → capture screenshot → verify clipboard content +- Test with different background settings and clipboard combinations +- Verify graceful degradation when Clipboard API unavailable + +### Browser Compatibility Testing +- Test across Chrome, Firefox, Safari, Edge +- Verify behavior on older browser versions +- Test permission scenarios and user gesture requirements + +### User Experience Testing +- Verify toggle state persistence across browser sessions +- Test visual feedback for success/error states +- Confirm tooltip and help text clarity + +## Implementation Considerations + +### Performance Impact +- Clipboard operations are asynchronous and won't block screenshot download +- Canvas-to-blob conversion adds minimal overhead +- No impact on screenshot capture performance + +### Security Considerations +- Clipboard API requires secure context (HTTPS) +- User gesture requirement for clipboard write operations +- No sensitive data exposure through clipboard operations + +### Accessibility +- Toggle control will include proper ARIA labels +- Keyboard navigation support for toggle control +- Screen reader compatible success/error messages + +## Browser API Dependencies + +### Required APIs +- `navigator.clipboard.write()` - Modern clipboard API +- `ClipboardItem` constructor - For image data handling +- `canvas.toBlob()` - Convert canvas to blob format + +### Fallback Strategy +- Detect API availability before attempting clipboard operations +- Provide clear user feedback when clipboard features unavailable +- Maintain full functionality for screenshot download regardless of clipboard support \ No newline at end of file diff --git a/.kiro/specs/copy-to-clipboard-toggle/requirements.md b/.kiro/specs/copy-to-clipboard-toggle/requirements.md new file mode 100644 index 0000000..2553626 --- /dev/null +++ b/.kiro/specs/copy-to-clipboard-toggle/requirements.md @@ -0,0 +1,51 @@ +# Requirements Document + +## Introduction + +This feature adds a copy to clipboard toggle option to the Full Screenshot Selector extension, allowing users to choose whether captured screenshots should be automatically copied to the clipboard in addition to being downloaded as files. This provides users with more flexibility in how they handle their screenshots, enabling quick pasting into other applications without needing to locate and open the downloaded file. + +## Requirements + +### Requirement 1 + +**User Story:** As a user of the screenshot extension, I want to toggle whether screenshots are copied to clipboard, so that I can quickly paste them into other applications without downloading files. + +#### Acceptance Criteria + +1. WHEN the popup is opened THEN the system SHALL display a toggle control for "Copy to Clipboard" +2. WHEN the user toggles the copy to clipboard option THEN the system SHALL save this preference to browser storage +3. WHEN the extension is reopened THEN the system SHALL restore the previously saved copy to clipboard preference +4. WHEN a screenshot is captured AND copy to clipboard is enabled THEN the system SHALL copy the image data to the clipboard +5. WHEN a screenshot is captured AND copy to clipboard is enabled THEN the system SHALL still download the file as before (both actions occur) + +### Requirement 2 + +**User Story:** As a user, I want the copy to clipboard feature to work reliably across different browsers, so that I can depend on this functionality regardless of my browser choice. + +#### Acceptance Criteria + +1. WHEN copy to clipboard is enabled THEN the system SHALL use the Clipboard API if available +2. IF the Clipboard API is not available THEN the system SHALL provide appropriate fallback behavior +3. WHEN clipboard copying fails THEN the system SHALL display an error message to the user +4. WHEN clipboard copying succeeds THEN the system SHALL display a success confirmation + +### Requirement 3 + +**User Story:** As a user, I want clear visual feedback about the copy to clipboard feature, so that I understand when it's enabled and when clipboard operations succeed or fail. + +#### Acceptance Criteria + +1. WHEN the copy to clipboard toggle is enabled THEN the system SHALL display visual indication of the enabled state +2. WHEN a screenshot is captured with clipboard copying enabled THEN the system SHALL show a success message indicating both download and clipboard copy +3. WHEN clipboard copying fails THEN the system SHALL show an error message while still confirming the download succeeded +4. WHEN hovering over the copy to clipboard toggle THEN the system SHALL display helpful tooltip text + +### Requirement 4 + +**User Story:** As a user, I want the copy to clipboard feature to handle different image formats appropriately, so that the clipboard content works well with various applications. + +#### Acceptance Criteria + +1. WHEN copying to clipboard THEN the system SHALL copy the image in PNG format +2. WHEN copying to clipboard THEN the system SHALL preserve the same image quality as the downloaded file +3. WHEN copying to clipboard THEN the system SHALL handle transparency settings according to the background preference \ No newline at end of file diff --git a/.kiro/specs/copy-to-clipboard-toggle/tasks.md b/.kiro/specs/copy-to-clipboard-toggle/tasks.md new file mode 100644 index 0000000..25d7b74 --- /dev/null +++ b/.kiro/specs/copy-to-clipboard-toggle/tasks.md @@ -0,0 +1,64 @@ +# Implementation Plan + +- [x] 1. Add copy to clipboard toggle UI to popup + - Add HTML toggle control between background selector and start button in popup.html + - Style the toggle control to match existing UI design patterns + - Include proper ARIA labels and accessibility attributes for the toggle + - _Requirements: 1.1, 3.1, 3.4_ + +- [x] 2. Implement clipboard preference storage in popup + - Extend existing storage logic in popup.js to handle copyToClipboard preference + - Set default value to true for better user experience + - Load saved clipboard preference on popup initialization + - Save clipboard preference changes to chrome.storage.sync + - _Requirements: 1.2, 1.3_ + +- [x] 3. Update message passing to include clipboard setting + - Modify startSelector message in popup.js to include copyToClipboard property + - Update content script message handler to receive and store clipboard preference + - Ensure backward compatibility with existing message structure + - _Requirements: 1.1, 1.4_ + +- [x] 4. Implement clipboard API functionality in content script + - Add copyCanvasToClipboard method to ScreenshotSelector class + - Implement canvas to blob conversion for PNG format + - Use navigator.clipboard.write() with ClipboardItem for image data + - Handle async clipboard operations with proper error catching + - _Requirements: 2.1, 4.1, 4.2, 4.3_ + +- [x] 5. Add clipboard API availability detection + - Create method to check for navigator.clipboard and ClipboardItem support + - Implement graceful fallback when Clipboard API is unavailable + - Provide user feedback when clipboard features are not supported + - _Requirements: 2.2_ + +- [x] 6. Integrate clipboard operations into screenshot capture workflow + - Modify captureElement method to include clipboard operations when enabled + - Ensure clipboard copying happens after successful canvas generation + - Maintain existing download functionality regardless of clipboard success/failure + - _Requirements: 1.4, 1.5_ + +- [x] 7. Implement comprehensive error handling for clipboard operations + - Handle clipboard write permission errors gracefully + - Provide specific error messages for different failure scenarios + - Ensure screenshot download continues even when clipboard operations fail + - _Requirements: 2.3_ + +- [x] 8. Update user feedback messages for clipboard operations + - Modify loading message to indicate clipboard preparation when enabled + - Update success message to confirm both download and clipboard copy + - Create error messages that distinguish between download and clipboard failures + - _Requirements: 2.4, 3.2, 3.3_ + +- [x] 9. Add tooltip and help text for clipboard toggle + - Implement tooltip showing clipboard feature explanation + - Add help text explaining clipboard functionality and browser requirements + - Ensure tooltip is accessible and keyboard navigable + - _Requirements: 3.4_ + +- [x] 10. Test clipboard functionality across different scenarios + - Write test cases for clipboard API availability detection + - Test clipboard operations with different background settings (transparent, black, white) + - Verify error handling when clipboard API is unavailable or permissions denied + - Test preference persistence across browser sessions + - _Requirements: 2.1, 2.2, 2.3, 4.3_ \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..5480842 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "kiroAgent.configureMCP": "Disabled" +} \ No newline at end of file diff --git a/content.js b/content.js index 45161dd..2b6e2cf 100644 --- a/content.js +++ b/content.js @@ -6,15 +6,17 @@ class ScreenshotSelector { this.overlay = null; this.style = null; this.background = 'black'; + this.copyToClipboard = true; // Default to true } - async init(background = 'black') { + async init(background = 'black', copyToClipboard = true) { if (this.isActive) { console.log('Screenshot selector already active'); return; } this.background = background; + this.copyToClipboard = copyToClipboard; this.isActive = true; this.createUI(); @@ -363,12 +365,23 @@ class ScreenshotSelector { async captureElement(element) { try { + // Determine loading message based on clipboard settings + let loadingMessage = 'Capturing screenshot...'; + if (this.copyToClipboard) { + const availability = this.checkClipboardAPIAvailability(); + if (availability.available) { + loadingMessage = 'Capturing screenshot and preparing for clipboard...'; + } else { + loadingMessage = 'Capturing screenshot... (Clipboard not available)'; + } + } + // Show loading state this.overlay.innerHTML = `
- Capturing screenshot... + ${loadingMessage}
+ + + +
+
+

All files content.js

+
+ +
+ 0% + Statements + 0/371 +
+ + +
+ 0% + Branches + 0/214 +
+ + +
+ 0% + Functions + 0/44 +
+ + +
+ 0% + Lines + 0/356 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +516 +517 +518 +519 +520 +521 +522 +523 +524 +525 +526 +527 +528 +529 +530 +531 +532 +533 +534 +535 +536 +537 +538 +539 +540 +541 +542 +543 +544 +545 +546 +547 +548 +549 +550 +551 +552 +553 +554 +555 +556 +557 +558 +559 +560 +561 +562 +563 +564 +565 +566 +567 +568 +569 +570 +571 +572 +573 +574 +575 +576 +577 +578 +579 +580 +581 +582 +583 +584 +585 +586 +587 +588 +589 +590 +591 +592 +593 +594 +595 +596 +597 +598 +599 +600 +601 +602 +603 +604 +605 +606 +607 +608 +609 +610 +611 +612 +613 +614 +615 +616 +617 +618 +619 +620 +621 +622 +623 +624 +625 +626 +627 +628 +629 +630 +631 +632 +633 +634 +635 +636 +637 +638 +639 +640 +641 +642 +643 +644 +645 +646 +647 +648 +649 +650 +651 +652 +653 +654 +655 +656 +657 +658 +659 +660 +661 +662 +663 +664 +665 +666 +667 +668 +669 +670 +671 +672 +673 +674 +675 +676 +677 +678 +679 +680 +681 +682 +683 +684 +685 +686 +687 +688 +689 +690 +691 +692 +693 +694 +695 +696 +697 +698 +699 +700 +701 +702 +703 +704 +705 +706 +707 +708 +709 +710 +711 +712 +713 +714 +715 +716 +717 +718 +719 +720 +721 +722 +723 +724 +725 +726 +727 +728 +729 +730 +731 +732 +733 +734 +735 +736 +737 +738 +739 +740 +741 +742 +743 +744 +745 +746 +747 +748 +749 +750 +751 +752 +753 +754 +755 +756 +757 +758 +759 +760 +761 +762 +763 +764 +765 +766 +767 +768 +769 +770 +771 +772 +773 +774 +775 +776 +777 +778 +779 +780 +781 +782 +783 +784 +785 +786 +787 +788 +789 +790 +791 +792 +793 +794 +795 +796 +797 +798 +799 +800 +801 +802 +803 +804 +805 +806 +807 +808 +809 +810 +811 +812 +813 +814 +815 +816 +817 +818 +819 +820 +821 +822 +823 +824 +825 +826 +827 +828 +829 +830 +831 +832 +833 +834 +835 +836 +837 +838 +839 +840 +841 +842 +843 +844 +845 +846 +847 +848 +849 +850 +851 +852 +853 +854 +855 +856 +857 +858 +859 +860 +861 +862 +863 +864 +865 +866 +867 +868 +869 +870 +871 +872 +873 +874 +875 +876 +877 +878 +879 +880 +881 +882 +883 +884 +885 +886 +887 +888 +889 +890 +891 +892 +893 +894 +895 +896 +897 +898 +899 +900 +901 +902 +903 +904 +905 +906 +907 +908 +909 +910 +911 +912 +913 +914 +915 +916 +917 +918 +919 +920 +921 +922 +923 +924 +925 +926 +927 +928 +929 +930 +931 +932 +933 +934 +935 +936 +937  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
// Content script for Full Screenshot Selector Chrome Extension
+class ScreenshotSelector {
+  constructor() {
+    this.isActive = false;
+    this.currentHighlight = null;
+    this.overlay = null;
+    this.style = null;
+    this.background = 'black';
+    this.copyToClipboard = true; // Default to true
+  }
+ 
+  async init(background = 'black', copyToClipboard = true) {
+    if (this.isActive) {
+      console.log('Screenshot selector already active');
+      return;
+    }
+ 
+    this.background = background;
+    this.copyToClipboard = copyToClipboard;
+    this.isActive = true;
+ 
+    this.createUI();
+    this.addEventListeners();
+  }
+ 
+  createUI() {
+    // Create overlay UI
+    this.overlay = document.createElement('div');
+    this.overlay.id = 'screenshot-selector-overlay';
+    this.overlay.innerHTML = `
+      <div style="position: fixed; top: 20px; left: 20px; background: rgba(0,0,0,0.9); color: white; padding: 12px 16px; border-radius: 8px; z-index: 2147483647; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 14px; box-shadow: 0 4px 20px rgba(0,0,0,0.3); backdrop-filter: blur(10px);">
+        <div style="display: flex; align-items: center; gap: 10px;">
+          <span>📸</span>
+          <span>Hover over elements and click to capture</span>
+          <button id="screenshot-cancel" style="margin-left: 10px; padding: 4px 8px; background: #ff4757; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px;">ESC</button>
+        </div>
+      </div>
+    `;
+ 
+    // Add hover highlighting styles
+    this.style = document.createElement('style');
+    this.style.textContent = `
+      .screenshot-highlight {
+        outline: 3px solid #00d2ff !important;
+        outline-offset: 2px !important;
+        cursor: crosshair !important;
+        position: relative !important;
+      }
+      .screenshot-highlight::after {
+        content: '';
+        position: absolute;
+        top: -5px;
+        left: -5px;
+        right: -5px;
+        bottom: -5px;
+        background: rgba(0, 210, 255, 0.1) !important;
+        pointer-events: none !important;
+        z-index: 2147483646 !important;
+      }
+      .screenshot-scrollable-indicator {
+        position: absolute !important;
+        border: 2px dotted #ff6b6b !important;
+        background: rgba(255, 107, 107, 0.05) !important;
+        pointer-events: none !important;
+        z-index: 2147483645 !important;
+      }
+      #screenshot-selector-overlay * {
+        cursor: default !important;
+      }
+    `;
+ 
+    document.head.appendChild(this.style);
+    document.body.appendChild(this.overlay);
+ 
+    // Cancel button functionality
+    document.getElementById('screenshot-cancel').onclick = () => this.cleanup();
+  }
+ 
+  addEventListeners() {
+    this.handleMouseOver = this.handleMouseOver.bind(this);
+    this.handleClick = this.handleClick.bind(this);
+    this.handleKeyPress = this.handleKeyPress.bind(this);
+ 
+    document.addEventListener('mouseover', this.handleMouseOver);
+    document.addEventListener('click', this.handleClick, true);
+    document.addEventListener('keydown', this.handleKeyPress);
+  }
+ 
+  handleMouseOver(e) {
+    if (e.target.closest('#screenshot-selector-overlay')) return;
+ 
+    // Clean up previous highlights and indicators
+    if (this.currentHighlight) {
+      this.currentHighlight.classList.remove('screenshot-highlight');
+      this.removeScrollableIndicators();
+    }
+ 
+    this.currentHighlight = e.target;
+    e.target.classList.add('screenshot-highlight');
+ 
+    // Add scrollable content indicators
+    this.addScrollableIndicators(e.target);
+  }
+ 
+  handleClick(e) {
+    if (e.target.closest('#screenshot-selector-overlay')) return;
+    e.preventDefault();
+    e.stopPropagation();
+ 
+    const element = e.target;
+    this.captureElement(element);
+  }
+ 
+  handleKeyPress(e) {
+    if (e.key === 'Escape') {
+      this.cleanup();
+    }
+  }
+ 
+  addScrollableIndicators(element) {
+    const rect = element.getBoundingClientRect();
+    const hasVerticalScroll = element.scrollHeight > element.clientHeight;
+    const hasHorizontalScroll = element.scrollWidth > element.clientWidth;
+ 
+    if (!hasVerticalScroll && !hasHorizontalScroll) return;
+ 
+    // Calculate the full content dimensions
+    const fullHeight = element.scrollHeight;
+    const fullWidth = element.scrollWidth;
+    const visibleHeight = element.clientHeight;
+    const visibleWidth = element.clientWidth;
+ 
+    // Create indicator for full scrollable area
+    if (hasVerticalScroll || hasHorizontalScroll) {
+      const indicator = document.createElement('div');
+      indicator.className = 'screenshot-scrollable-indicator';
+      indicator.setAttribute('data-screenshot-indicator', 'true');
+ 
+      // Position it relative to the viewport
+      const style = {
+        left: `${rect.left}px`,
+        top: `${rect.top}px`,
+        width: hasHorizontalScroll ? `${fullWidth}px` : `${rect.width}px`,
+        height: hasVerticalScroll ? `${fullHeight}px` : `${rect.height}px`,
+      };
+ 
+      Object.assign(indicator.style, style);
+      document.body.appendChild(indicator);
+ 
+      // Add a label to show the full dimensions
+      const label = document.createElement('div');
+      label.setAttribute('data-screenshot-indicator', 'true');
+      label.style.cssText = `
+        position: fixed;
+        top: ${rect.top - 25}px;
+        left: ${rect.left}px;
+        background: rgba(255, 107, 107, 0.9);
+        color: white;
+        padding: 2px 6px;
+        border-radius: 3px;
+        font-size: 11px;
+        font-family: monospace;
+        z-index: 2147483647;
+        pointer-events: none;
+      `;
+ 
+      const scrollInfo = [];
+      if (hasVerticalScroll) scrollInfo.push(`H: ${fullHeight}px (visible: ${visibleHeight}px)`);
+      if (hasHorizontalScroll) scrollInfo.push(`W: ${fullWidth}px (visible: ${visibleWidth}px)`);
+      label.textContent = `Full content - ${scrollInfo.join(', ')}`;
+ 
+      document.body.appendChild(label);
+    }
+  }
+ 
+  removeScrollableIndicators() {
+    const indicators = document.querySelectorAll('[data-screenshot-indicator]');
+    indicators.forEach(indicator => indicator.remove());
+  }
+ 
+  // Helper method to generate a unique selector for an element
+  getElementSelector(element) {
+    // Escape helper for CSS identifiers (handles Tailwind classes like lg:pr-0)
+    const escapeIdent = (ident) => {
+      try {
+        if (window.CSS && typeof window.CSS.escape === 'function') {
+          return window.CSS.escape(ident);
+        }
+      } catch (_) {}
+      // Fallback: escape most punctuation characters
+      return String(ident).replace(/([ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, '\\$1');
+    };
+ 
+    if (element.id) {
+      return `#${escapeIdent(element.id)}`;
+    }
+ 
+    if (element.classList && element.classList.length) {
+      const classSelector = Array.from(element.classList)
+        .filter(Boolean)
+        .map(cls => `.${escapeIdent(cls)}`)
+        .join('');
+      if (classSelector) {
+        return `${element.tagName.toLowerCase()}${classSelector}`;
+      }
+    }
+ 
+    // Fallback to tag name and position
+    const siblings = Array.from(element.parentNode?.children || []);
+    const index = siblings.indexOf(element);
+    return `${element.tagName.toLowerCase()}:nth-child(${index + 1})`;
+  }
+ 
+    // Preserve computed styles for better rendering
+  preserveComputedStyles(clonedElement, originalElement) {
+    if (!originalElement || !clonedElement) return;
+ 
+    const computedStyle = window.getComputedStyle(originalElement);
+ 
+    // Comprehensive style properties including layout and icon-related ones
+    const importantStyles = [
+      // Typography and fonts
+      'font-family', 'font-size', 'font-weight', 'font-style', 'font-variant',
+      'text-align', 'text-decoration', 'text-transform', 'text-indent',
+      'line-height', 'letter-spacing', 'word-spacing', 'white-space',
+ 
+      // Colors and backgrounds
+      'color', 'background-color', 'background-image', 'background-size',
+      'background-position', 'background-repeat', 'background-attachment',
+ 
+      // Layout and positioning
+      'display', 'position', 'top', 'left', 'right', 'bottom',
+      'width', 'height', 'min-width', 'min-height', 'max-width', 'max-height',
+      'margin', 'padding', 'border', 'border-radius',
+      'float', 'clear', 'vertical-align',
+ 
+      // Flexbox and Grid
+      'flex', 'flex-direction', 'flex-wrap', 'flex-basis', 'flex-grow', 'flex-shrink',
+      'justify-content', 'align-items', 'align-self', 'align-content',
+      'grid', 'grid-template', 'grid-area', 'grid-column', 'grid-row',
+ 
+      // Visual effects
+      'opacity', 'visibility', 'overflow', 'overflow-x', 'overflow-y',
+      'box-shadow', 'text-shadow', 'transform', 'filter',
+      'z-index', 'cursor'
+    ];
+ 
+    // Apply computed styles
+    importantStyles.forEach(property => {
+      const value = computedStyle.getPropertyValue(property);
+      if (value && value !== 'none' && value !== 'normal' && value !== 'auto' && value !== 'initial') {
+        try {
+          clonedElement.style.setProperty(property, value, 'important');
+        } catch (e) {
+          // Skip properties that can't be set
+        }
+      }
+    });
+ 
+    // Handle pseudo-elements (::before and ::after) which often contain icons
+    this.preservePseudoElements(clonedElement, originalElement);
+ 
+    // Recursively apply to children
+    const originalChildren = Array.from(originalElement.children);
+    const clonedChildren = Array.from(clonedElement.children);
+ 
+    for (let i = 0; i < Math.min(originalChildren.length, clonedChildren.length); i++) {
+      this.preserveComputedStyles(clonedChildren[i], originalChildren[i]);
+    }
+  }
+ 
+     // Handle pseudo-elements that often contain icons
+   preservePseudoElements(clonedElement, originalElement) {
+     try {
+       ['::before', '::after'].forEach(pseudo => {
+         const pseudoStyle = window.getComputedStyle(originalElement, pseudo);
+         const content = pseudoStyle.getPropertyValue('content');
+ 
+         // If pseudo-element has content, try to preserve it
+         if (content && content !== 'none' && content !== '""') {
+           const pseudoProperties = [
+             'content', 'display', 'position', 'top', 'left', 'right', 'bottom',
+             'width', 'height', 'font-family', 'font-size', 'color',
+             'background-color', 'background-image', 'border', 'border-radius',
+             'transform', 'opacity'
+           ];
+ 
+           // Create inline style for pseudo-element
+           let selector = this.getElementSelector(clonedElement);
+           let pseudoCSS = `${selector}${pseudo} {`;
+           pseudoProperties.forEach(prop => {
+             const value = pseudoStyle.getPropertyValue(prop);
+             if (value && value !== 'none' && value !== 'normal') {
+               pseudoCSS += `${prop}: ${value} !important;`;
+             }
+           });
+           pseudoCSS += '}';
+ 
+           // Add to document head
+           const style = document.createElement('style');
+           style.textContent = pseudoCSS;
+           document.head.appendChild(style);
+         }
+       });
+     } catch (e) {
+       // Pseudo-element handling failed, continue without it
+     }
+   }
+ 
+   // Wait for fonts and resources to load properly
+   async waitForFontsAndResources(element) {
+     // Wait for document fonts to load
+     if (document.fonts && document.fonts.ready) {
+       try {
+         await document.fonts.ready;
+       } catch (e) {
+         // Font loading API not available or failed
+       }
+     }
+ 
+     // Check for icon fonts (Font Awesome, Material Icons, etc.)
+     const iconFontFamilies = [
+       'FontAwesome', 'Font Awesome', 'Font Awesome 5', 'Font Awesome 6',
+       'Material Icons', 'Material Icons Outlined', 'Material Icons Sharp',
+       'Ionicons', 'Feather', 'Lucide', 'Tabler Icons'
+     ];
+ 
+     // Force load any icon fonts found in the element
+     const allElements = [element, ...element.querySelectorAll('*')];
+     const fontPromises = [];
+ 
+     allElements.forEach(el => {
+       const computedStyle = window.getComputedStyle(el);
+       const fontFamily = computedStyle.fontFamily;
+ 
+       iconFontFamilies.forEach(iconFont => {
+         if (fontFamily.includes(iconFont)) {
+           // Create a test element to ensure font is loaded
+           const testEl = document.createElement('span');
+           testEl.style.fontFamily = iconFont;
+           testEl.style.position = 'absolute';
+           testEl.style.left = '-9999px';
+           testEl.textContent = '■'; // Use a test character
+           document.body.appendChild(testEl);
+ 
+           const fontPromise = new Promise(resolve => {
+             setTimeout(() => {
+               document.body.removeChild(testEl);
+               resolve();
+             }, 100);
+           });
+           fontPromises.push(fontPromise);
+         }
+       });
+     });
+ 
+     // Wait for all font loading attempts
+     await Promise.all(fontPromises);
+ 
+         // Additional wait for any remaining resources
+    await new Promise(resolve => setTimeout(resolve, 500));
+  }
+ 
+ 
+ 
+  async captureElement(element) {
+    try {
+      // Determine loading message based on clipboard settings
+      let loadingMessage = 'Capturing screenshot...';
+      if (this.copyToClipboard) {
+        const availability = this.checkClipboardAPIAvailability();
+        if (availability.available) {
+          loadingMessage = 'Capturing screenshot and preparing for clipboard...';
+        } else {
+          loadingMessage = 'Capturing screenshot... (Clipboard not available)';
+        }
+      }
+ 
+      // Show loading state
+      this.overlay.innerHTML = `
+        <div style="position: fixed; top: 20px; left: 20px; background: rgba(0,0,0,0.9); color: white; padding: 12px 16px; border-radius: 8px; z-index: 2147483647; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 14px; box-shadow: 0 4px 20px rgba(0,0,0,0.3);">
+          <div style="display: flex; align-items: center; gap: 10px;">
+            <div style="width: 16px; height: 16px; border: 2px solid #fff; border-top: 2px solid transparent; border-radius: 50%; animation: spin 1s linear infinite;"></div>
+            <span>${loadingMessage}</span>
+          </div>
+        </div>
+        <style>
+          @keyframes spin {
+            0% { transform: rotate(0deg); }
+            100% { transform: rotate(360deg); }
+          }
+        </style>
+      `;
+ 
+      // Remove highlight for clean capture first
+      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);
+ 
+            // Enhanced html2canvas configuration
+      const canvas = await html2canvas(element, {
+        useCORS: true,
+        allowTaint: false,
+        backgroundColor: this.background === 'transparent' ? null :
+                        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
+ 
+        // 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);
+ 
+      // Always download the image first (core functionality)
+      const link = 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 (after successful canvas generation and download)
+      let clipboardResult = null;
+      if (this.copyToClipboard) {
+        const availability = this.checkClipboardAPIAvailability();
+        if (availability.available) {
+          try {
+            // Attempt clipboard operation with comprehensive error handling
+            clipboardResult = await this.copyCanvasToClipboard(canvas);
+          } catch (error) {
+            // Ensure clipboard errors don't break the workflow - this is a safety net
+            // The copyCanvasToClipboard method should handle all errors internally
+            console.error('Unexpected clipboard error caught in captureElement:', error);
+            clipboardResult = { 
+              success: false, 
+              error: 'Unexpected clipboard error occurred',
+              errorType: 'unexpected'
+            };
+          }
+        } else {
+          clipboardResult = { 
+            success: false, 
+            error: availability.reason,
+            errorType: 'api_unavailable'
+          };
+        }
+      }
+ 
+      // Generate user feedback message based on clipboard results
+      let successMessage = 'Screenshot downloaded successfully!';
+      let messageColor = 'rgba(39, 174, 96, 0.95)'; // Green for success
+      
+      if (this.copyToClipboard) {
+        if (clipboardResult && clipboardResult.success) {
+          successMessage = 'Screenshot downloaded and copied to clipboard!';
+          // Keep green color for full success
+        } else {
+          // Download succeeded but clipboard failed - provide specific error feedback
+          const errorMessage = this.getClipboardErrorMessage(clipboardResult);
+          successMessage = `Screenshot downloaded successfully! ${errorMessage}`;
+          messageColor = 'rgba(243, 156, 18, 0.95)'; // Orange for partial success
+        }
+      }
+ 
+      this.overlay.innerHTML = `
+        <div style="position: fixed; top: 20px; left: 20px; background: ${messageColor}; color: white; padding: 12px 16px; border-radius: 8px; z-index: 2147483647; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 14px; box-shadow: 0 4px 20px rgba(0,0,0,0.3);">
+          <div style="display: flex; align-items: center; gap: 10px;">
+            <span>✅</span>
+            <span>${successMessage}</span>
+          </div>
+        </div>
+      `;
+ 
+      // Notify popup
+      chrome.runtime.sendMessage({ action: 'selectorStopped', reason: 'screenshot-taken' });
+ 
+      setTimeout(() => this.cleanup(), 2000);
+ 
+    } catch (error) {
+      console.error('Screenshot failed:', error);
+      
+      // Provide specific error messages for screenshot failures
+      let errorMessage = 'Screenshot capture failed. Please try again.';
+      
+      if (error.message) {
+        if (error.message.includes('html2canvas')) {
+          errorMessage = 'Screenshot rendering failed. Try selecting a different element or refresh the page.';
+        } else if (error.message.includes('timeout')) {
+          errorMessage = 'Screenshot capture timed out. Try selecting a smaller area or simpler element.';
+        } else if (error.message.includes('network') || error.message.includes('CORS')) {
+          errorMessage = 'Screenshot failed: Some content is blocked by security restrictions.';
+        } else if (error.message.includes('memory') || error.message.includes('quota')) {
+          errorMessage = 'Screenshot failed: Not enough memory. Try capturing a smaller area.';
+        } else if (error.message.includes('canvas')) {
+          errorMessage = 'Screenshot failed: Unable to create image. Try a different element.';
+        }
+      }
+      
+      this.overlay.innerHTML = `
+        <div style="position: fixed; top: 20px; left: 20px; background: rgba(231, 76, 60, 0.95); color: white; padding: 12px 16px; border-radius: 8px; z-index: 2147483647; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 14px; box-shadow: 0 4px 20px rgba(0,0,0,0.3);">
+          <div style="display: flex; align-items: center; gap: 10px;">
+            <span>❌</span>
+            <span>${errorMessage}</span>
+          </div>
+        </div>
+      `;
+      
+      // Notify popup about the failure
+      chrome.runtime.sendMessage({ action: 'selectorStopped', reason: 'screenshot-failed' });
+      
+      setTimeout(() => this.cleanup(), 3000);
+    }
+  }
+ 
+  /**
+   * Check if Clipboard API is available and supported
+   * @returns {{available: boolean, reason?: string}} Clipboard API availability status
+   */
+  checkClipboardAPIAvailability() {
+    // Check for basic Clipboard API support
+    if (!navigator.clipboard) {
+      return {
+        available: false,
+        reason: 'Clipboard API not available in this browser'
+      };
+    }
+ 
+    // Check for ClipboardItem constructor (required for image data)
+    if (!window.ClipboardItem) {
+      return {
+        available: false,
+        reason: 'ClipboardItem not supported in this browser'
+      };
+    }
+ 
+    // Check if we're in a secure context (HTTPS required for clipboard)
+    if (!window.isSecureContext) {
+      return {
+        available: false,
+        reason: 'Clipboard API requires a secure context (HTTPS)'
+      };
+    }
+ 
+    // Check for write permission (this is the basic check, actual permission is checked during write)
+    if (!navigator.clipboard.write) {
+      return {
+        available: false,
+        reason: 'Clipboard write functionality not available'
+      };
+    }
+ 
+    return {
+      available: true
+    };
+  }
+ 
+  /**
+   * Get user-friendly feedback message about clipboard feature availability
+   * @returns {{supported: boolean, message: string}} User feedback about clipboard support
+   */
+  getClipboardSupportFeedback() {
+    const availability = this.checkClipboardAPIAvailability();
+    
+    if (availability.available) {
+      return {
+        supported: true,
+        message: 'Clipboard copying is available and ready to use.'
+      };
+    }
+ 
+    // Provide user-friendly messages for different scenarios
+    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
+    };
+  }
+ 
+  /**
+   * Generate user-friendly error message for clipboard operation failures
+   * @param {Object} clipboardResult - Result object from clipboard operation
+   * @returns {string} User-friendly error message
+   */
+  getClipboardErrorMessage(clipboardResult) {
+    if (!clipboardResult || !clipboardResult.error) {
+      return 'Clipboard copy failed due to unknown error.';
+    }
+ 
+    const errorType = clipboardResult.errorType;
+    const errorMessage = clipboardResult.error;
+ 
+    // Provide contextual error messages based on error type
+    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.';
+      
+      case 'network_error':
+        return 'Clipboard copy failed due to network error. Please try again.';
+      
+      case 'canvas_conversion':
+        return 'Clipboard copy failed: Unable to prepare image. Try capturing a different element.';
+      
+      case 'clipboard_item_creation':
+        return 'Clipboard copy not supported: Your browser doesn\'t support image clipboard operations.';
+      
+      case 'invalid_state':
+        return 'Clipboard copy failed: Browser clipboard is busy. Please try again.';
+      
+      case 'data_error':
+        return 'Clipboard copy failed: Invalid image data. Please try capturing again.';
+      
+      case 'unexpected':
+        return 'Clipboard copy failed due to unexpected error. Screenshot was still downloaded.';
+      
+      default:
+        // For unknown error types, try to extract meaningful info from the error message
+        if (errorMessage.includes('permission') || errorMessage.includes('denied')) {
+          return 'Clipboard copy failed: Access denied. Check browser permissions.';
+        } else if (errorMessage.includes('not supported') || errorMessage.includes('unavailable')) {
+          return 'Clipboard copy not supported in this browser.';
+        } else if (errorMessage.includes('timeout') || errorMessage.includes('timed out')) {
+          return 'Clipboard copy timed out. Try capturing a smaller area.';
+        } else if (errorMessage.includes('large') || errorMessage.includes('size')) {
+          return 'Clipboard copy failed: Image too large for clipboard.';
+        } else {
+          return `Clipboard copy failed: ${errorMessage}`;
+        }
+    }
+  }
+ 
+  /**
+   * Copy canvas content to clipboard using the Clipboard API
+   * @param {HTMLCanvasElement} canvas - The canvas element to copy
+   * @returns {Promise<{success: boolean, error?: string, errorType?: string}>} Result of clipboard operation
+   */
+  async copyCanvasToClipboard(canvas) {
+    try {
+      // Check clipboard API availability first
+      const availability = this.checkClipboardAPIAvailability();
+      if (!availability.available) {
+        return { 
+          success: false, 
+          error: availability.reason,
+          errorType: 'api_unavailable'
+        };
+      }
+ 
+      // Convert canvas to blob in PNG format with timeout
+      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)
+        )
+      ]);
+ 
+      // Validate blob size (prevent extremely large clipboard operations)
+      const maxBlobSize = 50 * 1024 * 1024; // 50MB limit
+      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'
+        };
+      }
+ 
+      // Create ClipboardItem with PNG image data
+      let clipboardItem;
+      try {
+        clipboardItem = new 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'
+        };
+      }
+ 
+      // Write to clipboard with timeout
+      await Promise.race([
+        navigator.clipboard.write([clipboardItem]),
+        new Promise((_, reject) => 
+          setTimeout(() => reject(new Error('Clipboard write operation timed out')), 15000)
+        )
+      ]);
+ 
+      return { success: true };
+ 
+    } catch (error) {
+      console.error('Clipboard operation failed:', error);
+      
+      // Comprehensive error handling with specific error types and user-friendly messages
+      let errorMessage = error.message || 'Unknown clipboard error';
+      let errorType = 'unknown';
+      
+      // Permission-related errors
+      if (error.name === 'NotAllowedError') {
+        errorMessage = 'Clipboard access denied. Please allow clipboard permissions in your browser settings.';
+        errorType = 'permission_denied';
+      } 
+      // API support errors
+      else if (error.name === 'NotSupportedError') {
+        errorMessage = 'Clipboard API not supported in this browser context.';
+        errorType = 'not_supported';
+      }
+      // Security context errors
+      else if (error.name === 'SecurityError') {
+        errorMessage = 'Clipboard access blocked by security policy. Try using HTTPS.';
+        errorType = 'security_error';
+      }
+      // Network or resource errors
+      else if (error.name === 'NetworkError') {
+        errorMessage = 'Network error during clipboard operation. Please try again.';
+        errorType = 'network_error';
+      }
+      // Timeout errors
+      else if (error.message.includes('timed out')) {
+        errorMessage = 'Clipboard operation timed out. The image may be too large.';
+        errorType = 'timeout';
+      }
+      // Canvas conversion errors
+      else if (error.message.includes('canvas') || error.message.includes('blob')) {
+        errorMessage = 'Failed to prepare image for clipboard. Try capturing a different area.';
+        errorType = 'canvas_conversion';
+      }
+      // Quota or storage errors
+      else if (error.name === 'QuotaExceededError' || error.message.includes('quota')) {
+        errorMessage = 'Clipboard storage quota exceeded. Try capturing a smaller image.';
+        errorType = 'quota_exceeded';
+      }
+      // DOM or state errors
+      else if (error.name === 'InvalidStateError') {
+        errorMessage = 'Clipboard is in an invalid state. Please try again.';
+        errorType = 'invalid_state';
+      }
+      // Data errors
+      else if (error.name === 'DataError') {
+        errorMessage = 'Invalid image data for clipboard. Please try again.';
+        errorType = 'data_error';
+      }
+      // Generic fallback for unknown errors
+      else {
+        errorMessage = `Clipboard operation failed: ${error.message}`;
+        errorType = 'unknown';
+      }
+ 
+      return { 
+        success: false, 
+        error: errorMessage,
+        errorType: errorType
+      };
+    }
+  }
+ 
+  cleanup() {
+    document.removeEventListener('mouseover', this.handleMouseOver);
+    document.removeEventListener('click', this.handleClick, true);
+    document.removeEventListener('keydown', this.handleKeyPress);
+ 
+    if (this.currentHighlight) {
+      this.currentHighlight.classList.remove('screenshot-highlight');
+    }
+ 
+    // Remove any scrollable indicators
+    this.removeScrollableIndicators();
+ 
+    if (this.overlay) {
+      this.overlay.remove();
+      this.overlay = null;
+    }
+ 
+    if (this.style) {
+      this.style.remove();
+      this.style = null;
+    }
+ 
+    this.isActive = false;
+    this.currentHighlight = null;
+  }
+}
+ 
+// Global instance
+let screenshotSelector = new ScreenshotSelector();
+ 
+// Add load indicator
+console.log('Full Screenshot Selector content script loaded');
+ 
+// Listen for messages from popup
+chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
+  try {
+  switch (message.action) {
+    case 'startSelector':
+      // Extract clipboard preference from message, default to true for backward compatibility
+      const copyToClipboard = message.copyToClipboard !== undefined ? message.copyToClipboard : true;
+      screenshotSelector.init(message.background, copyToClipboard);
+      sendResponse({ success: true });
+      break;
+ 
+    case 'stopSelector':
+      screenshotSelector.cleanup();
+      sendResponse({ success: true });
+      break;
+ 
+    case 'checkState':
+      sendResponse({ isActive: screenshotSelector.isActive });
+      break;
+ 
+    case 'checkClipboardSupport':
+      const availability = screenshotSelector.checkClipboardAPIAvailability();
+      const feedback = screenshotSelector.getClipboardSupportFeedback();
+      sendResponse({ 
+        availability: availability,
+        feedback: feedback
+      });
+      break;
+ 
+      default:
+        sendResponse({ error: 'Unknown action: ' + message.action });
+    }
+  } catch (error) {
+    console.error('Content script error:', error);
+    sendResponse({ error: error.message });
+  }
+ 
+  // Important: return true to indicate async response
+  return true;
+});
+ 
+// Handle keyboard shortcut
+chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
+  if (message.action === 'activateFromShortcut') {
+    if (!screenshotSelector.isActive) {
+      screenshotSelector.init('black', true); // Default background and clipboard enabled for shortcut
+    }
+  }
+});
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/favicon.png b/coverage/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..c1525b811a167671e9de1fa78aab9f5c0b61cef7 GIT binary patch literal 445 zcmV;u0Yd(XP))rP{nL}Ln%S7`m{0DjX9TLF* zFCb$4Oi7vyLOydb!7n&^ItCzb-%BoB`=x@N2jll2Nj`kauio%aw_@fe&*}LqlFT43 z8doAAe))z_%=P%v^@JHp3Hjhj^6*Kr_h|g_Gr?ZAa&y>wxHE99Gk>A)2MplWz2xdG zy8VD2J|Uf#EAw*bo5O*PO_}X2Tob{%bUoO2G~T`@%S6qPyc}VkhV}UifBuRk>%5v( z)x7B{I~z*k<7dv#5tC+m{km(D087J4O%+<<;K|qwefb6@GSX45wCK}Sn*> + + + + Code coverage report for All files + + + + + + + + + +
+
+

All files

+
+ +
+ 0% + Statements + 0/463 +
+ + +
+ 0% + Branches + 0/263 +
+ + +
+ 0% + Functions + 0/61 +
+ + +
+ 0% + Lines + 0/445 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
content.js +
+
0%0/3710%0/2140%0/440%0/356
popup.js +
+
0%0/920%0/490%0/170%0/89
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/base.css b/coverage/lcov-report/base.css new file mode 100644 index 0000000..f418035 --- /dev/null +++ b/coverage/lcov-report/base.css @@ -0,0 +1,224 @@ +body, html { + margin:0; padding: 0; + height: 100%; +} +body { + font-family: Helvetica Neue, Helvetica, Arial; + font-size: 14px; + color:#333; +} +.small { font-size: 12px; } +*, *:after, *:before { + -webkit-box-sizing:border-box; + -moz-box-sizing:border-box; + box-sizing:border-box; + } +h1 { font-size: 20px; margin: 0;} +h2 { font-size: 14px; } +pre { + font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace; + margin: 0; + padding: 0; + -moz-tab-size: 2; + -o-tab-size: 2; + tab-size: 2; +} +a { color:#0074D9; text-decoration:none; } +a:hover { text-decoration:underline; } +.strong { font-weight: bold; } +.space-top1 { padding: 10px 0 0 0; } +.pad2y { padding: 20px 0; } +.pad1y { padding: 10px 0; } +.pad2x { padding: 0 20px; } +.pad2 { padding: 20px; } +.pad1 { padding: 10px; } +.space-left2 { padding-left:55px; } +.space-right2 { padding-right:20px; } +.center { text-align:center; } +.clearfix { display:block; } +.clearfix:after { + content:''; + display:block; + height:0; + clear:both; + visibility:hidden; + } +.fl { float: left; } +@media only screen and (max-width:640px) { + .col3 { width:100%; max-width:100%; } + .hide-mobile { display:none!important; } +} + +.quiet { + color: #7f7f7f; + color: rgba(0,0,0,0.5); +} +.quiet a { opacity: 0.7; } + +.fraction { + font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; + font-size: 10px; + color: #555; + background: #E8E8E8; + padding: 4px 5px; + border-radius: 3px; + vertical-align: middle; +} + +div.path a:link, div.path a:visited { color: #333; } +table.coverage { + border-collapse: collapse; + margin: 10px 0 0 0; + padding: 0; +} + +table.coverage td { + margin: 0; + padding: 0; + vertical-align: top; +} +table.coverage td.line-count { + text-align: right; + padding: 0 5px 0 20px; +} +table.coverage td.line-coverage { + text-align: right; + padding-right: 10px; + min-width:20px; +} + +table.coverage td span.cline-any { + display: inline-block; + padding: 0 5px; + width: 100%; +} +.missing-if-branch { + display: inline-block; + margin-right: 5px; + border-radius: 3px; + position: relative; + padding: 0 4px; + background: #333; + color: yellow; +} + +.skip-if-branch { + display: none; + margin-right: 10px; + position: relative; + padding: 0 4px; + background: #ccc; + color: white; +} +.missing-if-branch .typ, .skip-if-branch .typ { + color: inherit !important; +} +.coverage-summary { + border-collapse: collapse; + width: 100%; +} +.coverage-summary tr { border-bottom: 1px solid #bbb; } +.keyline-all { border: 1px solid #ddd; } +.coverage-summary td, .coverage-summary th { padding: 10px; } +.coverage-summary tbody { border: 1px solid #bbb; } +.coverage-summary td { border-right: 1px solid #bbb; } +.coverage-summary td:last-child { border-right: none; } +.coverage-summary th { + text-align: left; + font-weight: normal; + white-space: nowrap; +} +.coverage-summary th.file { border-right: none !important; } +.coverage-summary th.pct { } +.coverage-summary th.pic, +.coverage-summary th.abs, +.coverage-summary td.pct, +.coverage-summary td.abs { text-align: right; } +.coverage-summary td.file { white-space: nowrap; } +.coverage-summary td.pic { min-width: 120px !important; } +.coverage-summary tfoot td { } + +.coverage-summary .sorter { + height: 10px; + width: 7px; + display: inline-block; + margin-left: 0.5em; + background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; +} +.coverage-summary .sorted .sorter { + background-position: 0 -20px; +} +.coverage-summary .sorted-desc .sorter { + background-position: 0 -10px; +} +.status-line { height: 10px; } +/* yellow */ +.cbranch-no { background: yellow !important; color: #111; } +/* dark red */ +.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 } +.low .chart { border:1px solid #C21F39 } +.highlighted, +.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{ + background: #C21F39 !important; +} +/* medium red */ +.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE } +/* light red */ +.low, .cline-no { background:#FCE1E5 } +/* light green */ +.high, .cline-yes { background:rgb(230,245,208) } +/* medium green */ +.cstat-yes { background:rgb(161,215,106) } +/* dark green */ +.status-line.high, .high .cover-fill { background:rgb(77,146,33) } +.high .chart { border:1px solid rgb(77,146,33) } +/* dark yellow (gold) */ +.status-line.medium, .medium .cover-fill { background: #f9cd0b; } +.medium .chart { border:1px solid #f9cd0b; } +/* light yellow */ +.medium { background: #fff4c2; } + +.cstat-skip { background: #ddd; color: #111; } +.fstat-skip { background: #ddd; color: #111 !important; } +.cbranch-skip { background: #ddd !important; color: #111; } + +span.cline-neutral { background: #eaeaea; } + +.coverage-summary td.empty { + opacity: .5; + padding-top: 4px; + padding-bottom: 4px; + line-height: 1; + color: #888; +} + +.cover-fill, .cover-empty { + display:inline-block; + height: 12px; +} +.chart { + line-height: 0; +} +.cover-empty { + background: white; +} +.cover-full { + border-right: none !important; +} +pre.prettyprint { + border: none !important; + padding: 0 !important; + margin: 0 !important; +} +.com { color: #999 !important; } +.ignore-none { color: #999; font-weight: normal; } + +.wrapper { + min-height: 100%; + height: auto !important; + height: 100%; + margin: 0 auto -48px; +} +.footer, .push { + height: 48px; +} diff --git a/coverage/lcov-report/block-navigation.js b/coverage/lcov-report/block-navigation.js new file mode 100644 index 0000000..cc12130 --- /dev/null +++ b/coverage/lcov-report/block-navigation.js @@ -0,0 +1,87 @@ +/* eslint-disable */ +var jumpToCode = (function init() { + // Classes of code we would like to highlight in the file view + var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no']; + + // Elements to highlight in the file listing view + var fileListingElements = ['td.pct.low']; + + // We don't want to select elements that are direct descendants of another match + var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > ` + + // Selecter that finds elements on the page to which we can jump + var selector = + fileListingElements.join(', ') + + ', ' + + notSelector + + missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b` + + // The NodeList of matching elements + var missingCoverageElements = document.querySelectorAll(selector); + + var currentIndex; + + function toggleClass(index) { + missingCoverageElements + .item(currentIndex) + .classList.remove('highlighted'); + missingCoverageElements.item(index).classList.add('highlighted'); + } + + function makeCurrent(index) { + toggleClass(index); + currentIndex = index; + missingCoverageElements.item(index).scrollIntoView({ + behavior: 'smooth', + block: 'center', + inline: 'center' + }); + } + + function goToPrevious() { + var nextIndex = 0; + if (typeof currentIndex !== 'number' || currentIndex === 0) { + nextIndex = missingCoverageElements.length - 1; + } else if (missingCoverageElements.length > 1) { + nextIndex = currentIndex - 1; + } + + makeCurrent(nextIndex); + } + + function goToNext() { + var nextIndex = 0; + + if ( + typeof currentIndex === 'number' && + currentIndex < missingCoverageElements.length - 1 + ) { + nextIndex = currentIndex + 1; + } + + makeCurrent(nextIndex); + } + + return function jump(event) { + if ( + document.getElementById('fileSearch') === document.activeElement && + document.activeElement != null + ) { + // if we're currently focused on the search input, we don't want to navigate + return; + } + + switch (event.which) { + case 78: // n + case 74: // j + goToNext(); + break; + case 66: // b + case 75: // k + case 80: // p + goToPrevious(); + break; + } + }; +})(); +window.addEventListener('keydown', jumpToCode); diff --git a/coverage/lcov-report/content.js.html b/coverage/lcov-report/content.js.html new file mode 100644 index 0000000..adf9f32 --- /dev/null +++ b/coverage/lcov-report/content.js.html @@ -0,0 +1,2893 @@ + + + + + + Code coverage report for content.js + + + + + + + + + +
+
+

All files content.js

+
+ +
+ 0% + Statements + 0/371 +
+ + +
+ 0% + Branches + 0/214 +
+ + +
+ 0% + Functions + 0/44 +
+ + +
+ 0% + Lines + 0/356 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +516 +517 +518 +519 +520 +521 +522 +523 +524 +525 +526 +527 +528 +529 +530 +531 +532 +533 +534 +535 +536 +537 +538 +539 +540 +541 +542 +543 +544 +545 +546 +547 +548 +549 +550 +551 +552 +553 +554 +555 +556 +557 +558 +559 +560 +561 +562 +563 +564 +565 +566 +567 +568 +569 +570 +571 +572 +573 +574 +575 +576 +577 +578 +579 +580 +581 +582 +583 +584 +585 +586 +587 +588 +589 +590 +591 +592 +593 +594 +595 +596 +597 +598 +599 +600 +601 +602 +603 +604 +605 +606 +607 +608 +609 +610 +611 +612 +613 +614 +615 +616 +617 +618 +619 +620 +621 +622 +623 +624 +625 +626 +627 +628 +629 +630 +631 +632 +633 +634 +635 +636 +637 +638 +639 +640 +641 +642 +643 +644 +645 +646 +647 +648 +649 +650 +651 +652 +653 +654 +655 +656 +657 +658 +659 +660 +661 +662 +663 +664 +665 +666 +667 +668 +669 +670 +671 +672 +673 +674 +675 +676 +677 +678 +679 +680 +681 +682 +683 +684 +685 +686 +687 +688 +689 +690 +691 +692 +693 +694 +695 +696 +697 +698 +699 +700 +701 +702 +703 +704 +705 +706 +707 +708 +709 +710 +711 +712 +713 +714 +715 +716 +717 +718 +719 +720 +721 +722 +723 +724 +725 +726 +727 +728 +729 +730 +731 +732 +733 +734 +735 +736 +737 +738 +739 +740 +741 +742 +743 +744 +745 +746 +747 +748 +749 +750 +751 +752 +753 +754 +755 +756 +757 +758 +759 +760 +761 +762 +763 +764 +765 +766 +767 +768 +769 +770 +771 +772 +773 +774 +775 +776 +777 +778 +779 +780 +781 +782 +783 +784 +785 +786 +787 +788 +789 +790 +791 +792 +793 +794 +795 +796 +797 +798 +799 +800 +801 +802 +803 +804 +805 +806 +807 +808 +809 +810 +811 +812 +813 +814 +815 +816 +817 +818 +819 +820 +821 +822 +823 +824 +825 +826 +827 +828 +829 +830 +831 +832 +833 +834 +835 +836 +837 +838 +839 +840 +841 +842 +843 +844 +845 +846 +847 +848 +849 +850 +851 +852 +853 +854 +855 +856 +857 +858 +859 +860 +861 +862 +863 +864 +865 +866 +867 +868 +869 +870 +871 +872 +873 +874 +875 +876 +877 +878 +879 +880 +881 +882 +883 +884 +885 +886 +887 +888 +889 +890 +891 +892 +893 +894 +895 +896 +897 +898 +899 +900 +901 +902 +903 +904 +905 +906 +907 +908 +909 +910 +911 +912 +913 +914 +915 +916 +917 +918 +919 +920 +921 +922 +923 +924 +925 +926 +927 +928 +929 +930 +931 +932 +933 +934 +935 +936 +937  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
// Content script for Full Screenshot Selector Chrome Extension
+class ScreenshotSelector {
+  constructor() {
+    this.isActive = false;
+    this.currentHighlight = null;
+    this.overlay = null;
+    this.style = null;
+    this.background = 'black';
+    this.copyToClipboard = true; // Default to true
+  }
+ 
+  async init(background = 'black', copyToClipboard = true) {
+    if (this.isActive) {
+      console.log('Screenshot selector already active');
+      return;
+    }
+ 
+    this.background = background;
+    this.copyToClipboard = copyToClipboard;
+    this.isActive = true;
+ 
+    this.createUI();
+    this.addEventListeners();
+  }
+ 
+  createUI() {
+    // Create overlay UI
+    this.overlay = document.createElement('div');
+    this.overlay.id = 'screenshot-selector-overlay';
+    this.overlay.innerHTML = `
+      <div style="position: fixed; top: 20px; left: 20px; background: rgba(0,0,0,0.9); color: white; padding: 12px 16px; border-radius: 8px; z-index: 2147483647; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 14px; box-shadow: 0 4px 20px rgba(0,0,0,0.3); backdrop-filter: blur(10px);">
+        <div style="display: flex; align-items: center; gap: 10px;">
+          <span>📸</span>
+          <span>Hover over elements and click to capture</span>
+          <button id="screenshot-cancel" style="margin-left: 10px; padding: 4px 8px; background: #ff4757; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px;">ESC</button>
+        </div>
+      </div>
+    `;
+ 
+    // Add hover highlighting styles
+    this.style = document.createElement('style');
+    this.style.textContent = `
+      .screenshot-highlight {
+        outline: 3px solid #00d2ff !important;
+        outline-offset: 2px !important;
+        cursor: crosshair !important;
+        position: relative !important;
+      }
+      .screenshot-highlight::after {
+        content: '';
+        position: absolute;
+        top: -5px;
+        left: -5px;
+        right: -5px;
+        bottom: -5px;
+        background: rgba(0, 210, 255, 0.1) !important;
+        pointer-events: none !important;
+        z-index: 2147483646 !important;
+      }
+      .screenshot-scrollable-indicator {
+        position: absolute !important;
+        border: 2px dotted #ff6b6b !important;
+        background: rgba(255, 107, 107, 0.05) !important;
+        pointer-events: none !important;
+        z-index: 2147483645 !important;
+      }
+      #screenshot-selector-overlay * {
+        cursor: default !important;
+      }
+    `;
+ 
+    document.head.appendChild(this.style);
+    document.body.appendChild(this.overlay);
+ 
+    // Cancel button functionality
+    document.getElementById('screenshot-cancel').onclick = () => this.cleanup();
+  }
+ 
+  addEventListeners() {
+    this.handleMouseOver = this.handleMouseOver.bind(this);
+    this.handleClick = this.handleClick.bind(this);
+    this.handleKeyPress = this.handleKeyPress.bind(this);
+ 
+    document.addEventListener('mouseover', this.handleMouseOver);
+    document.addEventListener('click', this.handleClick, true);
+    document.addEventListener('keydown', this.handleKeyPress);
+  }
+ 
+  handleMouseOver(e) {
+    if (e.target.closest('#screenshot-selector-overlay')) return;
+ 
+    // Clean up previous highlights and indicators
+    if (this.currentHighlight) {
+      this.currentHighlight.classList.remove('screenshot-highlight');
+      this.removeScrollableIndicators();
+    }
+ 
+    this.currentHighlight = e.target;
+    e.target.classList.add('screenshot-highlight');
+ 
+    // Add scrollable content indicators
+    this.addScrollableIndicators(e.target);
+  }
+ 
+  handleClick(e) {
+    if (e.target.closest('#screenshot-selector-overlay')) return;
+    e.preventDefault();
+    e.stopPropagation();
+ 
+    const element = e.target;
+    this.captureElement(element);
+  }
+ 
+  handleKeyPress(e) {
+    if (e.key === 'Escape') {
+      this.cleanup();
+    }
+  }
+ 
+  addScrollableIndicators(element) {
+    const rect = element.getBoundingClientRect();
+    const hasVerticalScroll = element.scrollHeight > element.clientHeight;
+    const hasHorizontalScroll = element.scrollWidth > element.clientWidth;
+ 
+    if (!hasVerticalScroll && !hasHorizontalScroll) return;
+ 
+    // Calculate the full content dimensions
+    const fullHeight = element.scrollHeight;
+    const fullWidth = element.scrollWidth;
+    const visibleHeight = element.clientHeight;
+    const visibleWidth = element.clientWidth;
+ 
+    // Create indicator for full scrollable area
+    if (hasVerticalScroll || hasHorizontalScroll) {
+      const indicator = document.createElement('div');
+      indicator.className = 'screenshot-scrollable-indicator';
+      indicator.setAttribute('data-screenshot-indicator', 'true');
+ 
+      // Position it relative to the viewport
+      const style = {
+        left: `${rect.left}px`,
+        top: `${rect.top}px`,
+        width: hasHorizontalScroll ? `${fullWidth}px` : `${rect.width}px`,
+        height: hasVerticalScroll ? `${fullHeight}px` : `${rect.height}px`,
+      };
+ 
+      Object.assign(indicator.style, style);
+      document.body.appendChild(indicator);
+ 
+      // Add a label to show the full dimensions
+      const label = document.createElement('div');
+      label.setAttribute('data-screenshot-indicator', 'true');
+      label.style.cssText = `
+        position: fixed;
+        top: ${rect.top - 25}px;
+        left: ${rect.left}px;
+        background: rgba(255, 107, 107, 0.9);
+        color: white;
+        padding: 2px 6px;
+        border-radius: 3px;
+        font-size: 11px;
+        font-family: monospace;
+        z-index: 2147483647;
+        pointer-events: none;
+      `;
+ 
+      const scrollInfo = [];
+      if (hasVerticalScroll) scrollInfo.push(`H: ${fullHeight}px (visible: ${visibleHeight}px)`);
+      if (hasHorizontalScroll) scrollInfo.push(`W: ${fullWidth}px (visible: ${visibleWidth}px)`);
+      label.textContent = `Full content - ${scrollInfo.join(', ')}`;
+ 
+      document.body.appendChild(label);
+    }
+  }
+ 
+  removeScrollableIndicators() {
+    const indicators = document.querySelectorAll('[data-screenshot-indicator]');
+    indicators.forEach(indicator => indicator.remove());
+  }
+ 
+  // Helper method to generate a unique selector for an element
+  getElementSelector(element) {
+    // Escape helper for CSS identifiers (handles Tailwind classes like lg:pr-0)
+    const escapeIdent = (ident) => {
+      try {
+        if (window.CSS && typeof window.CSS.escape === 'function') {
+          return window.CSS.escape(ident);
+        }
+      } catch (_) {}
+      // Fallback: escape most punctuation characters
+      return String(ident).replace(/([ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, '\\$1');
+    };
+ 
+    if (element.id) {
+      return `#${escapeIdent(element.id)}`;
+    }
+ 
+    if (element.classList && element.classList.length) {
+      const classSelector = Array.from(element.classList)
+        .filter(Boolean)
+        .map(cls => `.${escapeIdent(cls)}`)
+        .join('');
+      if (classSelector) {
+        return `${element.tagName.toLowerCase()}${classSelector}`;
+      }
+    }
+ 
+    // Fallback to tag name and position
+    const siblings = Array.from(element.parentNode?.children || []);
+    const index = siblings.indexOf(element);
+    return `${element.tagName.toLowerCase()}:nth-child(${index + 1})`;
+  }
+ 
+    // Preserve computed styles for better rendering
+  preserveComputedStyles(clonedElement, originalElement) {
+    if (!originalElement || !clonedElement) return;
+ 
+    const computedStyle = window.getComputedStyle(originalElement);
+ 
+    // Comprehensive style properties including layout and icon-related ones
+    const importantStyles = [
+      // Typography and fonts
+      'font-family', 'font-size', 'font-weight', 'font-style', 'font-variant',
+      'text-align', 'text-decoration', 'text-transform', 'text-indent',
+      'line-height', 'letter-spacing', 'word-spacing', 'white-space',
+ 
+      // Colors and backgrounds
+      'color', 'background-color', 'background-image', 'background-size',
+      'background-position', 'background-repeat', 'background-attachment',
+ 
+      // Layout and positioning
+      'display', 'position', 'top', 'left', 'right', 'bottom',
+      'width', 'height', 'min-width', 'min-height', 'max-width', 'max-height',
+      'margin', 'padding', 'border', 'border-radius',
+      'float', 'clear', 'vertical-align',
+ 
+      // Flexbox and Grid
+      'flex', 'flex-direction', 'flex-wrap', 'flex-basis', 'flex-grow', 'flex-shrink',
+      'justify-content', 'align-items', 'align-self', 'align-content',
+      'grid', 'grid-template', 'grid-area', 'grid-column', 'grid-row',
+ 
+      // Visual effects
+      'opacity', 'visibility', 'overflow', 'overflow-x', 'overflow-y',
+      'box-shadow', 'text-shadow', 'transform', 'filter',
+      'z-index', 'cursor'
+    ];
+ 
+    // Apply computed styles
+    importantStyles.forEach(property => {
+      const value = computedStyle.getPropertyValue(property);
+      if (value && value !== 'none' && value !== 'normal' && value !== 'auto' && value !== 'initial') {
+        try {
+          clonedElement.style.setProperty(property, value, 'important');
+        } catch (e) {
+          // Skip properties that can't be set
+        }
+      }
+    });
+ 
+    // Handle pseudo-elements (::before and ::after) which often contain icons
+    this.preservePseudoElements(clonedElement, originalElement);
+ 
+    // Recursively apply to children
+    const originalChildren = Array.from(originalElement.children);
+    const clonedChildren = Array.from(clonedElement.children);
+ 
+    for (let i = 0; i < Math.min(originalChildren.length, clonedChildren.length); i++) {
+      this.preserveComputedStyles(clonedChildren[i], originalChildren[i]);
+    }
+  }
+ 
+     // Handle pseudo-elements that often contain icons
+   preservePseudoElements(clonedElement, originalElement) {
+     try {
+       ['::before', '::after'].forEach(pseudo => {
+         const pseudoStyle = window.getComputedStyle(originalElement, pseudo);
+         const content = pseudoStyle.getPropertyValue('content');
+ 
+         // If pseudo-element has content, try to preserve it
+         if (content && content !== 'none' && content !== '""') {
+           const pseudoProperties = [
+             'content', 'display', 'position', 'top', 'left', 'right', 'bottom',
+             'width', 'height', 'font-family', 'font-size', 'color',
+             'background-color', 'background-image', 'border', 'border-radius',
+             'transform', 'opacity'
+           ];
+ 
+           // Create inline style for pseudo-element
+           let selector = this.getElementSelector(clonedElement);
+           let pseudoCSS = `${selector}${pseudo} {`;
+           pseudoProperties.forEach(prop => {
+             const value = pseudoStyle.getPropertyValue(prop);
+             if (value && value !== 'none' && value !== 'normal') {
+               pseudoCSS += `${prop}: ${value} !important;`;
+             }
+           });
+           pseudoCSS += '}';
+ 
+           // Add to document head
+           const style = document.createElement('style');
+           style.textContent = pseudoCSS;
+           document.head.appendChild(style);
+         }
+       });
+     } catch (e) {
+       // Pseudo-element handling failed, continue without it
+     }
+   }
+ 
+   // Wait for fonts and resources to load properly
+   async waitForFontsAndResources(element) {
+     // Wait for document fonts to load
+     if (document.fonts && document.fonts.ready) {
+       try {
+         await document.fonts.ready;
+       } catch (e) {
+         // Font loading API not available or failed
+       }
+     }
+ 
+     // Check for icon fonts (Font Awesome, Material Icons, etc.)
+     const iconFontFamilies = [
+       'FontAwesome', 'Font Awesome', 'Font Awesome 5', 'Font Awesome 6',
+       'Material Icons', 'Material Icons Outlined', 'Material Icons Sharp',
+       'Ionicons', 'Feather', 'Lucide', 'Tabler Icons'
+     ];
+ 
+     // Force load any icon fonts found in the element
+     const allElements = [element, ...element.querySelectorAll('*')];
+     const fontPromises = [];
+ 
+     allElements.forEach(el => {
+       const computedStyle = window.getComputedStyle(el);
+       const fontFamily = computedStyle.fontFamily;
+ 
+       iconFontFamilies.forEach(iconFont => {
+         if (fontFamily.includes(iconFont)) {
+           // Create a test element to ensure font is loaded
+           const testEl = document.createElement('span');
+           testEl.style.fontFamily = iconFont;
+           testEl.style.position = 'absolute';
+           testEl.style.left = '-9999px';
+           testEl.textContent = '■'; // Use a test character
+           document.body.appendChild(testEl);
+ 
+           const fontPromise = new Promise(resolve => {
+             setTimeout(() => {
+               document.body.removeChild(testEl);
+               resolve();
+             }, 100);
+           });
+           fontPromises.push(fontPromise);
+         }
+       });
+     });
+ 
+     // Wait for all font loading attempts
+     await Promise.all(fontPromises);
+ 
+         // Additional wait for any remaining resources
+    await new Promise(resolve => setTimeout(resolve, 500));
+  }
+ 
+ 
+ 
+  async captureElement(element) {
+    try {
+      // Determine loading message based on clipboard settings
+      let loadingMessage = 'Capturing screenshot...';
+      if (this.copyToClipboard) {
+        const availability = this.checkClipboardAPIAvailability();
+        if (availability.available) {
+          loadingMessage = 'Capturing screenshot and preparing for clipboard...';
+        } else {
+          loadingMessage = 'Capturing screenshot... (Clipboard not available)';
+        }
+      }
+ 
+      // Show loading state
+      this.overlay.innerHTML = `
+        <div style="position: fixed; top: 20px; left: 20px; background: rgba(0,0,0,0.9); color: white; padding: 12px 16px; border-radius: 8px; z-index: 2147483647; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 14px; box-shadow: 0 4px 20px rgba(0,0,0,0.3);">
+          <div style="display: flex; align-items: center; gap: 10px;">
+            <div style="width: 16px; height: 16px; border: 2px solid #fff; border-top: 2px solid transparent; border-radius: 50%; animation: spin 1s linear infinite;"></div>
+            <span>${loadingMessage}</span>
+          </div>
+        </div>
+        <style>
+          @keyframes spin {
+            0% { transform: rotate(0deg); }
+            100% { transform: rotate(360deg); }
+          }
+        </style>
+      `;
+ 
+      // Remove highlight for clean capture first
+      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);
+ 
+            // Enhanced html2canvas configuration
+      const canvas = await html2canvas(element, {
+        useCORS: true,
+        allowTaint: false,
+        backgroundColor: this.background === 'transparent' ? null :
+                        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
+ 
+        // 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);
+ 
+      // Always download the image first (core functionality)
+      const link = 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 (after successful canvas generation and download)
+      let clipboardResult = null;
+      if (this.copyToClipboard) {
+        const availability = this.checkClipboardAPIAvailability();
+        if (availability.available) {
+          try {
+            // Attempt clipboard operation with comprehensive error handling
+            clipboardResult = await this.copyCanvasToClipboard(canvas);
+          } catch (error) {
+            // Ensure clipboard errors don't break the workflow - this is a safety net
+            // The copyCanvasToClipboard method should handle all errors internally
+            console.error('Unexpected clipboard error caught in captureElement:', error);
+            clipboardResult = { 
+              success: false, 
+              error: 'Unexpected clipboard error occurred',
+              errorType: 'unexpected'
+            };
+          }
+        } else {
+          clipboardResult = { 
+            success: false, 
+            error: availability.reason,
+            errorType: 'api_unavailable'
+          };
+        }
+      }
+ 
+      // Generate user feedback message based on clipboard results
+      let successMessage = 'Screenshot downloaded successfully!';
+      let messageColor = 'rgba(39, 174, 96, 0.95)'; // Green for success
+      
+      if (this.copyToClipboard) {
+        if (clipboardResult && clipboardResult.success) {
+          successMessage = 'Screenshot downloaded and copied to clipboard!';
+          // Keep green color for full success
+        } else {
+          // Download succeeded but clipboard failed - provide specific error feedback
+          const errorMessage = this.getClipboardErrorMessage(clipboardResult);
+          successMessage = `Screenshot downloaded successfully! ${errorMessage}`;
+          messageColor = 'rgba(243, 156, 18, 0.95)'; // Orange for partial success
+        }
+      }
+ 
+      this.overlay.innerHTML = `
+        <div style="position: fixed; top: 20px; left: 20px; background: ${messageColor}; color: white; padding: 12px 16px; border-radius: 8px; z-index: 2147483647; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 14px; box-shadow: 0 4px 20px rgba(0,0,0,0.3);">
+          <div style="display: flex; align-items: center; gap: 10px;">
+            <span>✅</span>
+            <span>${successMessage}</span>
+          </div>
+        </div>
+      `;
+ 
+      // Notify popup
+      chrome.runtime.sendMessage({ action: 'selectorStopped', reason: 'screenshot-taken' });
+ 
+      setTimeout(() => this.cleanup(), 2000);
+ 
+    } catch (error) {
+      console.error('Screenshot failed:', error);
+      
+      // Provide specific error messages for screenshot failures
+      let errorMessage = 'Screenshot capture failed. Please try again.';
+      
+      if (error.message) {
+        if (error.message.includes('html2canvas')) {
+          errorMessage = 'Screenshot rendering failed. Try selecting a different element or refresh the page.';
+        } else if (error.message.includes('timeout')) {
+          errorMessage = 'Screenshot capture timed out. Try selecting a smaller area or simpler element.';
+        } else if (error.message.includes('network') || error.message.includes('CORS')) {
+          errorMessage = 'Screenshot failed: Some content is blocked by security restrictions.';
+        } else if (error.message.includes('memory') || error.message.includes('quota')) {
+          errorMessage = 'Screenshot failed: Not enough memory. Try capturing a smaller area.';
+        } else if (error.message.includes('canvas')) {
+          errorMessage = 'Screenshot failed: Unable to create image. Try a different element.';
+        }
+      }
+      
+      this.overlay.innerHTML = `
+        <div style="position: fixed; top: 20px; left: 20px; background: rgba(231, 76, 60, 0.95); color: white; padding: 12px 16px; border-radius: 8px; z-index: 2147483647; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 14px; box-shadow: 0 4px 20px rgba(0,0,0,0.3);">
+          <div style="display: flex; align-items: center; gap: 10px;">
+            <span>❌</span>
+            <span>${errorMessage}</span>
+          </div>
+        </div>
+      `;
+      
+      // Notify popup about the failure
+      chrome.runtime.sendMessage({ action: 'selectorStopped', reason: 'screenshot-failed' });
+      
+      setTimeout(() => this.cleanup(), 3000);
+    }
+  }
+ 
+  /**
+   * Check if Clipboard API is available and supported
+   * @returns {{available: boolean, reason?: string}} Clipboard API availability status
+   */
+  checkClipboardAPIAvailability() {
+    // Check for basic Clipboard API support
+    if (!navigator.clipboard) {
+      return {
+        available: false,
+        reason: 'Clipboard API not available in this browser'
+      };
+    }
+ 
+    // Check for ClipboardItem constructor (required for image data)
+    if (!window.ClipboardItem) {
+      return {
+        available: false,
+        reason: 'ClipboardItem not supported in this browser'
+      };
+    }
+ 
+    // Check if we're in a secure context (HTTPS required for clipboard)
+    if (!window.isSecureContext) {
+      return {
+        available: false,
+        reason: 'Clipboard API requires a secure context (HTTPS)'
+      };
+    }
+ 
+    // Check for write permission (this is the basic check, actual permission is checked during write)
+    if (!navigator.clipboard.write) {
+      return {
+        available: false,
+        reason: 'Clipboard write functionality not available'
+      };
+    }
+ 
+    return {
+      available: true
+    };
+  }
+ 
+  /**
+   * Get user-friendly feedback message about clipboard feature availability
+   * @returns {{supported: boolean, message: string}} User feedback about clipboard support
+   */
+  getClipboardSupportFeedback() {
+    const availability = this.checkClipboardAPIAvailability();
+    
+    if (availability.available) {
+      return {
+        supported: true,
+        message: 'Clipboard copying is available and ready to use.'
+      };
+    }
+ 
+    // Provide user-friendly messages for different scenarios
+    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
+    };
+  }
+ 
+  /**
+   * Generate user-friendly error message for clipboard operation failures
+   * @param {Object} clipboardResult - Result object from clipboard operation
+   * @returns {string} User-friendly error message
+   */
+  getClipboardErrorMessage(clipboardResult) {
+    if (!clipboardResult || !clipboardResult.error) {
+      return 'Clipboard copy failed due to unknown error.';
+    }
+ 
+    const errorType = clipboardResult.errorType;
+    const errorMessage = clipboardResult.error;
+ 
+    // Provide contextual error messages based on error type
+    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.';
+      
+      case 'network_error':
+        return 'Clipboard copy failed due to network error. Please try again.';
+      
+      case 'canvas_conversion':
+        return 'Clipboard copy failed: Unable to prepare image. Try capturing a different element.';
+      
+      case 'clipboard_item_creation':
+        return 'Clipboard copy not supported: Your browser doesn\'t support image clipboard operations.';
+      
+      case 'invalid_state':
+        return 'Clipboard copy failed: Browser clipboard is busy. Please try again.';
+      
+      case 'data_error':
+        return 'Clipboard copy failed: Invalid image data. Please try capturing again.';
+      
+      case 'unexpected':
+        return 'Clipboard copy failed due to unexpected error. Screenshot was still downloaded.';
+      
+      default:
+        // For unknown error types, try to extract meaningful info from the error message
+        if (errorMessage.includes('permission') || errorMessage.includes('denied')) {
+          return 'Clipboard copy failed: Access denied. Check browser permissions.';
+        } else if (errorMessage.includes('not supported') || errorMessage.includes('unavailable')) {
+          return 'Clipboard copy not supported in this browser.';
+        } else if (errorMessage.includes('timeout') || errorMessage.includes('timed out')) {
+          return 'Clipboard copy timed out. Try capturing a smaller area.';
+        } else if (errorMessage.includes('large') || errorMessage.includes('size')) {
+          return 'Clipboard copy failed: Image too large for clipboard.';
+        } else {
+          return `Clipboard copy failed: ${errorMessage}`;
+        }
+    }
+  }
+ 
+  /**
+   * Copy canvas content to clipboard using the Clipboard API
+   * @param {HTMLCanvasElement} canvas - The canvas element to copy
+   * @returns {Promise<{success: boolean, error?: string, errorType?: string}>} Result of clipboard operation
+   */
+  async copyCanvasToClipboard(canvas) {
+    try {
+      // Check clipboard API availability first
+      const availability = this.checkClipboardAPIAvailability();
+      if (!availability.available) {
+        return { 
+          success: false, 
+          error: availability.reason,
+          errorType: 'api_unavailable'
+        };
+      }
+ 
+      // Convert canvas to blob in PNG format with timeout
+      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)
+        )
+      ]);
+ 
+      // Validate blob size (prevent extremely large clipboard operations)
+      const maxBlobSize = 50 * 1024 * 1024; // 50MB limit
+      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'
+        };
+      }
+ 
+      // Create ClipboardItem with PNG image data
+      let clipboardItem;
+      try {
+        clipboardItem = new 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'
+        };
+      }
+ 
+      // Write to clipboard with timeout
+      await Promise.race([
+        navigator.clipboard.write([clipboardItem]),
+        new Promise((_, reject) => 
+          setTimeout(() => reject(new Error('Clipboard write operation timed out')), 15000)
+        )
+      ]);
+ 
+      return { success: true };
+ 
+    } catch (error) {
+      console.error('Clipboard operation failed:', error);
+      
+      // Comprehensive error handling with specific error types and user-friendly messages
+      let errorMessage = error.message || 'Unknown clipboard error';
+      let errorType = 'unknown';
+      
+      // Permission-related errors
+      if (error.name === 'NotAllowedError') {
+        errorMessage = 'Clipboard access denied. Please allow clipboard permissions in your browser settings.';
+        errorType = 'permission_denied';
+      } 
+      // API support errors
+      else if (error.name === 'NotSupportedError') {
+        errorMessage = 'Clipboard API not supported in this browser context.';
+        errorType = 'not_supported';
+      }
+      // Security context errors
+      else if (error.name === 'SecurityError') {
+        errorMessage = 'Clipboard access blocked by security policy. Try using HTTPS.';
+        errorType = 'security_error';
+      }
+      // Network or resource errors
+      else if (error.name === 'NetworkError') {
+        errorMessage = 'Network error during clipboard operation. Please try again.';
+        errorType = 'network_error';
+      }
+      // Timeout errors
+      else if (error.message.includes('timed out')) {
+        errorMessage = 'Clipboard operation timed out. The image may be too large.';
+        errorType = 'timeout';
+      }
+      // Canvas conversion errors
+      else if (error.message.includes('canvas') || error.message.includes('blob')) {
+        errorMessage = 'Failed to prepare image for clipboard. Try capturing a different area.';
+        errorType = 'canvas_conversion';
+      }
+      // Quota or storage errors
+      else if (error.name === 'QuotaExceededError' || error.message.includes('quota')) {
+        errorMessage = 'Clipboard storage quota exceeded. Try capturing a smaller image.';
+        errorType = 'quota_exceeded';
+      }
+      // DOM or state errors
+      else if (error.name === 'InvalidStateError') {
+        errorMessage = 'Clipboard is in an invalid state. Please try again.';
+        errorType = 'invalid_state';
+      }
+      // Data errors
+      else if (error.name === 'DataError') {
+        errorMessage = 'Invalid image data for clipboard. Please try again.';
+        errorType = 'data_error';
+      }
+      // Generic fallback for unknown errors
+      else {
+        errorMessage = `Clipboard operation failed: ${error.message}`;
+        errorType = 'unknown';
+      }
+ 
+      return { 
+        success: false, 
+        error: errorMessage,
+        errorType: errorType
+      };
+    }
+  }
+ 
+  cleanup() {
+    document.removeEventListener('mouseover', this.handleMouseOver);
+    document.removeEventListener('click', this.handleClick, true);
+    document.removeEventListener('keydown', this.handleKeyPress);
+ 
+    if (this.currentHighlight) {
+      this.currentHighlight.classList.remove('screenshot-highlight');
+    }
+ 
+    // Remove any scrollable indicators
+    this.removeScrollableIndicators();
+ 
+    if (this.overlay) {
+      this.overlay.remove();
+      this.overlay = null;
+    }
+ 
+    if (this.style) {
+      this.style.remove();
+      this.style = null;
+    }
+ 
+    this.isActive = false;
+    this.currentHighlight = null;
+  }
+}
+ 
+// Global instance
+let screenshotSelector = new ScreenshotSelector();
+ 
+// Add load indicator
+console.log('Full Screenshot Selector content script loaded');
+ 
+// Listen for messages from popup
+chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
+  try {
+  switch (message.action) {
+    case 'startSelector':
+      // Extract clipboard preference from message, default to true for backward compatibility
+      const copyToClipboard = message.copyToClipboard !== undefined ? message.copyToClipboard : true;
+      screenshotSelector.init(message.background, copyToClipboard);
+      sendResponse({ success: true });
+      break;
+ 
+    case 'stopSelector':
+      screenshotSelector.cleanup();
+      sendResponse({ success: true });
+      break;
+ 
+    case 'checkState':
+      sendResponse({ isActive: screenshotSelector.isActive });
+      break;
+ 
+    case 'checkClipboardSupport':
+      const availability = screenshotSelector.checkClipboardAPIAvailability();
+      const feedback = screenshotSelector.getClipboardSupportFeedback();
+      sendResponse({ 
+        availability: availability,
+        feedback: feedback
+      });
+      break;
+ 
+      default:
+        sendResponse({ error: 'Unknown action: ' + message.action });
+    }
+  } catch (error) {
+    console.error('Content script error:', error);
+    sendResponse({ error: error.message });
+  }
+ 
+  // Important: return true to indicate async response
+  return true;
+});
+ 
+// Handle keyboard shortcut
+chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
+  if (message.action === 'activateFromShortcut') {
+    if (!screenshotSelector.isActive) {
+      screenshotSelector.init('black', true); // Default background and clipboard enabled for shortcut
+    }
+  }
+});
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/favicon.png b/coverage/lcov-report/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..c1525b811a167671e9de1fa78aab9f5c0b61cef7 GIT binary patch literal 445 zcmV;u0Yd(XP))rP{nL}Ln%S7`m{0DjX9TLF* zFCb$4Oi7vyLOydb!7n&^ItCzb-%BoB`=x@N2jll2Nj`kauio%aw_@fe&*}LqlFT43 z8doAAe))z_%=P%v^@JHp3Hjhj^6*Kr_h|g_Gr?ZAa&y>wxHE99Gk>A)2MplWz2xdG zy8VD2J|Uf#EAw*bo5O*PO_}X2Tob{%bUoO2G~T`@%S6qPyc}VkhV}UifBuRk>%5v( z)x7B{I~z*k<7dv#5tC+m{km(D087J4O%+<<;K|qwefb6@GSX45wCK}Sn*> + + + + Code coverage report for All files + + + + + + + + + +
+
+

All files

+
+ +
+ 0% + Statements + 0/463 +
+ + +
+ 0% + Branches + 0/263 +
+ + +
+ 0% + Functions + 0/61 +
+ + +
+ 0% + Lines + 0/445 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
content.js +
+
0%0/3710%0/2140%0/440%0/356
popup.js +
+
0%0/920%0/490%0/170%0/89
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/popup.js.html b/coverage/lcov-report/popup.js.html new file mode 100644 index 0000000..5f1a6a1 --- /dev/null +++ b/coverage/lcov-report/popup.js.html @@ -0,0 +1,643 @@ + + + + + + Code coverage report for popup.js + + + + + + + + + +
+
+

All files popup.js

+
+ +
+ 0% + Statements + 0/92 +
+ + +
+ 0% + Branches + 0/49 +
+ + +
+ 0% + Functions + 0/17 +
+ + +
+ 0% + Lines + 0/89 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
// Popup script for Chrome extension
+document.addEventListener('DOMContentLoaded', async () => {
+  const backgroundSelect = document.getElementById('background-select');
+  const clipboardToggle = document.getElementById('clipboard-toggle');
+  const startBtn = document.getElementById('start-selector');
+  const stopBtn = document.getElementById('stop-selector');
+  const status = document.getElementById('status');
+ 
+  // Load saved preferences
+  const saved = await chrome.storage.sync.get(['backgroundPreference', 'copyToClipboard']);
+  if (saved.backgroundPreference) {
+    backgroundSelect.value = saved.backgroundPreference;
+  }
+  
+  // Set clipboard preference (default to true if not set)
+  clipboardToggle.checked = saved.copyToClipboard !== undefined ? saved.copyToClipboard : true;
+ 
+  // Save background preference when changed
+  backgroundSelect.addEventListener('change', () => {
+    chrome.storage.sync.set({ backgroundPreference: backgroundSelect.value });
+  });
+ 
+  // Save clipboard preference when changed
+  clipboardToggle.addEventListener('change', () => {
+    chrome.storage.sync.set({ copyToClipboard: clipboardToggle.checked });
+  });
+ 
+  // Start selector
+  startBtn.addEventListener('click', async () => {
+    const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
+ 
+    // Check if page is compatible
+    if (tab.url.startsWith('chrome://') ||
+        tab.url.startsWith('chrome-extension://') ||
+        tab.url.startsWith('edge://') ||
+        tab.url.startsWith('about:') ||
+        tab.url === 'chrome://newtab/' ||
+        tab.url === 'edge://newtab/') {
+      showStatus('Error: Cannot capture screenshots on this page type.', 'error');
+      return;
+    }
+ 
+    try {
+      await chrome.tabs.sendMessage(tab.id, {
+        action: 'startSelector',
+        background: backgroundSelect.value,
+        copyToClipboard: clipboardToggle.checked
+      });
+ 
+      showStatus('Selector activated! Hover over elements and click to capture.', 'success');
+      toggleButtons(true);
+ 
+      // Auto-close popup after starting
+      setTimeout(() => window.close(), 1500);
+ 
+    } catch (error) {
+      console.error('Full error details:', error);
+      if (error.message.includes('Could not establish connection')) {
+        showStatus('Error: Content script not loaded. Try refreshing the page.', 'error');
+      } else {
+        showStatus('Error: Could not activate on this page.', 'error');
+      }
+    }
+  });
+ 
+  // Stop selector
+  stopBtn.addEventListener('click', async () => {
+    const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
+ 
+    try {
+      await chrome.tabs.sendMessage(tab.id, { action: 'stopSelector' });
+      showStatus('Selector stopped.', 'success');
+      toggleButtons(false);
+    } catch (error) {
+      console.error('Error stopping selector:', error);
+    }
+  });
+ 
+  // Check current state
+  checkSelectorState();
+ 
+  // Handle tooltip keyboard navigation
+  setupTooltipAccessibility();
+ 
+  function toggleButtons(isActive) {
+    if (isActive) {
+      startBtn.style.display = 'none';
+      stopBtn.style.display = 'block';
+    } else {
+      startBtn.style.display = 'block';
+      stopBtn.style.display = 'none';
+    }
+  }
+ 
+  function showStatus(message, type = 'success') {
+    status.textContent = message;
+    status.className = `status ${type}`;
+    status.style.display = 'block';
+ 
+    setTimeout(() => {
+      status.style.display = 'none';
+    }, 3000);
+  }
+ 
+  async function checkSelectorState() {
+    const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
+ 
+    // Skip check for incompatible pages
+    if (tab.url.startsWith('chrome://') ||
+        tab.url.startsWith('chrome-extension://') ||
+        tab.url.startsWith('edge://') ||
+        tab.url.startsWith('about:')) {
+      return;
+    }
+ 
+    try {
+      const response = await chrome.tabs.sendMessage(tab.id, { action: 'checkState' });
+      if (response && response.isActive) {
+        toggleButtons(true);
+        showStatus('Selector is currently active', 'success');
+      }
+    } catch (error) {
+      // Content script not ready or selector not active
+      console.log('Content script not ready or selector inactive:', error.message);
+    }
+  }
+ 
+  function setupTooltipAccessibility() {
+    const tooltipIcon = document.querySelector('.tooltip-icon');
+    const tooltipContent = document.querySelector('.tooltip-content');
+    
+    if (!tooltipIcon || !tooltipContent) return;
+ 
+    // Handle keyboard events for tooltip
+    tooltipIcon.addEventListener('keydown', (e) => {
+      if (e.key === 'Enter' || e.key === ' ') {
+        e.preventDefault();
+        // Toggle tooltip visibility on Enter/Space
+        const isVisible = tooltipContent.style.visibility === 'visible';
+        tooltipContent.style.visibility = isVisible ? 'hidden' : 'visible';
+        tooltipContent.style.opacity = isVisible ? '0' : '1';
+      } else if (e.key === 'Escape') {
+        // Hide tooltip on Escape
+        tooltipContent.style.visibility = 'hidden';
+        tooltipContent.style.opacity = '0';
+      }
+    });
+ 
+    // Hide tooltip when clicking outside
+    document.addEventListener('click', (e) => {
+      if (!tooltipIcon.contains(e.target)) {
+        tooltipContent.style.visibility = 'hidden';
+        tooltipContent.style.opacity = '0';
+      }
+    });
+ 
+    // Handle focus loss
+    tooltipIcon.addEventListener('blur', (e) => {
+      // Small delay to allow for focus to move to tooltip content if needed
+      setTimeout(() => {
+        if (!tooltipIcon.matches(':focus-within')) {
+          tooltipContent.style.visibility = 'hidden';
+          tooltipContent.style.opacity = '0';
+        }
+      }, 100);
+    });
+  }
+});
+ 
+// Listen for messages from content script
+chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
+  if (message.action === 'selectorStopped') {
+    const startBtn = document.getElementById('start-selector');
+    const stopBtn = document.getElementById('stop-selector');
+    const status = document.getElementById('status');
+ 
+    startBtn.style.display = 'block';
+    stopBtn.style.display = 'none';
+ 
+    if (message.reason === 'screenshot-taken') {
+      status.textContent = 'Screenshot captured!';
+      status.className = 'status success';
+      status.style.display = 'block';
+      setTimeout(() => status.style.display = 'none', 2000);
+    }
+  }
+});
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/prettify.css b/coverage/lcov-report/prettify.css new file mode 100644 index 0000000..b317a7c --- /dev/null +++ b/coverage/lcov-report/prettify.css @@ -0,0 +1 @@ +.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} diff --git a/coverage/lcov-report/prettify.js b/coverage/lcov-report/prettify.js new file mode 100644 index 0000000..b322523 --- /dev/null +++ b/coverage/lcov-report/prettify.js @@ -0,0 +1,2 @@ +/* eslint-disable */ +window.PR_SHOULD_USE_CONTINUATION=true;(function(){var h=["break,continue,do,else,for,if,return,while"];var u=[h,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"];var p=[u,"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"];var l=[p,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"];var x=[p,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"];var R=[x,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"];var r="all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes";var w=[p,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"];var s="caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END";var I=[h,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"];var f=[h,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"];var H=[h,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"];var A=[l,R,w,s+I,f,H];var e=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/;var C="str";var z="kwd";var j="com";var O="typ";var G="lit";var L="pun";var F="pln";var m="tag";var E="dec";var J="src";var P="atn";var n="atv";var N="nocode";var M="(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*";function k(Z){var ad=0;var S=false;var ac=false;for(var V=0,U=Z.length;V122)){if(!(al<65||ag>90)){af.push([Math.max(65,ag)|32,Math.min(al,90)|32])}if(!(al<97||ag>122)){af.push([Math.max(97,ag)&~32,Math.min(al,122)&~32])}}}}af.sort(function(av,au){return(av[0]-au[0])||(au[1]-av[1])});var ai=[];var ap=[NaN,NaN];for(var ar=0;arat[0]){if(at[1]+1>at[0]){an.push("-")}an.push(T(at[1]))}}an.push("]");return an.join("")}function W(al){var aj=al.source.match(new RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g"));var ah=aj.length;var an=[];for(var ak=0,am=0;ak=2&&ai==="["){aj[ak]=X(ag)}else{if(ai!=="\\"){aj[ak]=ag.replace(/[a-zA-Z]/g,function(ao){var ap=ao.charCodeAt(0);return"["+String.fromCharCode(ap&~32,ap|32)+"]"})}}}}return aj.join("")}var aa=[];for(var V=0,U=Z.length;V=0;){S[ac.charAt(ae)]=Y}}var af=Y[1];var aa=""+af;if(!ag.hasOwnProperty(aa)){ah.push(af);ag[aa]=null}}ah.push(/[\0-\uffff]/);V=k(ah)})();var X=T.length;var W=function(ah){var Z=ah.sourceCode,Y=ah.basePos;var ad=[Y,F];var af=0;var an=Z.match(V)||[];var aj={};for(var ae=0,aq=an.length;ae=5&&"lang-"===ap.substring(0,5);if(am&&!(ai&&typeof ai[1]==="string")){am=false;ap=J}if(!am){aj[ag]=ap}}var ab=af;af+=ag.length;if(!am){ad.push(Y+ab,ap)}else{var al=ai[1];var ak=ag.indexOf(al);var ac=ak+al.length;if(ai[2]){ac=ag.length-ai[2].length;ak=ac-al.length}var ar=ap.substring(5);B(Y+ab,ag.substring(0,ak),W,ad);B(Y+ab+ak,al,q(ar,al),ad);B(Y+ab+ac,ag.substring(ac),W,ad)}}ah.decorations=ad};return W}function i(T){var W=[],S=[];if(T.tripleQuotedStrings){W.push([C,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,null,"'\""])}else{if(T.multiLineStrings){W.push([C,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"])}else{W.push([C,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"])}}if(T.verbatimStrings){S.push([C,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null])}var Y=T.hashComments;if(Y){if(T.cStyleComments){if(Y>1){W.push([j,/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,null,"#"])}else{W.push([j,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"])}S.push([C,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,null])}else{W.push([j,/^#[^\r\n]*/,null,"#"])}}if(T.cStyleComments){S.push([j,/^\/\/[^\r\n]*/,null]);S.push([j,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}if(T.regexLiterals){var X=("/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/");S.push(["lang-regex",new RegExp("^"+M+"("+X+")")])}var V=T.types;if(V){S.push([O,V])}var U=(""+T.keywords).replace(/^ | $/g,"");if(U.length){S.push([z,new RegExp("^(?:"+U.replace(/[\s,]+/g,"|")+")\\b"),null])}W.push([F,/^\s+/,null," \r\n\t\xA0"]);S.push([G,/^@[a-z_$][a-z_$@0-9]*/i,null],[O,/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],[F,/^[a-z_$][a-z_$@0-9]*/i,null],[G,new RegExp("^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*","i"),null,"0123456789"],[F,/^\\[\s\S]?/,null],[L,/^.[^\s\w\.$@\'\"\`\/\#\\]*/,null]);return g(W,S)}var K=i({keywords:A,hashComments:true,cStyleComments:true,multiLineStrings:true,regexLiterals:true});function Q(V,ag){var U=/(?:^|\s)nocode(?:\s|$)/;var ab=/\r\n?|\n/;var ac=V.ownerDocument;var S;if(V.currentStyle){S=V.currentStyle.whiteSpace}else{if(window.getComputedStyle){S=ac.defaultView.getComputedStyle(V,null).getPropertyValue("white-space")}}var Z=S&&"pre"===S.substring(0,3);var af=ac.createElement("LI");while(V.firstChild){af.appendChild(V.firstChild)}var W=[af];function ae(al){switch(al.nodeType){case 1:if(U.test(al.className)){break}if("BR"===al.nodeName){ad(al);if(al.parentNode){al.parentNode.removeChild(al)}}else{for(var an=al.firstChild;an;an=an.nextSibling){ae(an)}}break;case 3:case 4:if(Z){var am=al.nodeValue;var aj=am.match(ab);if(aj){var ai=am.substring(0,aj.index);al.nodeValue=ai;var ah=am.substring(aj.index+aj[0].length);if(ah){var ak=al.parentNode;ak.insertBefore(ac.createTextNode(ah),al.nextSibling)}ad(al);if(!ai){al.parentNode.removeChild(al)}}}break}}function ad(ak){while(!ak.nextSibling){ak=ak.parentNode;if(!ak){return}}function ai(al,ar){var aq=ar?al.cloneNode(false):al;var ao=al.parentNode;if(ao){var ap=ai(ao,1);var an=al.nextSibling;ap.appendChild(aq);for(var am=an;am;am=an){an=am.nextSibling;ap.appendChild(am)}}return aq}var ah=ai(ak.nextSibling,0);for(var aj;(aj=ah.parentNode)&&aj.nodeType===1;){ah=aj}W.push(ah)}for(var Y=0;Y=S){ah+=2}if(V>=ap){Z+=2}}}var t={};function c(U,V){for(var S=V.length;--S>=0;){var T=V[S];if(!t.hasOwnProperty(T)){t[T]=U}else{if(window.console){console.warn("cannot override language handler %s",T)}}}}function q(T,S){if(!(T&&t.hasOwnProperty(T))){T=/^\s*]*(?:>|$)/],[j,/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],[L,/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);c(g([[F,/^[\s]+/,null," \t\r\n"],[n,/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[[m,/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],[P,/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],[L,/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);c(g([],[[n,/^[\s\S]+/]]),["uq.val"]);c(i({keywords:l,hashComments:true,cStyleComments:true,types:e}),["c","cc","cpp","cxx","cyc","m"]);c(i({keywords:"null,true,false"}),["json"]);c(i({keywords:R,hashComments:true,cStyleComments:true,verbatimStrings:true,types:e}),["cs"]);c(i({keywords:x,cStyleComments:true}),["java"]);c(i({keywords:H,hashComments:true,multiLineStrings:true}),["bsh","csh","sh"]);c(i({keywords:I,hashComments:true,multiLineStrings:true,tripleQuotedStrings:true}),["cv","py"]);c(i({keywords:s,hashComments:true,multiLineStrings:true,regexLiterals:true}),["perl","pl","pm"]);c(i({keywords:f,hashComments:true,multiLineStrings:true,regexLiterals:true}),["rb"]);c(i({keywords:w,cStyleComments:true,regexLiterals:true}),["js"]);c(i({keywords:r,hashComments:3,cStyleComments:true,multilineStrings:true,tripleQuotedStrings:true,regexLiterals:true}),["coffee"]);c(g([],[[C,/^[\s\S]+/]]),["regex"]);function d(V){var U=V.langExtension;try{var S=a(V.sourceNode);var T=S.sourceCode;V.sourceCode=T;V.spans=S.spans;V.basePos=0;q(U,T)(V);D(V)}catch(W){if("console" in window){console.log(W&&W.stack?W.stack:W)}}}function y(W,V,U){var S=document.createElement("PRE");S.innerHTML=W;if(U){Q(S,U)}var T={langExtension:V,numberLines:U,sourceNode:S};d(T);return S.innerHTML}function b(ad){function Y(af){return document.getElementsByTagName(af)}var ac=[Y("pre"),Y("code"),Y("xmp")];var T=[];for(var aa=0;aa=0){var ah=ai.match(ab);var am;if(!ah&&(am=o(aj))&&"CODE"===am.tagName){ah=am.className.match(ab)}if(ah){ah=ah[1]}var al=false;for(var ak=aj.parentNode;ak;ak=ak.parentNode){if((ak.tagName==="pre"||ak.tagName==="code"||ak.tagName==="xmp")&&ak.className&&ak.className.indexOf("prettyprint")>=0){al=true;break}}if(!al){var af=aj.className.match(/\blinenums\b(?::(\d+))?/);af=af?af[1]&&af[1].length?+af[1]:true:false;if(af){Q(aj,af)}S={langExtension:ah,sourceNode:aj,numberLines:af};d(S)}}}if(X]*(?:>|$)/],[PR.PR_COMMENT,/^<\!--[\s\S]*?(?:-\->|$)/],[PR.PR_PUNCTUATION,/^(?:<[%?]|[%?]>)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-handlebars",/^]*type\s*=\s*['"]?text\/x-handlebars-template['"]?\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i],[PR.PR_DECLARATION,/^{{[#^>/]?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{&?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{{>?\s*[\w.][^}]*}}}/],[PR.PR_COMMENT,/^{{![^}]*}}/]]),["handlebars","hbs"]);PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[ \t\r\n\f]+/,null," \t\r\n\f"]],[[PR.PR_STRING,/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],[PR.PR_STRING,/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']*)\)/i],[PR.PR_KEYWORD,/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],[PR.PR_COMMENT,/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//],[PR.PR_COMMENT,/^(?:)/],[PR.PR_LITERAL,/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],[PR.PR_LITERAL,/^#(?:[0-9a-f]{3}){1,2}/i],[PR.PR_PLAIN,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i],[PR.PR_PUNCTUATION,/^[^\s\w\'\"]+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_KEYWORD,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_STRING,/^[^\)\"\']+/]]),["css-str"]); diff --git a/coverage/lcov-report/sort-arrow-sprite.png b/coverage/lcov-report/sort-arrow-sprite.png new file mode 100644 index 0000000000000000000000000000000000000000..6ed68316eb3f65dec9063332d2f69bf3093bbfab GIT binary patch literal 138 zcmeAS@N?(olHy`uVBq!ia0vp^>_9Bd!3HEZxJ@+%Qh}Z>jv*C{$p!i!8j}?a+@3A= zIAGwzjijN=FBi!|L1t?LM;Q;gkwn>2cAy-KV{dn nf0J1DIvEHQu*n~6U}x}qyky7vi4|9XhBJ7&`njxgN@xNA8m%nc literal 0 HcmV?d00001 diff --git a/coverage/lcov-report/sorter.js b/coverage/lcov-report/sorter.js new file mode 100644 index 0000000..2bb296a --- /dev/null +++ b/coverage/lcov-report/sorter.js @@ -0,0 +1,196 @@ +/* eslint-disable */ +var addSorting = (function() { + 'use strict'; + var cols, + currentSort = { + index: 0, + desc: false + }; + + // returns the summary table element + function getTable() { + return document.querySelector('.coverage-summary'); + } + // returns the thead element of the summary table + function getTableHeader() { + return getTable().querySelector('thead tr'); + } + // returns the tbody element of the summary table + function getTableBody() { + return getTable().querySelector('tbody'); + } + // returns the th element for nth column + function getNthColumn(n) { + return getTableHeader().querySelectorAll('th')[n]; + } + + function onFilterInput() { + const searchValue = document.getElementById('fileSearch').value; + const rows = document.getElementsByTagName('tbody')[0].children; + for (let i = 0; i < rows.length; i++) { + const row = rows[i]; + if ( + row.textContent + .toLowerCase() + .includes(searchValue.toLowerCase()) + ) { + row.style.display = ''; + } else { + row.style.display = 'none'; + } + } + } + + // loads the search box + function addSearchBox() { + var template = document.getElementById('filterTemplate'); + var templateClone = template.content.cloneNode(true); + templateClone.getElementById('fileSearch').oninput = onFilterInput; + template.parentElement.appendChild(templateClone); + } + + // loads all columns + function loadColumns() { + var colNodes = getTableHeader().querySelectorAll('th'), + colNode, + cols = [], + col, + i; + + for (i = 0; i < colNodes.length; i += 1) { + colNode = colNodes[i]; + col = { + key: colNode.getAttribute('data-col'), + sortable: !colNode.getAttribute('data-nosort'), + type: colNode.getAttribute('data-type') || 'string' + }; + cols.push(col); + if (col.sortable) { + col.defaultDescSort = col.type === 'number'; + colNode.innerHTML = + colNode.innerHTML + ''; + } + } + return cols; + } + // attaches a data attribute to every tr element with an object + // of data values keyed by column name + function loadRowData(tableRow) { + var tableCols = tableRow.querySelectorAll('td'), + colNode, + col, + data = {}, + i, + val; + for (i = 0; i < tableCols.length; i += 1) { + colNode = tableCols[i]; + col = cols[i]; + val = colNode.getAttribute('data-value'); + if (col.type === 'number') { + val = Number(val); + } + data[col.key] = val; + } + return data; + } + // loads all row data + function loadData() { + var rows = getTableBody().querySelectorAll('tr'), + i; + + for (i = 0; i < rows.length; i += 1) { + rows[i].data = loadRowData(rows[i]); + } + } + // sorts the table using the data for the ith column + function sortByIndex(index, desc) { + var key = cols[index].key, + sorter = function(a, b) { + a = a.data[key]; + b = b.data[key]; + return a < b ? -1 : a > b ? 1 : 0; + }, + finalSorter = sorter, + tableBody = document.querySelector('.coverage-summary tbody'), + rowNodes = tableBody.querySelectorAll('tr'), + rows = [], + i; + + if (desc) { + finalSorter = function(a, b) { + return -1 * sorter(a, b); + }; + } + + for (i = 0; i < rowNodes.length; i += 1) { + rows.push(rowNodes[i]); + tableBody.removeChild(rowNodes[i]); + } + + rows.sort(finalSorter); + + for (i = 0; i < rows.length; i += 1) { + tableBody.appendChild(rows[i]); + } + } + // removes sort indicators for current column being sorted + function removeSortIndicators() { + var col = getNthColumn(currentSort.index), + cls = col.className; + + cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, ''); + col.className = cls; + } + // adds sort indicators for current column being sorted + function addSortIndicators() { + getNthColumn(currentSort.index).className += currentSort.desc + ? ' sorted-desc' + : ' sorted'; + } + // adds event listeners for all sorter widgets + function enableUI() { + var i, + el, + ithSorter = function ithSorter(i) { + var col = cols[i]; + + return function() { + var desc = col.defaultDescSort; + + if (currentSort.index === i) { + desc = !currentSort.desc; + } + sortByIndex(i, desc); + removeSortIndicators(); + currentSort.index = i; + currentSort.desc = desc; + addSortIndicators(); + }; + }; + for (i = 0; i < cols.length; i += 1) { + if (cols[i].sortable) { + // add the click event handler on the th so users + // dont have to click on those tiny arrows + el = getNthColumn(i).querySelector('.sorter').parentElement; + if (el.addEventListener) { + el.addEventListener('click', ithSorter(i)); + } else { + el.attachEvent('onclick', ithSorter(i)); + } + } + } + } + // adds sorting functionality to the UI + return function() { + if (!getTable()) { + return; + } + cols = loadColumns(); + loadData(); + addSearchBox(); + addSortIndicators(); + enableUI(); + }; +})(); + +window.addEventListener('load', addSorting); diff --git a/coverage/lcov.info b/coverage/lcov.info new file mode 100644 index 0000000..374cf82 --- /dev/null +++ b/coverage/lcov.info @@ -0,0 +1,848 @@ +TN: +SF:content.js +FN:3,(anonymous_0) +FN:12,(anonymous_1) +FN:26,(anonymous_2) +FN:76,(anonymous_3) +FN:79,(anonymous_4) +FN:89,(anonymous_5) +FN:105,(anonymous_6) +FN:114,(anonymous_7) +FN:120,(anonymous_8) +FN:176,(anonymous_9) +FN:178,(anonymous_10) +FN:182,(anonymous_11) +FN:184,(anonymous_12) +FN:201,(anonymous_13) +FN:215,(anonymous_14) +FN:249,(anonymous_15) +FN:273,(anonymous_16) +FN:275,(anonymous_17) +FN:291,(anonymous_18) +FN:311,(anonymous_19) +FN:332,(anonymous_20) +FN:336,(anonymous_21) +FN:346,(anonymous_22) +FN:347,(anonymous_23) +FN:361,(anonymous_24) +FN:366,(anonymous_25) +FN:435,(anonymous_26) +FN:441,(anonymous_27) +FN:455,(anonymous_28) +FN:544,(anonymous_29) +FN:578,(anonymous_30) +FN:586,(anonymous_31) +FN:628,(anonymous_32) +FN:662,(anonymous_33) +FN:728,(anonymous_34) +FN:742,(anonymous_35) +FN:743,(anonymous_36) +FN:751,(anonymous_37) +FN:752,(anonymous_38) +FN:783,(anonymous_39) +FN:784,(anonymous_40) +FN:856,(anonymous_41) +FN:890,(anonymous_42) +FN:931,(anonymous_43) +FNF:44 +FNH:0 +FNDA:0,(anonymous_0) +FNDA:0,(anonymous_1) +FNDA:0,(anonymous_2) +FNDA:0,(anonymous_3) +FNDA:0,(anonymous_4) +FNDA:0,(anonymous_5) +FNDA:0,(anonymous_6) +FNDA:0,(anonymous_7) +FNDA:0,(anonymous_8) +FNDA:0,(anonymous_9) +FNDA:0,(anonymous_10) +FNDA:0,(anonymous_11) +FNDA:0,(anonymous_12) +FNDA:0,(anonymous_13) +FNDA:0,(anonymous_14) +FNDA:0,(anonymous_15) +FNDA:0,(anonymous_16) +FNDA:0,(anonymous_17) +FNDA:0,(anonymous_18) +FNDA:0,(anonymous_19) +FNDA:0,(anonymous_20) +FNDA:0,(anonymous_21) +FNDA:0,(anonymous_22) +FNDA:0,(anonymous_23) +FNDA:0,(anonymous_24) +FNDA:0,(anonymous_25) +FNDA:0,(anonymous_26) +FNDA:0,(anonymous_27) +FNDA:0,(anonymous_28) +FNDA:0,(anonymous_29) +FNDA:0,(anonymous_30) +FNDA:0,(anonymous_31) +FNDA:0,(anonymous_32) +FNDA:0,(anonymous_33) +FNDA:0,(anonymous_34) +FNDA:0,(anonymous_35) +FNDA:0,(anonymous_36) +FNDA:0,(anonymous_37) +FNDA:0,(anonymous_38) +FNDA:0,(anonymous_39) +FNDA:0,(anonymous_40) +FNDA:0,(anonymous_41) +FNDA:0,(anonymous_42) +FNDA:0,(anonymous_43) +DA:4,0 +DA:5,0 +DA:6,0 +DA:7,0 +DA:8,0 +DA:9,0 +DA:13,0 +DA:14,0 +DA:15,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:22,0 +DA:23,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:41,0 +DA:42,0 +DA:72,0 +DA:73,0 +DA:76,0 +DA:80,0 +DA:81,0 +DA:82,0 +DA:84,0 +DA:85,0 +DA:86,0 +DA:90,0 +DA:93,0 +DA:94,0 +DA:95,0 +DA:98,0 +DA:99,0 +DA:102,0 +DA:106,0 +DA:107,0 +DA:108,0 +DA:110,0 +DA:111,0 +DA:115,0 +DA:116,0 +DA:121,0 +DA:122,0 +DA:123,0 +DA:125,0 +DA:128,0 +DA:129,0 +DA:130,0 +DA:131,0 +DA:134,0 +DA:135,0 +DA:136,0 +DA:137,0 +DA:140,0 +DA:147,0 +DA:148,0 +DA:151,0 +DA:152,0 +DA:153,0 +DA:167,0 +DA:168,0 +DA:169,0 +DA:170,0 +DA:172,0 +DA:177,0 +DA:178,0 +DA:184,0 +DA:185,0 +DA:186,0 +DA:187,0 +DA:191,0 +DA:194,0 +DA:195,0 +DA:198,0 +DA:199,0 +DA:201,0 +DA:203,0 +DA:204,0 +DA:209,0 +DA:210,0 +DA:211,0 +DA:216,0 +DA:218,0 +DA:221,0 +DA:249,0 +DA:250,0 +DA:251,0 +DA:252,0 +DA:253,0 +DA:261,0 +DA:264,0 +DA:265,0 +DA:267,0 +DA:268,0 +DA:274,0 +DA:275,0 +DA:276,0 +DA:277,0 +DA:280,0 +DA:281,0 +DA:289,0 +DA:290,0 +DA:291,0 +DA:292,0 +DA:293,0 +DA:294,0 +DA:297,0 +DA:300,0 +DA:301,0 +DA:302,0 +DA:313,0 +DA:314,0 +DA:315,0 +DA:322,0 +DA:329,0 +DA:330,0 +DA:332,0 +DA:333,0 +DA:334,0 +DA:336,0 +DA:337,0 +DA:339,0 +DA:340,0 +DA:341,0 +DA:342,0 +DA:343,0 +DA:344,0 +DA:346,0 +DA:347,0 +DA:348,0 +DA:349,0 +DA:352,0 +DA:358,0 +DA:361,0 +DA:367,0 +DA:369,0 +DA:370,0 +DA:371,0 +DA:372,0 +DA:373,0 +DA:375,0 +DA:380,0 +DA:396,0 +DA:397,0 +DA:400,0 +DA:411,0 +DA:412,0 +DA:413,0 +DA:414,0 +DA:415,0 +DA:418,0 +DA:421,0 +DA:437,0 +DA:438,0 +DA:441,0 +DA:442,0 +DA:443,0 +DA:445,0 +DA:446,0 +DA:447,0 +DA:448,0 +DA:449,0 +DA:450,0 +DA:452,0 +DA:453,0 +DA:454,0 +DA:455,0 +DA:456,0 +DA:459,0 +DA:461,0 +DA:465,0 +DA:470,0 +DA:471,0 +DA:472,0 +DA:473,0 +DA:476,0 +DA:481,0 +DA:484,0 +DA:485,0 +DA:486,0 +DA:487,0 +DA:490,0 +DA:491,0 +DA:492,0 +DA:493,0 +DA:494,0 +DA:496,0 +DA:500,0 +DA:501,0 +DA:508,0 +DA:517,0 +DA:518,0 +DA:520,0 +DA:521,0 +DA:522,0 +DA:526,0 +DA:527,0 +DA:528,0 +DA:532,0 +DA:542,0 +DA:544,0 +DA:547,0 +DA:550,0 +DA:552,0 +DA:553,0 +DA:554,0 +DA:555,0 +DA:556,0 +DA:557,0 +DA:558,0 +DA:559,0 +DA:560,0 +DA:561,0 +DA:562,0 +DA:566,0 +DA:576,0 +DA:578,0 +DA:588,0 +DA:589,0 +DA:596,0 +DA:597,0 +DA:604,0 +DA:605,0 +DA:612,0 +DA:613,0 +DA:619,0 +DA:629,0 +DA:631,0 +DA:632,0 +DA:639,0 +DA:641,0 +DA:642,0 +DA:643,0 +DA:644,0 +DA:645,0 +DA:646,0 +DA:648,0 +DA:651,0 +DA:663,0 +DA:664,0 +DA:667,0 +DA:668,0 +DA:671,0 +DA:673,0 +DA:677,0 +DA:680,0 +DA:684,0 +DA:687,0 +DA:690,0 +DA:693,0 +DA:696,0 +DA:699,0 +DA:702,0 +DA:705,0 +DA:709,0 +DA:710,0 +DA:711,0 +DA:712,0 +DA:713,0 +DA:714,0 +DA:715,0 +DA:716,0 +DA:718,0 +DA:729,0 +DA:731,0 +DA:732,0 +DA:733,0 +DA:741,0 +DA:743,0 +DA:744,0 +DA:745,0 +DA:747,0 +DA:752,0 +DA:757,0 +DA:758,0 +DA:759,0 +DA:768,0 +DA:769,0 +DA:773,0 +DA:781,0 +DA:784,0 +DA:788,0 +DA:791,0 +DA:794,0 +DA:795,0 +DA:798,0 +DA:799,0 +DA:800,0 +DA:803,0 +DA:804,0 +DA:805,0 +DA:808,0 +DA:809,0 +DA:810,0 +DA:813,0 +DA:814,0 +DA:815,0 +DA:818,0 +DA:819,0 +DA:820,0 +DA:823,0 +DA:824,0 +DA:825,0 +DA:828,0 +DA:829,0 +DA:830,0 +DA:833,0 +DA:834,0 +DA:835,0 +DA:838,0 +DA:839,0 +DA:840,0 +DA:844,0 +DA:845,0 +DA:848,0 +DA:857,0 +DA:858,0 +DA:859,0 +DA:861,0 +DA:862,0 +DA:866,0 +DA:868,0 +DA:869,0 +DA:870,0 +DA:873,0 +DA:874,0 +DA:875,0 +DA:878,0 +DA:879,0 +DA:884,0 +DA:887,0 +DA:890,0 +DA:891,0 +DA:892,0 +DA:895,0 +DA:896,0 +DA:897,0 +DA:898,0 +DA:901,0 +DA:902,0 +DA:903,0 +DA:906,0 +DA:907,0 +DA:910,0 +DA:911,0 +DA:912,0 +DA:916,0 +DA:919,0 +DA:922,0 +DA:923,0 +DA:927,0 +DA:931,0 +DA:932,0 +DA:933,0 +DA:934,0 +LF:356 +LH:0 +BRDA:12,0,0,0 +BRDA:12,1,0,0 +BRDA:13,2,0,0 +BRDA:13,2,1,0 +BRDA:90,3,0,0 +BRDA:90,3,1,0 +BRDA:93,4,0,0 +BRDA:93,4,1,0 +BRDA:106,5,0,0 +BRDA:106,5,1,0 +BRDA:115,6,0,0 +BRDA:115,6,1,0 +BRDA:125,7,0,0 +BRDA:125,7,1,0 +BRDA:125,8,0,0 +BRDA:125,8,1,0 +BRDA:134,9,0,0 +BRDA:134,9,1,0 +BRDA:134,10,0,0 +BRDA:134,10,1,0 +BRDA:143,11,0,0 +BRDA:143,11,1,0 +BRDA:144,12,0,0 +BRDA:144,12,1,0 +BRDA:168,13,0,0 +BRDA:168,13,1,0 +BRDA:169,14,0,0 +BRDA:169,14,1,0 +BRDA:186,15,0,0 +BRDA:186,15,1,0 +BRDA:186,16,0,0 +BRDA:186,16,1,0 +BRDA:194,17,0,0 +BRDA:194,17,1,0 +BRDA:198,18,0,0 +BRDA:198,18,1,0 +BRDA:198,19,0,0 +BRDA:198,19,1,0 +BRDA:203,20,0,0 +BRDA:203,20,1,0 +BRDA:209,21,0,0 +BRDA:209,21,1,0 +BRDA:216,22,0,0 +BRDA:216,22,1,0 +BRDA:216,23,0,0 +BRDA:216,23,1,0 +BRDA:251,24,0,0 +BRDA:251,24,1,0 +BRDA:251,25,0,0 +BRDA:251,25,1,0 +BRDA:251,25,2,0 +BRDA:251,25,3,0 +BRDA:251,25,4,0 +BRDA:280,26,0,0 +BRDA:280,26,1,0 +BRDA:280,27,0,0 +BRDA:280,27,1,0 +BRDA:280,27,2,0 +BRDA:293,28,0,0 +BRDA:293,28,1,0 +BRDA:293,29,0,0 +BRDA:293,29,1,0 +BRDA:293,29,2,0 +BRDA:313,30,0,0 +BRDA:313,30,1,0 +BRDA:313,31,0,0 +BRDA:313,31,1,0 +BRDA:337,32,0,0 +BRDA:337,32,1,0 +BRDA:370,33,0,0 +BRDA:370,33,1,0 +BRDA:372,34,0,0 +BRDA:372,34,1,0 +BRDA:424,35,0,0 +BRDA:424,35,1,0 +BRDA:425,36,0,0 +BRDA:425,36,1,0 +BRDA:428,37,0,0 +BRDA:428,37,1,0 +BRDA:443,38,0,0 +BRDA:443,38,1,0 +BRDA:450,39,0,0 +BRDA:450,39,1,0 +BRDA:450,40,0,0 +BRDA:450,40,1,0 +BRDA:472,41,0,0 +BRDA:472,41,1,0 +BRDA:472,42,0,0 +BRDA:472,42,1,0 +BRDA:491,43,0,0 +BRDA:491,43,1,0 +BRDA:493,44,0,0 +BRDA:493,44,1,0 +BRDA:520,45,0,0 +BRDA:520,45,1,0 +BRDA:521,46,0,0 +BRDA:521,46,1,0 +BRDA:521,47,0,0 +BRDA:521,47,1,0 +BRDA:552,48,0,0 +BRDA:552,48,1,0 +BRDA:553,49,0,0 +BRDA:553,49,1,0 +BRDA:555,50,0,0 +BRDA:555,50,1,0 +BRDA:557,51,0,0 +BRDA:557,51,1,0 +BRDA:557,52,0,0 +BRDA:557,52,1,0 +BRDA:559,53,0,0 +BRDA:559,53,1,0 +BRDA:559,54,0,0 +BRDA:559,54,1,0 +BRDA:561,55,0,0 +BRDA:561,55,1,0 +BRDA:588,56,0,0 +BRDA:588,56,1,0 +BRDA:596,57,0,0 +BRDA:596,57,1,0 +BRDA:604,58,0,0 +BRDA:604,58,1,0 +BRDA:612,59,0,0 +BRDA:612,59,1,0 +BRDA:631,60,0,0 +BRDA:631,60,1,0 +BRDA:641,61,0,0 +BRDA:641,61,1,0 +BRDA:641,62,0,0 +BRDA:641,62,1,0 +BRDA:643,63,0,0 +BRDA:643,63,1,0 +BRDA:645,64,0,0 +BRDA:645,64,1,0 +BRDA:663,65,0,0 +BRDA:663,65,1,0 +BRDA:663,66,0,0 +BRDA:663,66,1,0 +BRDA:671,67,0,0 +BRDA:671,67,1,0 +BRDA:671,67,2,0 +BRDA:671,67,3,0 +BRDA:671,67,4,0 +BRDA:671,67,5,0 +BRDA:671,67,6,0 +BRDA:671,67,7,0 +BRDA:671,67,8,0 +BRDA:671,67,9,0 +BRDA:671,67,10,0 +BRDA:671,67,11,0 +BRDA:671,67,12,0 +BRDA:671,67,13,0 +BRDA:709,68,0,0 +BRDA:709,68,1,0 +BRDA:709,69,0,0 +BRDA:709,69,1,0 +BRDA:711,70,0,0 +BRDA:711,70,1,0 +BRDA:711,71,0,0 +BRDA:711,71,1,0 +BRDA:713,72,0,0 +BRDA:713,72,1,0 +BRDA:713,73,0,0 +BRDA:713,73,1,0 +BRDA:715,74,0,0 +BRDA:715,74,1,0 +BRDA:715,75,0,0 +BRDA:715,75,1,0 +BRDA:732,76,0,0 +BRDA:732,76,1,0 +BRDA:744,77,0,0 +BRDA:744,77,1,0 +BRDA:758,78,0,0 +BRDA:758,78,1,0 +BRDA:794,79,0,0 +BRDA:794,79,1,0 +BRDA:798,80,0,0 +BRDA:798,80,1,0 +BRDA:803,81,0,0 +BRDA:803,81,1,0 +BRDA:808,82,0,0 +BRDA:808,82,1,0 +BRDA:813,83,0,0 +BRDA:813,83,1,0 +BRDA:818,84,0,0 +BRDA:818,84,1,0 +BRDA:823,85,0,0 +BRDA:823,85,1,0 +BRDA:823,86,0,0 +BRDA:823,86,1,0 +BRDA:828,87,0,0 +BRDA:828,87,1,0 +BRDA:828,88,0,0 +BRDA:828,88,1,0 +BRDA:833,89,0,0 +BRDA:833,89,1,0 +BRDA:838,90,0,0 +BRDA:838,90,1,0 +BRDA:861,91,0,0 +BRDA:861,91,1,0 +BRDA:868,92,0,0 +BRDA:868,92,1,0 +BRDA:873,93,0,0 +BRDA:873,93,1,0 +BRDA:892,94,0,0 +BRDA:892,94,1,0 +BRDA:892,94,2,0 +BRDA:892,94,3,0 +BRDA:892,94,4,0 +BRDA:895,95,0,0 +BRDA:895,95,1,0 +BRDA:932,96,0,0 +BRDA:932,96,1,0 +BRDA:933,97,0,0 +BRDA:933,97,1,0 +BRF:214 +BRH:0 +end_of_record +TN: +SF:popup.js +FN:2,(anonymous_0) +FN:19,(anonymous_1) +FN:24,(anonymous_2) +FN:29,(anonymous_3) +FN:54,(anonymous_4) +FN:67,(anonymous_5) +FN:85,toggleButtons +FN:95,showStatus +FN:100,(anonymous_8) +FN:105,checkSelectorState +FN:128,setupTooltipAccessibility +FN:135,(anonymous_11) +FN:150,(anonymous_12) +FN:158,(anonymous_13) +FN:160,(anonymous_14) +FN:171,(anonymous_15) +FN:184,(anonymous_16) +FNF:17 +FNH:0 +FNDA:0,(anonymous_0) +FNDA:0,(anonymous_1) +FNDA:0,(anonymous_2) +FNDA:0,(anonymous_3) +FNDA:0,(anonymous_4) +FNDA:0,(anonymous_5) +FNDA:0,toggleButtons +FNDA:0,showStatus +FNDA:0,(anonymous_8) +FNDA:0,checkSelectorState +FNDA:0,setupTooltipAccessibility +FNDA:0,(anonymous_11) +FNDA:0,(anonymous_12) +FNDA:0,(anonymous_13) +FNDA:0,(anonymous_14) +FNDA:0,(anonymous_15) +FNDA:0,(anonymous_16) +DA:2,0 +DA:3,0 +DA:4,0 +DA:5,0 +DA:6,0 +DA:7,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:16,0 +DA:19,0 +DA:20,0 +DA:24,0 +DA:25,0 +DA:29,0 +DA:30,0 +DA:33,0 +DA:39,0 +DA:40,0 +DA:43,0 +DA:44,0 +DA:50,0 +DA:51,0 +DA:54,0 +DA:57,0 +DA:58,0 +DA:59,0 +DA:61,0 +DA:67,0 +DA:68,0 +DA:70,0 +DA:71,0 +DA:72,0 +DA:73,0 +DA:75,0 +DA:80,0 +DA:83,0 +DA:86,0 +DA:87,0 +DA:88,0 +DA:90,0 +DA:91,0 +DA:96,0 +DA:97,0 +DA:98,0 +DA:100,0 +DA:101,0 +DA:106,0 +DA:109,0 +DA:113,0 +DA:116,0 +DA:117,0 +DA:118,0 +DA:119,0 +DA:120,0 +DA:124,0 +DA:129,0 +DA:130,0 +DA:132,0 +DA:135,0 +DA:136,0 +DA:137,0 +DA:139,0 +DA:140,0 +DA:141,0 +DA:142,0 +DA:144,0 +DA:145,0 +DA:150,0 +DA:151,0 +DA:152,0 +DA:153,0 +DA:158,0 +DA:160,0 +DA:161,0 +DA:162,0 +DA:163,0 +DA:171,0 +DA:172,0 +DA:173,0 +DA:174,0 +DA:175,0 +DA:177,0 +DA:178,0 +DA:180,0 +DA:181,0 +DA:182,0 +DA:183,0 +DA:184,0 +LF:89 +LH:0 +BRDA:11,0,0,0 +BRDA:11,0,1,0 +BRDA:16,1,0,0 +BRDA:16,1,1,0 +BRDA:33,2,0,0 +BRDA:33,2,1,0 +BRDA:33,3,0,0 +BRDA:33,3,1,0 +BRDA:33,3,2,0 +BRDA:33,3,3,0 +BRDA:33,3,4,0 +BRDA:33,3,5,0 +BRDA:58,4,0,0 +BRDA:58,4,1,0 +BRDA:86,5,0,0 +BRDA:86,5,1,0 +BRDA:95,6,0,0 +BRDA:109,7,0,0 +BRDA:109,7,1,0 +BRDA:109,8,0,0 +BRDA:109,8,1,0 +BRDA:109,8,2,0 +BRDA:109,8,3,0 +BRDA:118,9,0,0 +BRDA:118,9,1,0 +BRDA:118,10,0,0 +BRDA:118,10,1,0 +BRDA:132,11,0,0 +BRDA:132,11,1,0 +BRDA:132,12,0,0 +BRDA:132,12,1,0 +BRDA:136,13,0,0 +BRDA:136,13,1,0 +BRDA:136,14,0,0 +BRDA:136,14,1,0 +BRDA:140,15,0,0 +BRDA:140,15,1,0 +BRDA:141,16,0,0 +BRDA:141,16,1,0 +BRDA:142,17,0,0 +BRDA:142,17,1,0 +BRDA:151,18,0,0 +BRDA:151,18,1,0 +BRDA:161,19,0,0 +BRDA:161,19,1,0 +BRDA:172,20,0,0 +BRDA:172,20,1,0 +BRDA:180,21,0,0 +BRDA:180,21,1,0 +BRF:49 +BRH:0 +end_of_record diff --git a/coverage/popup.js.html b/coverage/popup.js.html new file mode 100644 index 0000000..c932cc4 --- /dev/null +++ b/coverage/popup.js.html @@ -0,0 +1,643 @@ + + + + + + Code coverage report for popup.js + + + + + + + + + +
+
+

All files popup.js

+
+ +
+ 0% + Statements + 0/92 +
+ + +
+ 0% + Branches + 0/49 +
+ + +
+ 0% + Functions + 0/17 +
+ + +
+ 0% + Lines + 0/89 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
// Popup script for Chrome extension
+document.addEventListener('DOMContentLoaded', async () => {
+  const backgroundSelect = document.getElementById('background-select');
+  const clipboardToggle = document.getElementById('clipboard-toggle');
+  const startBtn = document.getElementById('start-selector');
+  const stopBtn = document.getElementById('stop-selector');
+  const status = document.getElementById('status');
+ 
+  // Load saved preferences
+  const saved = await chrome.storage.sync.get(['backgroundPreference', 'copyToClipboard']);
+  if (saved.backgroundPreference) {
+    backgroundSelect.value = saved.backgroundPreference;
+  }
+  
+  // Set clipboard preference (default to true if not set)
+  clipboardToggle.checked = saved.copyToClipboard !== undefined ? saved.copyToClipboard : true;
+ 
+  // Save background preference when changed
+  backgroundSelect.addEventListener('change', () => {
+    chrome.storage.sync.set({ backgroundPreference: backgroundSelect.value });
+  });
+ 
+  // Save clipboard preference when changed
+  clipboardToggle.addEventListener('change', () => {
+    chrome.storage.sync.set({ copyToClipboard: clipboardToggle.checked });
+  });
+ 
+  // Start selector
+  startBtn.addEventListener('click', async () => {
+    const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
+ 
+    // Check if page is compatible
+    if (tab.url.startsWith('chrome://') ||
+        tab.url.startsWith('chrome-extension://') ||
+        tab.url.startsWith('edge://') ||
+        tab.url.startsWith('about:') ||
+        tab.url === 'chrome://newtab/' ||
+        tab.url === 'edge://newtab/') {
+      showStatus('Error: Cannot capture screenshots on this page type.', 'error');
+      return;
+    }
+ 
+    try {
+      await chrome.tabs.sendMessage(tab.id, {
+        action: 'startSelector',
+        background: backgroundSelect.value,
+        copyToClipboard: clipboardToggle.checked
+      });
+ 
+      showStatus('Selector activated! Hover over elements and click to capture.', 'success');
+      toggleButtons(true);
+ 
+      // Auto-close popup after starting
+      setTimeout(() => window.close(), 1500);
+ 
+    } catch (error) {
+      console.error('Full error details:', error);
+      if (error.message.includes('Could not establish connection')) {
+        showStatus('Error: Content script not loaded. Try refreshing the page.', 'error');
+      } else {
+        showStatus('Error: Could not activate on this page.', 'error');
+      }
+    }
+  });
+ 
+  // Stop selector
+  stopBtn.addEventListener('click', async () => {
+    const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
+ 
+    try {
+      await chrome.tabs.sendMessage(tab.id, { action: 'stopSelector' });
+      showStatus('Selector stopped.', 'success');
+      toggleButtons(false);
+    } catch (error) {
+      console.error('Error stopping selector:', error);
+    }
+  });
+ 
+  // Check current state
+  checkSelectorState();
+ 
+  // Handle tooltip keyboard navigation
+  setupTooltipAccessibility();
+ 
+  function toggleButtons(isActive) {
+    if (isActive) {
+      startBtn.style.display = 'none';
+      stopBtn.style.display = 'block';
+    } else {
+      startBtn.style.display = 'block';
+      stopBtn.style.display = 'none';
+    }
+  }
+ 
+  function showStatus(message, type = 'success') {
+    status.textContent = message;
+    status.className = `status ${type}`;
+    status.style.display = 'block';
+ 
+    setTimeout(() => {
+      status.style.display = 'none';
+    }, 3000);
+  }
+ 
+  async function checkSelectorState() {
+    const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
+ 
+    // Skip check for incompatible pages
+    if (tab.url.startsWith('chrome://') ||
+        tab.url.startsWith('chrome-extension://') ||
+        tab.url.startsWith('edge://') ||
+        tab.url.startsWith('about:')) {
+      return;
+    }
+ 
+    try {
+      const response = await chrome.tabs.sendMessage(tab.id, { action: 'checkState' });
+      if (response && response.isActive) {
+        toggleButtons(true);
+        showStatus('Selector is currently active', 'success');
+      }
+    } catch (error) {
+      // Content script not ready or selector not active
+      console.log('Content script not ready or selector inactive:', error.message);
+    }
+  }
+ 
+  function setupTooltipAccessibility() {
+    const tooltipIcon = document.querySelector('.tooltip-icon');
+    const tooltipContent = document.querySelector('.tooltip-content');
+    
+    if (!tooltipIcon || !tooltipContent) return;
+ 
+    // Handle keyboard events for tooltip
+    tooltipIcon.addEventListener('keydown', (e) => {
+      if (e.key === 'Enter' || e.key === ' ') {
+        e.preventDefault();
+        // Toggle tooltip visibility on Enter/Space
+        const isVisible = tooltipContent.style.visibility === 'visible';
+        tooltipContent.style.visibility = isVisible ? 'hidden' : 'visible';
+        tooltipContent.style.opacity = isVisible ? '0' : '1';
+      } else if (e.key === 'Escape') {
+        // Hide tooltip on Escape
+        tooltipContent.style.visibility = 'hidden';
+        tooltipContent.style.opacity = '0';
+      }
+    });
+ 
+    // Hide tooltip when clicking outside
+    document.addEventListener('click', (e) => {
+      if (!tooltipIcon.contains(e.target)) {
+        tooltipContent.style.visibility = 'hidden';
+        tooltipContent.style.opacity = '0';
+      }
+    });
+ 
+    // Handle focus loss
+    tooltipIcon.addEventListener('blur', (e) => {
+      // Small delay to allow for focus to move to tooltip content if needed
+      setTimeout(() => {
+        if (!tooltipIcon.matches(':focus-within')) {
+          tooltipContent.style.visibility = 'hidden';
+          tooltipContent.style.opacity = '0';
+        }
+      }, 100);
+    });
+  }
+});
+ 
+// Listen for messages from content script
+chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
+  if (message.action === 'selectorStopped') {
+    const startBtn = document.getElementById('start-selector');
+    const stopBtn = document.getElementById('stop-selector');
+    const status = document.getElementById('status');
+ 
+    startBtn.style.display = 'block';
+    stopBtn.style.display = 'none';
+ 
+    if (message.reason === 'screenshot-taken') {
+      status.textContent = 'Screenshot captured!';
+      status.className = 'status success';
+      status.style.display = 'block';
+      setTimeout(() => status.style.display = 'none', 2000);
+    }
+  }
+});
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/prettify.css b/coverage/prettify.css new file mode 100644 index 0000000..b317a7c --- /dev/null +++ b/coverage/prettify.css @@ -0,0 +1 @@ +.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} diff --git a/coverage/prettify.js b/coverage/prettify.js new file mode 100644 index 0000000..b322523 --- /dev/null +++ b/coverage/prettify.js @@ -0,0 +1,2 @@ +/* eslint-disable */ +window.PR_SHOULD_USE_CONTINUATION=true;(function(){var h=["break,continue,do,else,for,if,return,while"];var u=[h,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"];var p=[u,"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"];var l=[p,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"];var x=[p,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"];var R=[x,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"];var r="all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes";var w=[p,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"];var s="caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END";var I=[h,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"];var f=[h,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"];var H=[h,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"];var A=[l,R,w,s+I,f,H];var e=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/;var C="str";var z="kwd";var j="com";var O="typ";var G="lit";var L="pun";var F="pln";var m="tag";var E="dec";var J="src";var P="atn";var n="atv";var N="nocode";var M="(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*";function k(Z){var ad=0;var S=false;var ac=false;for(var V=0,U=Z.length;V122)){if(!(al<65||ag>90)){af.push([Math.max(65,ag)|32,Math.min(al,90)|32])}if(!(al<97||ag>122)){af.push([Math.max(97,ag)&~32,Math.min(al,122)&~32])}}}}af.sort(function(av,au){return(av[0]-au[0])||(au[1]-av[1])});var ai=[];var ap=[NaN,NaN];for(var ar=0;arat[0]){if(at[1]+1>at[0]){an.push("-")}an.push(T(at[1]))}}an.push("]");return an.join("")}function W(al){var aj=al.source.match(new RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g"));var ah=aj.length;var an=[];for(var ak=0,am=0;ak=2&&ai==="["){aj[ak]=X(ag)}else{if(ai!=="\\"){aj[ak]=ag.replace(/[a-zA-Z]/g,function(ao){var ap=ao.charCodeAt(0);return"["+String.fromCharCode(ap&~32,ap|32)+"]"})}}}}return aj.join("")}var aa=[];for(var V=0,U=Z.length;V=0;){S[ac.charAt(ae)]=Y}}var af=Y[1];var aa=""+af;if(!ag.hasOwnProperty(aa)){ah.push(af);ag[aa]=null}}ah.push(/[\0-\uffff]/);V=k(ah)})();var X=T.length;var W=function(ah){var Z=ah.sourceCode,Y=ah.basePos;var ad=[Y,F];var af=0;var an=Z.match(V)||[];var aj={};for(var ae=0,aq=an.length;ae=5&&"lang-"===ap.substring(0,5);if(am&&!(ai&&typeof ai[1]==="string")){am=false;ap=J}if(!am){aj[ag]=ap}}var ab=af;af+=ag.length;if(!am){ad.push(Y+ab,ap)}else{var al=ai[1];var ak=ag.indexOf(al);var ac=ak+al.length;if(ai[2]){ac=ag.length-ai[2].length;ak=ac-al.length}var ar=ap.substring(5);B(Y+ab,ag.substring(0,ak),W,ad);B(Y+ab+ak,al,q(ar,al),ad);B(Y+ab+ac,ag.substring(ac),W,ad)}}ah.decorations=ad};return W}function i(T){var W=[],S=[];if(T.tripleQuotedStrings){W.push([C,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,null,"'\""])}else{if(T.multiLineStrings){W.push([C,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"])}else{W.push([C,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"])}}if(T.verbatimStrings){S.push([C,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null])}var Y=T.hashComments;if(Y){if(T.cStyleComments){if(Y>1){W.push([j,/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,null,"#"])}else{W.push([j,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"])}S.push([C,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,null])}else{W.push([j,/^#[^\r\n]*/,null,"#"])}}if(T.cStyleComments){S.push([j,/^\/\/[^\r\n]*/,null]);S.push([j,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}if(T.regexLiterals){var X=("/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/");S.push(["lang-regex",new RegExp("^"+M+"("+X+")")])}var V=T.types;if(V){S.push([O,V])}var U=(""+T.keywords).replace(/^ | $/g,"");if(U.length){S.push([z,new RegExp("^(?:"+U.replace(/[\s,]+/g,"|")+")\\b"),null])}W.push([F,/^\s+/,null," \r\n\t\xA0"]);S.push([G,/^@[a-z_$][a-z_$@0-9]*/i,null],[O,/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],[F,/^[a-z_$][a-z_$@0-9]*/i,null],[G,new RegExp("^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*","i"),null,"0123456789"],[F,/^\\[\s\S]?/,null],[L,/^.[^\s\w\.$@\'\"\`\/\#\\]*/,null]);return g(W,S)}var K=i({keywords:A,hashComments:true,cStyleComments:true,multiLineStrings:true,regexLiterals:true});function Q(V,ag){var U=/(?:^|\s)nocode(?:\s|$)/;var ab=/\r\n?|\n/;var ac=V.ownerDocument;var S;if(V.currentStyle){S=V.currentStyle.whiteSpace}else{if(window.getComputedStyle){S=ac.defaultView.getComputedStyle(V,null).getPropertyValue("white-space")}}var Z=S&&"pre"===S.substring(0,3);var af=ac.createElement("LI");while(V.firstChild){af.appendChild(V.firstChild)}var W=[af];function ae(al){switch(al.nodeType){case 1:if(U.test(al.className)){break}if("BR"===al.nodeName){ad(al);if(al.parentNode){al.parentNode.removeChild(al)}}else{for(var an=al.firstChild;an;an=an.nextSibling){ae(an)}}break;case 3:case 4:if(Z){var am=al.nodeValue;var aj=am.match(ab);if(aj){var ai=am.substring(0,aj.index);al.nodeValue=ai;var ah=am.substring(aj.index+aj[0].length);if(ah){var ak=al.parentNode;ak.insertBefore(ac.createTextNode(ah),al.nextSibling)}ad(al);if(!ai){al.parentNode.removeChild(al)}}}break}}function ad(ak){while(!ak.nextSibling){ak=ak.parentNode;if(!ak){return}}function ai(al,ar){var aq=ar?al.cloneNode(false):al;var ao=al.parentNode;if(ao){var ap=ai(ao,1);var an=al.nextSibling;ap.appendChild(aq);for(var am=an;am;am=an){an=am.nextSibling;ap.appendChild(am)}}return aq}var ah=ai(ak.nextSibling,0);for(var aj;(aj=ah.parentNode)&&aj.nodeType===1;){ah=aj}W.push(ah)}for(var Y=0;Y=S){ah+=2}if(V>=ap){Z+=2}}}var t={};function c(U,V){for(var S=V.length;--S>=0;){var T=V[S];if(!t.hasOwnProperty(T)){t[T]=U}else{if(window.console){console.warn("cannot override language handler %s",T)}}}}function q(T,S){if(!(T&&t.hasOwnProperty(T))){T=/^\s*]*(?:>|$)/],[j,/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],[L,/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);c(g([[F,/^[\s]+/,null," \t\r\n"],[n,/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[[m,/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],[P,/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],[L,/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);c(g([],[[n,/^[\s\S]+/]]),["uq.val"]);c(i({keywords:l,hashComments:true,cStyleComments:true,types:e}),["c","cc","cpp","cxx","cyc","m"]);c(i({keywords:"null,true,false"}),["json"]);c(i({keywords:R,hashComments:true,cStyleComments:true,verbatimStrings:true,types:e}),["cs"]);c(i({keywords:x,cStyleComments:true}),["java"]);c(i({keywords:H,hashComments:true,multiLineStrings:true}),["bsh","csh","sh"]);c(i({keywords:I,hashComments:true,multiLineStrings:true,tripleQuotedStrings:true}),["cv","py"]);c(i({keywords:s,hashComments:true,multiLineStrings:true,regexLiterals:true}),["perl","pl","pm"]);c(i({keywords:f,hashComments:true,multiLineStrings:true,regexLiterals:true}),["rb"]);c(i({keywords:w,cStyleComments:true,regexLiterals:true}),["js"]);c(i({keywords:r,hashComments:3,cStyleComments:true,multilineStrings:true,tripleQuotedStrings:true,regexLiterals:true}),["coffee"]);c(g([],[[C,/^[\s\S]+/]]),["regex"]);function d(V){var U=V.langExtension;try{var S=a(V.sourceNode);var T=S.sourceCode;V.sourceCode=T;V.spans=S.spans;V.basePos=0;q(U,T)(V);D(V)}catch(W){if("console" in window){console.log(W&&W.stack?W.stack:W)}}}function y(W,V,U){var S=document.createElement("PRE");S.innerHTML=W;if(U){Q(S,U)}var T={langExtension:V,numberLines:U,sourceNode:S};d(T);return S.innerHTML}function b(ad){function Y(af){return document.getElementsByTagName(af)}var ac=[Y("pre"),Y("code"),Y("xmp")];var T=[];for(var aa=0;aa=0){var ah=ai.match(ab);var am;if(!ah&&(am=o(aj))&&"CODE"===am.tagName){ah=am.className.match(ab)}if(ah){ah=ah[1]}var al=false;for(var ak=aj.parentNode;ak;ak=ak.parentNode){if((ak.tagName==="pre"||ak.tagName==="code"||ak.tagName==="xmp")&&ak.className&&ak.className.indexOf("prettyprint")>=0){al=true;break}}if(!al){var af=aj.className.match(/\blinenums\b(?::(\d+))?/);af=af?af[1]&&af[1].length?+af[1]:true:false;if(af){Q(aj,af)}S={langExtension:ah,sourceNode:aj,numberLines:af};d(S)}}}if(X]*(?:>|$)/],[PR.PR_COMMENT,/^<\!--[\s\S]*?(?:-\->|$)/],[PR.PR_PUNCTUATION,/^(?:<[%?]|[%?]>)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-handlebars",/^]*type\s*=\s*['"]?text\/x-handlebars-template['"]?\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i],[PR.PR_DECLARATION,/^{{[#^>/]?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{&?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{{>?\s*[\w.][^}]*}}}/],[PR.PR_COMMENT,/^{{![^}]*}}/]]),["handlebars","hbs"]);PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[ \t\r\n\f]+/,null," \t\r\n\f"]],[[PR.PR_STRING,/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],[PR.PR_STRING,/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']*)\)/i],[PR.PR_KEYWORD,/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],[PR.PR_COMMENT,/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//],[PR.PR_COMMENT,/^(?:)/],[PR.PR_LITERAL,/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],[PR.PR_LITERAL,/^#(?:[0-9a-f]{3}){1,2}/i],[PR.PR_PLAIN,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i],[PR.PR_PUNCTUATION,/^[^\s\w\'\"]+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_KEYWORD,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_STRING,/^[^\)\"\']+/]]),["css-str"]); diff --git a/coverage/sort-arrow-sprite.png b/coverage/sort-arrow-sprite.png new file mode 100644 index 0000000000000000000000000000000000000000..6ed68316eb3f65dec9063332d2f69bf3093bbfab GIT binary patch literal 138 zcmeAS@N?(olHy`uVBq!ia0vp^>_9Bd!3HEZxJ@+%Qh}Z>jv*C{$p!i!8j}?a+@3A= zIAGwzjijN=FBi!|L1t?LM;Q;gkwn>2cAy-KV{dn nf0J1DIvEHQu*n~6U}x}qyky7vi4|9XhBJ7&`njxgN@xNA8m%nc literal 0 HcmV?d00001 diff --git a/coverage/sorter.js b/coverage/sorter.js new file mode 100644 index 0000000..2bb296a --- /dev/null +++ b/coverage/sorter.js @@ -0,0 +1,196 @@ +/* eslint-disable */ +var addSorting = (function() { + 'use strict'; + var cols, + currentSort = { + index: 0, + desc: false + }; + + // returns the summary table element + function getTable() { + return document.querySelector('.coverage-summary'); + } + // returns the thead element of the summary table + function getTableHeader() { + return getTable().querySelector('thead tr'); + } + // returns the tbody element of the summary table + function getTableBody() { + return getTable().querySelector('tbody'); + } + // returns the th element for nth column + function getNthColumn(n) { + return getTableHeader().querySelectorAll('th')[n]; + } + + function onFilterInput() { + const searchValue = document.getElementById('fileSearch').value; + const rows = document.getElementsByTagName('tbody')[0].children; + for (let i = 0; i < rows.length; i++) { + const row = rows[i]; + if ( + row.textContent + .toLowerCase() + .includes(searchValue.toLowerCase()) + ) { + row.style.display = ''; + } else { + row.style.display = 'none'; + } + } + } + + // loads the search box + function addSearchBox() { + var template = document.getElementById('filterTemplate'); + var templateClone = template.content.cloneNode(true); + templateClone.getElementById('fileSearch').oninput = onFilterInput; + template.parentElement.appendChild(templateClone); + } + + // loads all columns + function loadColumns() { + var colNodes = getTableHeader().querySelectorAll('th'), + colNode, + cols = [], + col, + i; + + for (i = 0; i < colNodes.length; i += 1) { + colNode = colNodes[i]; + col = { + key: colNode.getAttribute('data-col'), + sortable: !colNode.getAttribute('data-nosort'), + type: colNode.getAttribute('data-type') || 'string' + }; + cols.push(col); + if (col.sortable) { + col.defaultDescSort = col.type === 'number'; + colNode.innerHTML = + colNode.innerHTML + ''; + } + } + return cols; + } + // attaches a data attribute to every tr element with an object + // of data values keyed by column name + function loadRowData(tableRow) { + var tableCols = tableRow.querySelectorAll('td'), + colNode, + col, + data = {}, + i, + val; + for (i = 0; i < tableCols.length; i += 1) { + colNode = tableCols[i]; + col = cols[i]; + val = colNode.getAttribute('data-value'); + if (col.type === 'number') { + val = Number(val); + } + data[col.key] = val; + } + return data; + } + // loads all row data + function loadData() { + var rows = getTableBody().querySelectorAll('tr'), + i; + + for (i = 0; i < rows.length; i += 1) { + rows[i].data = loadRowData(rows[i]); + } + } + // sorts the table using the data for the ith column + function sortByIndex(index, desc) { + var key = cols[index].key, + sorter = function(a, b) { + a = a.data[key]; + b = b.data[key]; + return a < b ? -1 : a > b ? 1 : 0; + }, + finalSorter = sorter, + tableBody = document.querySelector('.coverage-summary tbody'), + rowNodes = tableBody.querySelectorAll('tr'), + rows = [], + i; + + if (desc) { + finalSorter = function(a, b) { + return -1 * sorter(a, b); + }; + } + + for (i = 0; i < rowNodes.length; i += 1) { + rows.push(rowNodes[i]); + tableBody.removeChild(rowNodes[i]); + } + + rows.sort(finalSorter); + + for (i = 0; i < rows.length; i += 1) { + tableBody.appendChild(rows[i]); + } + } + // removes sort indicators for current column being sorted + function removeSortIndicators() { + var col = getNthColumn(currentSort.index), + cls = col.className; + + cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, ''); + col.className = cls; + } + // adds sort indicators for current column being sorted + function addSortIndicators() { + getNthColumn(currentSort.index).className += currentSort.desc + ? ' sorted-desc' + : ' sorted'; + } + // adds event listeners for all sorter widgets + function enableUI() { + var i, + el, + ithSorter = function ithSorter(i) { + var col = cols[i]; + + return function() { + var desc = col.defaultDescSort; + + if (currentSort.index === i) { + desc = !currentSort.desc; + } + sortByIndex(i, desc); + removeSortIndicators(); + currentSort.index = i; + currentSort.desc = desc; + addSortIndicators(); + }; + }; + for (i = 0; i < cols.length; i += 1) { + if (cols[i].sortable) { + // add the click event handler on the th so users + // dont have to click on those tiny arrows + el = getNthColumn(i).querySelector('.sorter').parentElement; + if (el.addEventListener) { + el.addEventListener('click', ithSorter(i)); + } else { + el.attachEvent('onclick', ithSorter(i)); + } + } + } + } + // adds sorting functionality to the UI + return function() { + if (!getTable()) { + return; + } + cols = loadColumns(); + loadData(); + addSearchBox(); + addSortIndicators(); + enableUI(); + }; +})(); + +window.addEventListener('load', addSorting); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..fbfe3d4 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4466 @@ +{ + "name": "full-screenshot-selector-tests", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "full-screenshot-selector-tests", + "version": "1.0.0", + "devDependencies": { + "@types/jest": "^29.5.8", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", + "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.6", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", + "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.0", + "@babel/types": "^7.28.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz", + "integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", + "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.30", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", + "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/jsdom": { + "version": "20.0.1", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz", + "integrity": "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, + "node_modules/@types/node": { + "version": "24.2.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.1.tgz", + "integrity": "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.10.0" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-globals": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", + "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.1.0", + "acorn-walk": "^8.0.2" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.2.tgz", + "integrity": "sha512-0si2SJK3ooGzIawRu61ZdPCO1IncZwS8IzuX73sPZsXW6EQ/w/DAfPyKI8l1ETTCr2MnvqWitmlCUxgdul45jA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001733", + "electron-to-chromium": "^1.5.199", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001734", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001734.tgz", + "integrity": "sha512-uhE1Ye5vgqju6OI71HTQqcBCZrvHugk0MjLak7Q+HfoBgoq5Bi+5YnwjP4fjDgrtYr/l8MVRBvzz9dPD4KyK0A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssom": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssom": "~0.3.6" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cssstyle/node_modules/cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", + "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/dedent": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", + "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/domexception": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "license": "MIT", + "dependencies": { + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.200", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.200.tgz", + "integrity": "sha512-rFCxROw7aOe4uPTfIAx+rXv9cEcGx+buAF4npnhtTqCJk5KDFRnh3+KYj7rdVh6lsFt5/aPs+Irj9rZ33WMA7w==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-jsdom": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz", + "integrity": "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/jsdom": "^20.0.0", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0", + "jsdom": "^20.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", + "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.6", + "acorn": "^8.8.1", + "acorn-globals": "^7.0.0", + "cssom": "^0.5.0", + "cssstyle": "^2.3.0", + "data-urls": "^3.0.2", + "decimal.js": "^10.4.2", + "domexception": "^4.0.0", + "escodegen": "^2.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.2", + "parse5": "^7.1.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0", + "ws": "^8.11.0", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nwsapi": { + "version": "2.2.21", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.21.tgz", + "integrity": "sha512-o6nIY3qwiSXl7/LuOU0Dmuctd34Yay0yeuZRLFmDPrrdHpXKFndPj3hM+YEPVHYC5fx2otBx4Ilc/gyYSAUaIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..9b37ffd --- /dev/null +++ b/package.json @@ -0,0 +1,37 @@ +{ + "name": "full-screenshot-selector-tests", + "version": "1.0.0", + "description": "Test suite for Full Screenshot Selector Chrome Extension clipboard functionality", + "scripts": { + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage", + "test:clipboard": "jest tests/clipboard-functionality.test.js", + "test:persistence": "jest tests/preference-persistence.test.js", + "test:integration": "jest tests/integration-scenarios.test.js" + }, + "devDependencies": { + "@types/jest": "^29.5.8", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0" + }, + "jest": { + "testEnvironment": "jsdom", + "setupFilesAfterEnv": [ + "/tests/setup.js" + ], + "collectCoverageFrom": [ + "content.js", + "popup.js", + "!node_modules/**" + ], + "coverageReporters": [ + "text", + "lcov", + "html" + ], + "testMatch": [ + "**/tests/**/*.test.js" + ] + } +} diff --git a/popup.html b/popup.html index 8dea928..89d5448 100644 --- a/popup.html +++ b/popup.html @@ -80,6 +80,140 @@ background: #545b62; } + .toggle-container { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 20px; + } + + .toggle-label { + font-weight: 500; + color: #333; + font-size: 14px; + margin: 0; + cursor: pointer; + } + + .toggle-switch { + position: relative; + display: inline-block; + width: 44px; + height: 24px; + } + + .toggle-switch input { + opacity: 0; + width: 0; + height: 0; + } + + .toggle-slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + transition: 0.3s; + border-radius: 24px; + } + + .toggle-slider:before { + position: absolute; + content: ""; + height: 18px; + width: 18px; + left: 3px; + bottom: 3px; + background-color: white; + transition: 0.3s; + border-radius: 50%; + } + + input:checked + .toggle-slider { + background-color: #007bff; + } + + input:focus + .toggle-slider { + box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25); + } + + input:checked + .toggle-slider:before { + transform: translateX(20px); + } + + .toggle-help { + font-size: 11px; + color: #666; + margin-top: 4px; + line-height: 1.3; + } + + .tooltip { + position: relative; + display: inline-block; + margin-left: 6px; + cursor: help; + } + + .tooltip-icon { + width: 14px; + height: 14px; + border-radius: 50%; + background: #6c757d; + color: white; + font-size: 10px; + font-weight: bold; + display: inline-flex; + align-items: center; + justify-content: center; + user-select: none; + } + + .tooltip-content { + visibility: hidden; + width: 280px; + background-color: #333; + color: #fff; + text-align: left; + border-radius: 6px; + padding: 12px; + position: absolute; + z-index: 1000; + bottom: 125%; + left: 50%; + margin-left: -140px; + opacity: 0; + transition: opacity 0.3s, visibility 0.3s; + font-size: 12px; + line-height: 1.4; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + } + + .tooltip-content::after { + content: ""; + position: absolute; + top: 100%; + left: 50%; + margin-left: -5px; + border-width: 5px; + border-style: solid; + border-color: #333 transparent transparent transparent; + } + + .tooltip:hover .tooltip-content, + .tooltip:focus-within .tooltip-content { + visibility: visible; + opacity: 1; + } + + .tooltip-icon:focus { + outline: 2px solid #007bff; + outline-offset: 2px; + } + .shortcut-info { font-size: 11px; color: #666; @@ -124,6 +258,39 @@ +
+
+ +
+ + +
+
+
+ Automatically copy screenshots to clipboard for quick pasting. Hover over the "?" for more details. +
+
+ diff --git a/popup.js b/popup.js index d5fffc3..edebae4 100644 --- a/popup.js +++ b/popup.js @@ -1,21 +1,30 @@ // Popup script for Chrome extension document.addEventListener('DOMContentLoaded', async () => { const backgroundSelect = document.getElementById('background-select'); + const clipboardToggle = document.getElementById('clipboard-toggle'); const startBtn = document.getElementById('start-selector'); const stopBtn = document.getElementById('stop-selector'); const status = document.getElementById('status'); - // Load saved background preference - const saved = await chrome.storage.sync.get(['backgroundPreference']); + // Load saved preferences + const saved = await chrome.storage.sync.get(['backgroundPreference', 'copyToClipboard']); if (saved.backgroundPreference) { backgroundSelect.value = saved.backgroundPreference; } + + // Set clipboard preference (default to true if not set) + clipboardToggle.checked = saved.copyToClipboard !== undefined ? saved.copyToClipboard : true; // Save background preference when changed backgroundSelect.addEventListener('change', () => { chrome.storage.sync.set({ backgroundPreference: backgroundSelect.value }); }); + // Save clipboard preference when changed + clipboardToggle.addEventListener('change', () => { + chrome.storage.sync.set({ copyToClipboard: clipboardToggle.checked }); + }); + // Start selector startBtn.addEventListener('click', async () => { const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); @@ -34,7 +43,8 @@ document.addEventListener('DOMContentLoaded', async () => { try { await chrome.tabs.sendMessage(tab.id, { action: 'startSelector', - background: backgroundSelect.value + background: backgroundSelect.value, + copyToClipboard: clipboardToggle.checked }); showStatus('Selector activated! Hover over elements and click to capture.', 'success'); @@ -69,6 +79,9 @@ document.addEventListener('DOMContentLoaded', async () => { // Check current state checkSelectorState(); + // Handle tooltip keyboard navigation + setupTooltipAccessibility(); + function toggleButtons(isActive) { if (isActive) { startBtn.style.display = 'none'; @@ -111,6 +124,47 @@ document.addEventListener('DOMContentLoaded', async () => { console.log('Content script not ready or selector inactive:', error.message); } } + + function setupTooltipAccessibility() { + const tooltipIcon = document.querySelector('.tooltip-icon'); + const tooltipContent = document.querySelector('.tooltip-content'); + + if (!tooltipIcon || !tooltipContent) return; + + // Handle keyboard events for tooltip + tooltipIcon.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + // Toggle tooltip visibility on Enter/Space + const isVisible = tooltipContent.style.visibility === 'visible'; + tooltipContent.style.visibility = isVisible ? 'hidden' : 'visible'; + tooltipContent.style.opacity = isVisible ? '0' : '1'; + } else if (e.key === 'Escape') { + // Hide tooltip on Escape + tooltipContent.style.visibility = 'hidden'; + tooltipContent.style.opacity = '0'; + } + }); + + // Hide tooltip when clicking outside + document.addEventListener('click', (e) => { + if (!tooltipIcon.contains(e.target)) { + tooltipContent.style.visibility = 'hidden'; + tooltipContent.style.opacity = '0'; + } + }); + + // Handle focus loss + tooltipIcon.addEventListener('blur', (e) => { + // Small delay to allow for focus to move to tooltip content if needed + setTimeout(() => { + if (!tooltipIcon.matches(':focus-within')) { + tooltipContent.style.visibility = 'hidden'; + tooltipContent.style.opacity = '0'; + } + }, 100); + }); + } }); // Listen for messages from content script -- 2.49.1 From 0687c12b92e87fb7ab8ac94af0bba7c0ba6f9656 Mon Sep 17 00:00:00 2001 From: KarthiDreamr Date: Wed, 13 Aug 2025 21:01:18 +0530 Subject: [PATCH 03/10] feat: added tests --- tests/clipboard-functionality.test.js | 480 ++++++++++++++++++++++++ tests/integration-scenarios.test.js | 521 ++++++++++++++++++++++++++ tests/preference-persistence.test.js | 414 ++++++++++++++++++++ tests/setup.js | 237 ++++++++++++ 4 files changed, 1652 insertions(+) create mode 100644 tests/clipboard-functionality.test.js create mode 100644 tests/integration-scenarios.test.js create mode 100644 tests/preference-persistence.test.js create mode 100644 tests/setup.js diff --git a/tests/clipboard-functionality.test.js b/tests/clipboard-functionality.test.js new file mode 100644 index 0000000..ffdf9bd --- /dev/null +++ b/tests/clipboard-functionality.test.js @@ -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.'); + }); +}); \ No newline at end of file diff --git a/tests/integration-scenarios.test.js b/tests/integration-scenarios.test.js new file mode 100644 index 0000000..2221bf7 --- /dev/null +++ b/tests/integration-scenarios.test.js @@ -0,0 +1,521 @@ +/** + * 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(); + + // 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); + + // Mock full clipboard API support + global.navigator = { + clipboard: { + write: jest.fn().mockResolvedValue() + } + }; + global.window.ClipboardItem = jest.fn(); + + // Import ScreenshotSelector (simplified for testing) + const ScreenshotSelector = class { + constructor() { + this.isActive = false; + this.background = 'black'; + this.copyToClipboard = true; + this.overlay = null; + } + + async init(background = 'black', copyToClipboard = true) { + this.background = background; + this.copyToClipboard = copyToClipboard; + 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 + let successMessage = 'Screenshot downloaded successfully!'; + if (this.copyToClipboard && clipboardResult && clipboardResult.success) { + successMessage = 'Screenshot downloaded and copied to clipboard!'; + } else if (this.copyToClipboard && clipboardResult && !clipboardResult.success) { + successMessage = `Screenshot downloaded successfully! Clipboard copy failed: ${clipboardResult.error}`; + } + + this.overlay.innerHTML = `
${successMessage}
`; + + return { + success: true, + downloadSuccess: true, + clipboardResult: clipboardResult, + message: successMessage + }; + + } catch (error) { + this.overlay.innerHTML = `
Screenshot failed: ${error.message}
`; + 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 downloaded 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 downloaded 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 downloaded and copied to clipboard!'); + }); + + test('should capture without clipboard when disabled', async () => { + await screenshotSelector.init('black', false); + + const result = await screenshotSelector.captureElement(mockElement); + + expect(result.success).toBe(true); + expect(result.downloadSuccess).toBe(true); + expect(result.clipboardResult).toBe(null); // No clipboard operation attempted + expect(result.message).toBe('Screenshot downloaded successfully!'); + expect(global.navigator.clipboard.write).not.toHaveBeenCalled(); + }); + + test('should handle clipboard failure gracefully while maintaining download', async () => { + // Mock clipboard failure + global.navigator.clipboard.write.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 downloaded successfully!'); + expect(result.message).toContain('Clipboard copy failed'); + }); + + 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 downloaded successfully!'); + expect(result.message).toContain('Clipboard copy failed'); + }); +}); + +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(); + }); +}); \ No newline at end of file diff --git a/tests/preference-persistence.test.js b/tests/preference-persistence.test.js new file mode 100644 index 0000000..6db7941 --- /dev/null +++ b/tests/preference-persistence.test.js @@ -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 + }); + }); +}); \ No newline at end of file diff --git a/tests/setup.js b/tests/setup.js new file mode 100644 index 0000000..4ba46b1 --- /dev/null +++ b/tests/setup.js @@ -0,0 +1,237 @@ +/** + * Jest setup file for clipboard functionality tests + * Provides common mocks and utilities for all test files + */ + +// 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 = () => { + global.chrome = { + storage: { + sync: { + get: jest.fn().mockResolvedValue({}), + set: jest.fn().mockResolvedValue() + } + }, + runtime: { + sendMessage: jest.fn().mockResolvedValue(), + onMessage: { + addListener: jest.fn() + } + }, + tabs: { + query: jest.fn().mockResolvedValue([{ id: 123, url: 'https://example.com' }]), + sendMessage: jest.fn().mockResolvedValue() + } + }; +}; + +// Mock DOM APIs consistently +global.mockDOMAPIs = () => { + 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: '' + })), + head: { appendChild: jest.fn() }, + body: { appendChild: jest.fn() }, + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + querySelectorAll: jest.fn(() => []), + getElementById: jest.fn(() => null) + }; + + 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; + }), + clearTimeout: jest.fn(), + close: jest.fn() + }; +}; + +// Mock clipboard APIs with different scenarios +global.mockClipboardAPI = (scenario = 'supported') => { + switch (scenario) { + case 'supported': + global.navigator = { + clipboard: { + write: jest.fn().mockResolvedValue() + } + }; + global.window.ClipboardItem = jest.fn(); + global.window.isSecureContext = true; + break; + + case 'no-clipboard': + global.navigator = {}; + break; + + case 'no-clipboarditem': + global.navigator = { + clipboard: { write: jest.fn() } + }; + global.window.ClipboardItem = undefined; + break; + + case 'insecure-context': + global.navigator = { + clipboard: { write: jest.fn() } + }; + global.window.ClipboardItem = jest.fn(); + global.window.isSecureContext = false; + break; + + case 'permission-denied': + global.navigator = { + clipboard: { + write: jest.fn().mockRejectedValue(new DOMException('Permission denied', 'NotAllowedError')) + } + }; + global.window.ClipboardItem = jest.fn(); + global.window.isSecureContext = true; + break; + + case 'security-error': + global.navigator = { + clipboard: { + write: jest.fn().mockRejectedValue(new DOMException('Security error', 'SecurityError')) + } + }; + 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 + }; + } + } +}); \ No newline at end of file -- 2.49.1 From 3342ba7e15e53fa3edf005fc62d17cc0a9edcf41 Mon Sep 17 00:00:00 2001 From: KarthiDreamr Date: Wed, 13 Aug 2025 21:03:22 +0530 Subject: [PATCH 04/10] feat: added feature spec --- .kiro/specs/save-to-pc-toggle/design.md | 187 ++++++++++++++++++ .kiro/specs/save-to-pc-toggle/requirements.md | 51 +++++ .kiro/specs/save-to-pc-toggle/tasks.md | 71 +++++++ 3 files changed, 309 insertions(+) create mode 100644 .kiro/specs/save-to-pc-toggle/design.md create mode 100644 .kiro/specs/save-to-pc-toggle/requirements.md create mode 100644 .kiro/specs/save-to-pc-toggle/tasks.md diff --git a/.kiro/specs/save-to-pc-toggle/design.md b/.kiro/specs/save-to-pc-toggle/design.md new file mode 100644 index 0000000..b877881 --- /dev/null +++ b/.kiro/specs/save-to-pc-toggle/design.md @@ -0,0 +1,187 @@ +# Design Document + +## Overview + +The save to PC toggle feature will be implemented as an additional setting in the extension popup, positioned alongside the existing clipboard toggle. The feature will modify the existing screenshot capture workflow to conditionally perform file downloads based on user preference. The implementation will ensure that users can independently control both clipboard copying and file downloading, providing maximum flexibility in their screenshot workflow. + +## Architecture + +The feature will be implemented across three main components: + +1. **Popup UI Enhancement**: Add toggle control and persistence logic +2. **Content Script Integration**: Modify screenshot capture to conditionally download files +3. **Storage Management**: Extend existing preference storage system +4. **Validation Logic**: Ensure at least one output method is selected + +The save to PC functionality will be implemented as a conditional enhancement to the existing screenshot workflow, maintaining backward compatibility while providing new flexibility. + +## Components and Interfaces + +### Popup Interface (`popup.html` & `popup.js`) + +**New UI Elements:** +- Toggle switch control for "Save to PC" option +- Positioned above the clipboard toggle for logical grouping +- Consistent styling with existing controls +- Warning message when both toggles are disabled + +**Storage Integration:** +- Extend existing `chrome.storage.sync` usage to include `saveToPc` preference +- Default value: `true` (enabled by default to maintain existing behavior) +- Load and save preferences alongside existing background and clipboard preferences + +**Validation Logic:** +- Check that at least one output method (save to PC or clipboard) is enabled +- Display warning message when both are disabled +- Prevent screenshot capture when no output method is selected + +**Message Passing:** +- Include `saveToPc` setting in the `startSelector` message to content script +- No changes needed to existing message structure, just additional property + +### Content Script Enhancement (`content.js`) + +**ScreenshotSelector Class Modifications:** +- Add `saveToPc` property to constructor +- Modify `init()` method to accept and store save to PC preference +- Enhance `captureElement()` method to conditionally perform file downloads + +**Conditional Download Implementation:** +```javascript +// In captureElement method, replace automatic download with conditional logic +if (this.saveToPc) { + const link = document.createElement('a'); + link.href = canvas.toDataURL('image/png'); + link.download = `screenshot-${new Date().toISOString().slice(0, 19).replace(/[:.]/g, '-')}.png`; + link.click(); +} +``` + +**User Feedback Enhancement:** +- Update success messages to reflect which operations were performed +- Handle cases where no output method is selected (should not occur due to popup validation) + +### User Feedback System + +**Success States:** +- "Screenshot saved and copied to clipboard!" - when both operations succeed +- "Screenshot saved!" - when only save to PC is enabled and succeeds +- "Screenshot copied to clipboard!" - when only clipboard is enabled and succeeds +- "Screenshot captured!" - fallback message for edge cases + +**Error States:** +- "Please enable at least one output method (Save to PC or Copy to Clipboard)" - when both are disabled +- Existing clipboard error messages remain unchanged + +**Loading States:** +- Update loading message to reflect enabled operations +- "Capturing screenshot..." - when only save to PC is enabled +- "Capturing screenshot and preparing for clipboard..." - when only clipboard is enabled +- "Capturing screenshot and preparing for clipboard..." - when both are enabled (clipboard preparation is the longer operation) + +## Data Models + +### Settings Storage Schema +```javascript +{ + backgroundPreference: 'black' | 'transparent' | 'white', + copyToClipboard: boolean, + saveToPc: boolean // New addition +} +``` + +### Message Interface Extension +```javascript +// startSelector message +{ + action: 'startSelector', + background: string, + copyToClipboard: boolean, + saveToPc: boolean // New addition +} +``` + +## Error Handling + +### Validation Errors +- Prevent screenshot capture when both save to PC and clipboard are disabled +- Display clear error message in popup when no output method is selected +- Provide guidance on enabling at least one option + +### Download Failures +- Handle file download failures gracefully (though these are rare in modern browsers) +- Ensure clipboard operations continue even if download fails (when both are enabled) +- Provide specific error messages for download-related issues + +### Backward Compatibility +- Ensure existing users see no change in behavior (save to PC enabled by default) +- Handle cases where preference is not yet set in storage +- Maintain existing error handling for clipboard operations + +## Testing Strategy + +### Unit Testing Approach +- Test preference storage and retrieval for save to PC setting +- Verify message passing includes new saveToPc property +- Test validation logic for ensuring at least one output method is enabled +- Mock file download operations for testing conditional download logic + +### Integration Testing +- Test complete workflow: toggle setting → capture screenshot → verify file download behavior +- Test all four combinations of save to PC and clipboard settings +- Verify error handling when both options are disabled +- Test preference persistence across browser sessions + +### User Experience Testing +- Verify toggle state persistence across browser sessions +- Test visual feedback for different operation combinations +- Confirm tooltip and help text clarity +- Test warning messages when both toggles are disabled + +## Implementation Considerations + +### Performance Impact +- No performance impact when save to PC is enabled (existing behavior) +- Slight performance improvement when save to PC is disabled (no file creation) +- No impact on clipboard operations + +### Security Considerations +- File downloads use existing browser download mechanisms +- No new security concerns introduced +- Maintains existing security model for clipboard operations + +### Accessibility +- Save to PC toggle will include proper ARIA labels +- Keyboard navigation support for toggle control +- Screen reader compatible success/error messages +- Warning messages will be announced to screen readers + +### User Experience Design +- Logical grouping of output method toggles +- Clear visual hierarchy with save to PC above clipboard (primary → secondary) +- Consistent styling with existing UI elements +- Intuitive default settings (both enabled initially) + +## Browser Compatibility + +### File Download Support +- File downloads work in all modern browsers +- No fallback needed as this is core browser functionality +- Existing implementation already handles download edge cases + +### Storage and UI +- Uses existing chrome.storage.sync API (already implemented) +- Toggle UI uses existing CSS patterns +- No new browser API dependencies + +## Migration Strategy + +### Existing Users +- Default `saveToPc` to `true` when preference doesn't exist +- No change in behavior for existing users +- Seamless upgrade experience + +### New Users +- Both save to PC and clipboard enabled by default +- Provides full functionality out of the box +- Users can customize based on their workflow preferences \ No newline at end of file diff --git a/.kiro/specs/save-to-pc-toggle/requirements.md b/.kiro/specs/save-to-pc-toggle/requirements.md new file mode 100644 index 0000000..20b9a19 --- /dev/null +++ b/.kiro/specs/save-to-pc-toggle/requirements.md @@ -0,0 +1,51 @@ +# Requirements Document + +## Introduction + +This feature adds a save to PC toggle option to the Full Screenshot Selector extension, allowing users to choose whether captured screenshots should be automatically downloaded as files to their computer. This provides users with more flexibility in how they handle their screenshots, enabling them to use only the clipboard functionality without cluttering their downloads folder with files they may not need. + +## Requirements + +### Requirement 1 + +**User Story:** As a user of the screenshot extension, I want to toggle whether screenshots are saved to my PC, so that I can avoid downloading files when I only need to paste the image elsewhere. + +#### Acceptance Criteria + +1. WHEN the popup is opened THEN the system SHALL display a toggle control for "Save to PC" +2. WHEN the user toggles the save to PC option THEN the system SHALL save this preference to browser storage +3. WHEN the extension is reopened THEN the system SHALL restore the previously saved save to PC preference +4. WHEN a screenshot is captured AND save to PC is enabled THEN the system SHALL download the file as before +5. WHEN a screenshot is captured AND save to PC is disabled THEN the system SHALL NOT download any file + +### Requirement 2 + +**User Story:** As a user, I want clear visual feedback about the save to PC feature, so that I understand when files will be downloaded and when they won't. + +#### Acceptance Criteria + +1. WHEN the save to PC toggle is enabled THEN the system SHALL display visual indication of the enabled state +2. WHEN a screenshot is captured with save to PC enabled THEN the system SHALL show a success message indicating the download +3. WHEN a screenshot is captured with save to PC disabled THEN the system SHALL show a success message without mentioning download +4. WHEN hovering over the save to PC toggle THEN the system SHALL display helpful tooltip text + +### Requirement 3 + +**User Story:** As a user, I want the save to PC feature to work independently of the clipboard feature, so that I can choose any combination of saving and copying that suits my workflow. + +#### Acceptance Criteria + +1. WHEN both save to PC and copy to clipboard are enabled THEN the system SHALL perform both operations +2. WHEN only save to PC is enabled THEN the system SHALL only download the file +3. WHEN only copy to clipboard is enabled THEN the system SHALL only copy to clipboard +4. WHEN both features are disabled THEN the system SHALL show an error message and not capture the screenshot + +### Requirement 4 + +**User Story:** As a user, I want sensible default settings for the save to PC feature, so that the extension works intuitively without requiring configuration. + +#### Acceptance Criteria + +1. WHEN the extension is used for the first time THEN the save to PC option SHALL be enabled by default +2. WHEN upgrading from a previous version THEN the save to PC option SHALL be enabled by default to maintain existing behavior +3. WHEN both save to PC and clipboard options are available THEN users SHALL be able to configure them independently \ No newline at end of file diff --git a/.kiro/specs/save-to-pc-toggle/tasks.md b/.kiro/specs/save-to-pc-toggle/tasks.md new file mode 100644 index 0000000..ebf1a72 --- /dev/null +++ b/.kiro/specs/save-to-pc-toggle/tasks.md @@ -0,0 +1,71 @@ +# Implementation Plan + +- [ ] 1. Add save to PC toggle UI to popup + - Add HTML toggle control above the clipboard toggle in popup.html + - Style the toggle control to match existing UI design patterns + - Include proper ARIA labels and accessibility attributes for the toggle + - Position the save to PC toggle logically above clipboard toggle + - _Requirements: 1.1, 2.1, 2.4_ + +- [ ] 2. Implement save to PC preference storage in popup + - Extend existing storage logic in popup.js to handle saveToPc preference + - Set default value to true to maintain existing behavior for current users + - Load saved save to PC preference on popup initialization + - Save save to PC preference changes to chrome.storage.sync + - _Requirements: 1.2, 1.3, 4.1, 4.2_ + +- [ ] 3. Add validation logic for output method selection + - Create validation function to ensure at least one output method is enabled + - Display warning message in popup when both save to PC and clipboard are disabled + - Prevent screenshot capture when no output method is selected + - Update start button state based on validation results + - _Requirements: 3.4_ + +- [ ] 4. Update message passing to include save to PC setting + - Modify startSelector message in popup.js to include saveToPc property + - Update content script message handler to receive and store save to PC preference + - Ensure backward compatibility with existing message structure + - _Requirements: 1.1, 1.4_ + +- [ ] 5. Implement conditional file download in content script + - Modify captureElement method in ScreenshotSelector class to conditionally download files + - Wrap existing download logic in conditional check for saveToPc preference + - Ensure canvas generation still occurs regardless of download preference (needed for clipboard) + - Maintain existing download filename generation and format + - _Requirements: 1.4, 1.5, 3.1, 3.2_ + +- [ ] 6. Update user feedback messages for different operation combinations + - Modify success messages to reflect which operations were performed + - Create specific messages for save-only, clipboard-only, and both operations + - Update loading messages to reflect enabled operations + - Ensure error messages are appropriate for each operation combination + - _Requirements: 2.2, 2.3, 3.1, 3.2, 3.3_ + +- [ ] 7. Add tooltip and help text for save to PC toggle + - Implement tooltip showing save to PC feature explanation + - Add help text explaining the relationship between save to PC and clipboard features + - Ensure tooltip is accessible and keyboard navigable + - Include information about default behavior and workflow flexibility + - _Requirements: 2.4_ + +- [ ] 8. Implement warning system for disabled output methods + - Create visual warning when both save to PC and clipboard toggles are disabled + - Display clear guidance on enabling at least one output method + - Make warning accessible to screen readers + - Update warning state dynamically as toggles change + - _Requirements: 3.4_ + +- [ ] 9. Test all combinations of save to PC and clipboard settings + - Write test cases for save-only mode (saveToPc: true, clipboard: false) + - Write test cases for clipboard-only mode (saveToPc: false, clipboard: true) + - Write test cases for both enabled (saveToPc: true, clipboard: true) + - Write test cases for validation when both disabled (saveToPc: false, clipboard: false) + - Verify preference persistence across browser sessions for all combinations + - _Requirements: 3.1, 3.2, 3.3, 3.4, 4.3_ + +- [ ] 10. Test backward compatibility and migration + - Test behavior when saveToPc preference doesn't exist in storage (new installation) + - Test behavior when upgrading from version without saveToPc preference + - Verify that existing users maintain current behavior (files still download by default) + - Test that new users get both features enabled by default + - _Requirements: 4.1, 4.2_ \ No newline at end of file -- 2.49.1 From 5af01454ab8dab7421137885b947caf70cf9a7c1 Mon Sep 17 00:00:00 2001 From: KarthiDreamr Date: Wed, 13 Aug 2025 22:06:30 +0530 Subject: [PATCH 05/10] feat: added toggle for save to pc --- .kiro/specs/save-to-pc-toggle/tasks.md | 18 +-- content.js | 134 +++++++++++++------ popup.html | 56 +++++++- popup.js | 177 +++++++++++++++++++------ 4 files changed, 296 insertions(+), 89 deletions(-) diff --git a/.kiro/specs/save-to-pc-toggle/tasks.md b/.kiro/specs/save-to-pc-toggle/tasks.md index ebf1a72..f5d16d3 100644 --- a/.kiro/specs/save-to-pc-toggle/tasks.md +++ b/.kiro/specs/save-to-pc-toggle/tasks.md @@ -1,61 +1,61 @@ # Implementation Plan -- [ ] 1. Add save to PC toggle UI to popup +- [x] 1. Add save to PC toggle UI to popup - Add HTML toggle control above the clipboard toggle in popup.html - Style the toggle control to match existing UI design patterns - Include proper ARIA labels and accessibility attributes for the toggle - Position the save to PC toggle logically above clipboard toggle - _Requirements: 1.1, 2.1, 2.4_ -- [ ] 2. Implement save to PC preference storage in popup +- [x] 2. Implement save to PC preference storage in popup - Extend existing storage logic in popup.js to handle saveToPc preference - Set default value to true to maintain existing behavior for current users - Load saved save to PC preference on popup initialization - Save save to PC preference changes to chrome.storage.sync - _Requirements: 1.2, 1.3, 4.1, 4.2_ -- [ ] 3. Add validation logic for output method selection +- [x] 3. Add validation logic for output method selection - Create validation function to ensure at least one output method is enabled - Display warning message in popup when both save to PC and clipboard are disabled - Prevent screenshot capture when no output method is selected - Update start button state based on validation results - _Requirements: 3.4_ -- [ ] 4. Update message passing to include save to PC setting +- [x] 4. Update message passing to include save to PC setting - Modify startSelector message in popup.js to include saveToPc property - Update content script message handler to receive and store save to PC preference - Ensure backward compatibility with existing message structure - _Requirements: 1.1, 1.4_ -- [ ] 5. Implement conditional file download in content script +- [x] 5. Implement conditional file download in content script - Modify captureElement method in ScreenshotSelector class to conditionally download files - Wrap existing download logic in conditional check for saveToPc preference - Ensure canvas generation still occurs regardless of download preference (needed for clipboard) - Maintain existing download filename generation and format - _Requirements: 1.4, 1.5, 3.1, 3.2_ -- [ ] 6. Update user feedback messages for different operation combinations +- [x] 6. Update user feedback messages for different operation combinations - Modify success messages to reflect which operations were performed - Create specific messages for save-only, clipboard-only, and both operations - Update loading messages to reflect enabled operations - Ensure error messages are appropriate for each operation combination - _Requirements: 2.2, 2.3, 3.1, 3.2, 3.3_ -- [ ] 7. Add tooltip and help text for save to PC toggle +- [x] 7. Add tooltip and help text for save to PC toggle - Implement tooltip showing save to PC feature explanation - Add help text explaining the relationship between save to PC and clipboard features - Ensure tooltip is accessible and keyboard navigable - Include information about default behavior and workflow flexibility - _Requirements: 2.4_ -- [ ] 8. Implement warning system for disabled output methods +- [x] 8. Implement warning system for disabled output methods - Create visual warning when both save to PC and clipboard toggles are disabled - Display clear guidance on enabling at least one output method - Make warning accessible to screen readers - Update warning state dynamically as toggles change - _Requirements: 3.4_ -- [ ] 9. Test all combinations of save to PC and clipboard settings +- [x] 9. Test all combinations of save to PC and clipboard settings - Write test cases for save-only mode (saveToPc: true, clipboard: false) - Write test cases for clipboard-only mode (saveToPc: false, clipboard: true) - Write test cases for both enabled (saveToPc: true, clipboard: true) diff --git a/content.js b/content.js index 2b6e2cf..98b61c8 100644 --- a/content.js +++ b/content.js @@ -7,9 +7,10 @@ class ScreenshotSelector { this.style = null; this.background = 'black'; this.copyToClipboard = true; // Default to true + this.saveToPc = true; // Default to true for backward compatibility } - async init(background = 'black', copyToClipboard = true) { + async init(background = 'black', copyToClipboard = true, saveToPc = true) { if (this.isActive) { console.log('Screenshot selector already active'); return; @@ -17,6 +18,7 @@ class ScreenshotSelector { this.background = background; this.copyToClipboard = copyToClipboard; + this.saveToPc = saveToPc; this.isActive = true; this.createUI(); @@ -365,12 +367,25 @@ class ScreenshotSelector { async captureElement(element) { try { - // Determine loading message based on clipboard settings + // Determine loading message based on enabled operations let loadingMessage = 'Capturing screenshot...'; - if (this.copyToClipboard) { + + if (this.saveToPc && this.copyToClipboard) { + // Both operations enabled const availability = this.checkClipboardAPIAvailability(); if (availability.available) { loadingMessage = 'Capturing screenshot and preparing for clipboard...'; + } else { + loadingMessage = 'Capturing screenshot for download... (Clipboard not available)'; + } + } else if (this.saveToPc && !this.copyToClipboard) { + // Only save to PC enabled + loadingMessage = 'Capturing screenshot for download...'; + } else if (!this.saveToPc && this.copyToClipboard) { + // Only clipboard enabled + const availability = this.checkClipboardAPIAvailability(); + if (availability.available) { + loadingMessage = 'Capturing screenshot for clipboard...'; } else { loadingMessage = 'Capturing screenshot... (Clipboard not available)'; } @@ -480,11 +495,13 @@ class ScreenshotSelector { // Restore original styles Object.assign(element.style, originalStyles); - // Always download the image first (core functionality) - const link = document.createElement('a'); - link.href = canvas.toDataURL('image/png'); - link.download = `screenshot-${new Date().toISOString().slice(0, 19).replace(/[:.]/g, '-')}.png`; - link.click(); + // Conditionally download the image based on saveToPc preference + if (this.saveToPc) { + const link = 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 (after successful canvas generation and download) let clipboardResult = null; @@ -513,26 +530,46 @@ class ScreenshotSelector { } } - // Generate user feedback message based on clipboard results - let successMessage = 'Screenshot downloaded successfully!'; + // Generate user feedback message based on operation combinations + let successMessage = 'Screenshot captured!'; let messageColor = 'rgba(39, 174, 96, 0.95)'; // Green for success + let messageIcon = '✅'; - if (this.copyToClipboard) { + // Determine success message based on enabled operations + if (this.saveToPc && this.copyToClipboard) { + // Both operations enabled if (clipboardResult && clipboardResult.success) { - successMessage = 'Screenshot downloaded and copied to clipboard!'; - // Keep green color for full success + successMessage = 'Screenshot saved and copied to clipboard!'; + messageIcon = '✅'; } else { - // Download succeeded but clipboard failed - provide specific error feedback + // Save succeeded but clipboard failed const errorMessage = this.getClipboardErrorMessage(clipboardResult); - successMessage = `Screenshot downloaded successfully! ${errorMessage}`; + successMessage = `Screenshot saved successfully! However, ${errorMessage.toLowerCase()}`; messageColor = 'rgba(243, 156, 18, 0.95)'; // Orange for partial success + 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 = this.getClipboardErrorMessage(clipboardResult); + successMessage = `Screenshot capture failed: ${errorMessage}`; + messageColor = 'rgba(231, 76, 60, 0.95)'; // Red for failure + messageIcon = '❌'; } } this.overlay.innerHTML = `
- + ${messageIcon} ${successMessage}
@@ -546,21 +583,35 @@ class ScreenshotSelector { } catch (error) { console.error('Screenshot failed:', error); - // Provide specific error messages for screenshot failures + // Provide specific error messages for screenshot failures based on enabled operations let errorMessage = 'Screenshot capture failed. Please try again.'; + let operationContext = ''; + + // Add context about what operations were attempted + if (this.saveToPc && this.copyToClipboard) { + operationContext = ' Neither file download nor clipboard copy could be completed.'; + } else if (this.saveToPc && !this.copyToClipboard) { + operationContext = ' File download could not be completed.'; + } else if (!this.saveToPc && this.copyToClipboard) { + operationContext = ' Clipboard copy could not be completed.'; + } if (error.message) { if (error.message.includes('html2canvas')) { - errorMessage = 'Screenshot rendering failed. Try selecting a different element or refresh the page.'; + 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.'; + errorMessage = `Screenshot capture timed out. Try selecting a smaller area or simpler element.${operationContext}`; } else if (error.message.includes('network') || error.message.includes('CORS')) { - errorMessage = 'Screenshot failed: Some content is blocked by security restrictions.'; + errorMessage = `Screenshot failed: Some content is blocked by security restrictions.${operationContext}`; } else if (error.message.includes('memory') || error.message.includes('quota')) { - errorMessage = 'Screenshot failed: Not enough memory. Try capturing a smaller area.'; + errorMessage = `Screenshot failed: Not enough memory. Try capturing a smaller area.${operationContext}`; } else if (error.message.includes('canvas')) { - errorMessage = 'Screenshot failed: Unable to create image. Try a different element.'; + errorMessage = `Screenshot failed: Unable to create image. Try a different element.${operationContext}`; + } else { + errorMessage = `Screenshot capture failed: ${error.message}${operationContext}`; } + } else { + errorMessage = `Screenshot capture failed. Please try again.${operationContext}`; } this.overlay.innerHTML = ` @@ -661,61 +712,62 @@ class ScreenshotSelector { */ getClipboardErrorMessage(clipboardResult) { if (!clipboardResult || !clipboardResult.error) { - return 'Clipboard copy failed due to unknown error.'; + return 'clipboard copy failed due to unknown error'; } const errorType = clipboardResult.errorType; const errorMessage = clipboardResult.error; // Provide contextual error messages based on error type + // Note: These messages are designed to work in the context of "Screenshot saved successfully! However, [message]" switch (errorType) { case 'permission_denied': - return 'Clipboard copy failed: Permission denied. Please allow clipboard access in your browser settings.'; + 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.'; + return 'clipboard copy is not available in this browser'; case 'security_error': - return 'Clipboard copy blocked by browser security. Try using HTTPS or check site permissions.'; + return 'clipboard copy was 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.'; + 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.'; + return 'clipboard copy timed out. The image may be too large or complex'; case 'network_error': - return 'Clipboard copy failed due to network error. Please try again.'; + return 'clipboard copy failed due to network error. Please try again'; case 'canvas_conversion': - return 'Clipboard copy failed: Unable to prepare image. Try capturing a different element.'; + return 'clipboard copy failed: unable to prepare image. Try capturing a different element'; case 'clipboard_item_creation': - return 'Clipboard copy not supported: Your browser doesn\'t support image clipboard operations.'; + return 'clipboard copy is not supported: your browser doesn\'t support image clipboard operations'; case 'invalid_state': - return 'Clipboard copy failed: Browser clipboard is busy. Please try again.'; + return 'clipboard copy failed: browser clipboard is busy. Please try again'; case 'data_error': - return 'Clipboard copy failed: Invalid image data. Please try capturing again.'; + return 'clipboard copy failed: invalid image data. Please try capturing again'; case 'unexpected': - return 'Clipboard copy failed due to unexpected error. Screenshot was still downloaded.'; + return 'clipboard copy failed due to unexpected error'; default: // For unknown error types, try to extract meaningful info from the error message if (errorMessage.includes('permission') || errorMessage.includes('denied')) { - return 'Clipboard copy failed: Access denied. Check browser permissions.'; + return 'clipboard copy failed: access denied. Check browser permissions'; } else if (errorMessage.includes('not supported') || errorMessage.includes('unavailable')) { - return 'Clipboard copy not supported in this browser.'; + return 'clipboard copy is not supported in this browser'; } else if (errorMessage.includes('timeout') || errorMessage.includes('timed out')) { - return 'Clipboard copy timed out. Try capturing a smaller area.'; + return 'clipboard copy timed out. Try capturing a smaller area'; } else if (errorMessage.includes('large') || errorMessage.includes('size')) { - return 'Clipboard copy failed: Image too large for clipboard.'; + return 'clipboard copy failed: image too large for clipboard'; } else { - return `Clipboard copy failed: ${errorMessage}`; + return `clipboard copy failed: ${errorMessage.toLowerCase()}`; } } } @@ -893,7 +945,9 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { case 'startSelector': // Extract clipboard preference from message, default to true for backward compatibility const copyToClipboard = message.copyToClipboard !== undefined ? message.copyToClipboard : true; - screenshotSelector.init(message.background, copyToClipboard); + // Extract save to PC preference from message, default to true for backward compatibility + const saveToPc = message.saveToPc !== undefined ? message.saveToPc : true; + screenshotSelector.init(message.background, copyToClipboard, saveToPc); sendResponse({ success: true }); break; diff --git a/popup.html b/popup.html index 89d5448..88317d4 100644 --- a/popup.html +++ b/popup.html @@ -241,6 +241,23 @@ background: #f8d7da; color: #721c24; } + + .status.warning { + background: #fff3cd; + color: #856404; + border: 1px solid #ffeaa7; + } + + .btn:disabled { + background: #6c757d; + color: #fff; + cursor: not-allowed; + opacity: 0.6; + } + + .btn:disabled:hover { + background: #6c757d; + } @@ -258,6 +275,39 @@ +
+
+ +
+ + +
+
+
+ Automatically download screenshots as PNG files to your computer. Hover over the "?" for more details. +
+
+
@@ -291,6 +341,10 @@ + + diff --git a/popup.js b/popup.js index edebae4..5c6a712 100644 --- a/popup.js +++ b/popup.js @@ -2,18 +2,43 @@ document.addEventListener('DOMContentLoaded', async () => { const backgroundSelect = document.getElementById('background-select'); const clipboardToggle = document.getElementById('clipboard-toggle'); + const saveToPcToggle = document.getElementById('save-to-pc-toggle'); const startBtn = document.getElementById('start-selector'); const stopBtn = document.getElementById('stop-selector'); const status = document.getElementById('status'); + const validationWarning = document.getElementById('validation-warning'); // Load saved preferences - const saved = await chrome.storage.sync.get(['backgroundPreference', 'copyToClipboard']); + const saved = await chrome.storage.sync.get(['backgroundPreference', 'copyToClipboard', 'saveToPc']); if (saved.backgroundPreference) { backgroundSelect.value = saved.backgroundPreference; } // Set clipboard preference (default to true if not set) clipboardToggle.checked = saved.copyToClipboard !== undefined ? saved.copyToClipboard : true; + + // Set save to PC preference (default to true if not set to maintain existing behavior) + saveToPcToggle.checked = saved.saveToPc !== undefined ? saved.saveToPc : true; + + // Validation function to ensure at least one output method is enabled + function validateOutputMethods() { + 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; + } + + // Initial validation check + validateOutputMethods(); // Save background preference when changed backgroundSelect.addEventListener('change', () => { @@ -23,10 +48,23 @@ document.addEventListener('DOMContentLoaded', async () => { // Save clipboard preference when changed clipboardToggle.addEventListener('change', () => { chrome.storage.sync.set({ copyToClipboard: clipboardToggle.checked }); + validateOutputMethods(); + }); + + // Save save to PC preference when changed + saveToPcToggle.addEventListener('change', () => { + chrome.storage.sync.set({ saveToPc: saveToPcToggle.checked }); + validateOutputMethods(); }); // Start selector startBtn.addEventListener('click', async () => { + // Validate output methods before proceeding + if (!validateOutputMethods()) { + showStatus('Error: Please enable at least one output method (Save to PC or Copy to Clipboard) to capture screenshots.', 'error'); + return; + } + const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); // Check if page is compatible @@ -44,10 +82,22 @@ document.addEventListener('DOMContentLoaded', async () => { await chrome.tabs.sendMessage(tab.id, { action: 'startSelector', background: backgroundSelect.value, - copyToClipboard: clipboardToggle.checked + copyToClipboard: clipboardToggle.checked, + saveToPc: saveToPcToggle.checked }); - showStatus('Selector activated! Hover over elements and click to capture.', 'success'); + // Generate activation message based on enabled operations + let activationMessage = 'Selector activated! Hover over elements and click to capture.'; + + if (saveToPcToggle.checked && clipboardToggle.checked) { + activationMessage = 'Selector activated! Screenshots will be saved and copied to clipboard.'; + } else if (saveToPcToggle.checked && !clipboardToggle.checked) { + activationMessage = 'Selector activated! Screenshots will be saved to downloads folder.'; + } else if (!saveToPcToggle.checked && clipboardToggle.checked) { + activationMessage = 'Selector activated! Screenshots will be copied to clipboard only.'; + } + + showStatus(activationMessage, 'success'); toggleButtons(true); // Auto-close popup after starting @@ -55,11 +105,27 @@ document.addEventListener('DOMContentLoaded', async () => { } catch (error) { console.error('Full error details:', error); + + let errorMessage = 'Error: Could not activate on this page.'; + if (error.message.includes('Could not establish connection')) { - showStatus('Error: Content script not loaded. Try refreshing the page.', 'error'); - } else { - showStatus('Error: Could not activate on this page.', 'error'); + errorMessage = 'Error: Content script not loaded. Try refreshing the page and try again.'; + } else if (error.message.includes('permission')) { + errorMessage = 'Error: Permission denied. Check if the page allows extensions.'; + } else if (error.message.includes('protocol')) { + errorMessage = 'Error: Cannot capture screenshots on this page type (chrome://, file://, etc.).'; } + + // Add context about what operations were attempted + if (saveToPcToggle.checked && clipboardToggle.checked) { + errorMessage += ' Neither file download nor clipboard copy could be set up.'; + } else if (saveToPcToggle.checked && !clipboardToggle.checked) { + errorMessage += ' File download could not be set up.'; + } else if (!saveToPcToggle.checked && clipboardToggle.checked) { + errorMessage += ' Clipboard copy could not be set up.'; + } + + showStatus(errorMessage, 'error'); } }); @@ -126,43 +192,47 @@ document.addEventListener('DOMContentLoaded', async () => { } function setupTooltipAccessibility() { - const tooltipIcon = document.querySelector('.tooltip-icon'); - const tooltipContent = document.querySelector('.tooltip-content'); + const tooltips = document.querySelectorAll('.tooltip'); - if (!tooltipIcon || !tooltipContent) return; + tooltips.forEach(tooltip => { + const tooltipIcon = tooltip.querySelector('.tooltip-icon'); + const tooltipContent = tooltip.querySelector('.tooltip-content'); + + if (!tooltipIcon || !tooltipContent) return; - // Handle keyboard events for tooltip - tooltipIcon.addEventListener('keydown', (e) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - // Toggle tooltip visibility on Enter/Space - const isVisible = tooltipContent.style.visibility === 'visible'; - tooltipContent.style.visibility = isVisible ? 'hidden' : 'visible'; - tooltipContent.style.opacity = isVisible ? '0' : '1'; - } else if (e.key === 'Escape') { - // Hide tooltip on Escape - tooltipContent.style.visibility = 'hidden'; - tooltipContent.style.opacity = '0'; - } - }); - - // Hide tooltip when clicking outside - document.addEventListener('click', (e) => { - if (!tooltipIcon.contains(e.target)) { - tooltipContent.style.visibility = 'hidden'; - tooltipContent.style.opacity = '0'; - } - }); - - // Handle focus loss - tooltipIcon.addEventListener('blur', (e) => { - // Small delay to allow for focus to move to tooltip content if needed - setTimeout(() => { - if (!tooltipIcon.matches(':focus-within')) { + // Handle keyboard events for tooltip + tooltipIcon.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + // Toggle tooltip visibility on Enter/Space + const isVisible = tooltipContent.style.visibility === 'visible'; + tooltipContent.style.visibility = isVisible ? 'hidden' : 'visible'; + tooltipContent.style.opacity = isVisible ? '0' : '1'; + } else if (e.key === 'Escape') { + // Hide tooltip on Escape tooltipContent.style.visibility = 'hidden'; tooltipContent.style.opacity = '0'; } - }, 100); + }); + + // Hide tooltip when clicking outside + document.addEventListener('click', (e) => { + if (!tooltip.contains(e.target)) { + tooltipContent.style.visibility = 'hidden'; + tooltipContent.style.opacity = '0'; + } + }); + + // Handle focus loss + tooltipIcon.addEventListener('blur', (e) => { + // Small delay to allow for focus to move to tooltip content if needed + setTimeout(() => { + if (!tooltipIcon.matches(':focus-within')) { + tooltipContent.style.visibility = 'hidden'; + tooltipContent.style.opacity = '0'; + } + }, 100); + }); }); } }); @@ -173,15 +243,44 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { const startBtn = document.getElementById('start-selector'); const stopBtn = document.getElementById('stop-selector'); const status = document.getElementById('status'); + const saveToPcToggle = document.getElementById('save-to-pc-toggle'); + const clipboardToggle = document.getElementById('clipboard-toggle'); startBtn.style.display = 'block'; stopBtn.style.display = 'none'; if (message.reason === 'screenshot-taken') { - status.textContent = 'Screenshot captured!'; + // Generate success message based on current toggle states + let successMessage = 'Screenshot captured!'; + + if (saveToPcToggle.checked && clipboardToggle.checked) { + successMessage = 'Screenshot saved and copied to clipboard!'; + } else if (saveToPcToggle.checked && !clipboardToggle.checked) { + successMessage = 'Screenshot saved to downloads folder!'; + } else if (!saveToPcToggle.checked && clipboardToggle.checked) { + successMessage = 'Screenshot copied to clipboard!'; + } + + status.textContent = successMessage; status.className = 'status success'; status.style.display = 'block'; setTimeout(() => status.style.display = 'none', 2000); + } else if (message.reason === 'screenshot-failed') { + // Handle screenshot failure + let failureMessage = 'Screenshot capture failed. Please try again.'; + + if (saveToPcToggle.checked && clipboardToggle.checked) { + failureMessage = 'Screenshot capture failed. Neither file download nor clipboard copy could be completed.'; + } else if (saveToPcToggle.checked && !clipboardToggle.checked) { + failureMessage = 'Screenshot capture failed. File download could not be completed.'; + } else if (!saveToPcToggle.checked && clipboardToggle.checked) { + failureMessage = 'Screenshot capture failed. Clipboard copy could not be completed.'; + } + + status.textContent = failureMessage; + status.className = 'status error'; + status.style.display = 'block'; + setTimeout(() => status.style.display = 'none', 3000); } } }); \ No newline at end of file -- 2.49.1 From fdac507802108d1951120a6f1814ab1811f4e666 Mon Sep 17 00:00:00 2001 From: KarthiDreamr Date: Wed, 13 Aug 2025 22:06:39 +0530 Subject: [PATCH 06/10] feat: added tests --- tests/integration-scenarios.test.js | 88 ++- .../save-to-pc-clipboard-combinations.test.js | 637 ++++++++++++++++++ tests/validation-logic.test.js | 209 ++++++ 3 files changed, 908 insertions(+), 26 deletions(-) create mode 100644 tests/save-to-pc-clipboard-combinations.test.js create mode 100644 tests/validation-logic.test.js diff --git a/tests/integration-scenarios.test.js b/tests/integration-scenarios.test.js index 2221bf7..e32a8c1 100644 --- a/tests/integration-scenarios.test.js +++ b/tests/integration-scenarios.test.js @@ -64,6 +64,18 @@ describe('Integration Scenarios - Clipboard with Different Backgrounds', () => { beforeEach(() => { jest.clearAllMocks(); + // Reset clipboard mock for each test + if (!global.navigator) { + global.navigator = {}; + } + global.navigator.clipboard = { + write: jest.fn().mockResolvedValue() + }; + global.window.ClipboardItem = jest.fn().mockImplementation((data) => { + return { data, type: 'image/png' }; + }); + global.window.isSecureContext = true; + // Mock element to capture mockElement = { classList: { @@ -92,13 +104,7 @@ describe('Integration Scenarios - Clipboard with Different Backgrounds', () => { global.html2canvas = jest.fn().mockResolvedValue(mockCanvas); - // Mock full clipboard API support - global.navigator = { - clipboard: { - write: jest.fn().mockResolvedValue() - } - }; - global.window.ClipboardItem = jest.fn(); + // Import ScreenshotSelector (simplified for testing) const ScreenshotSelector = class { @@ -106,12 +112,14 @@ describe('Integration Scenarios - Clipboard with Different Backgrounds', () => { this.isActive = false; this.background = 'black'; this.copyToClipboard = true; + this.saveToPc = true; this.overlay = null; } - async init(background = 'black', copyToClipboard = true) { + async init(background = 'black', copyToClipboard = true, saveToPc = true) { this.background = background; this.copyToClipboard = copyToClipboard; + this.saveToPc = saveToPc; this.isActive = true; this.createUI(); } @@ -194,12 +202,36 @@ describe('Integration Scenarios - Clipboard with Different Backgrounds', () => { clipboardResult = await this.copyCanvasToClipboard(canvas); } - // Update UI based on results - let successMessage = 'Screenshot downloaded successfully!'; - if (this.copyToClipboard && clipboardResult && clipboardResult.success) { - successMessage = 'Screenshot downloaded and copied to clipboard!'; - } else if (this.copyToClipboard && clipboardResult && !clipboardResult.success) { - successMessage = `Screenshot downloaded successfully! Clipboard copy failed: ${clipboardResult.error}`; + // Update UI based on results (matching new message format) + let successMessage = 'Screenshot captured!'; + let messageIcon = '✅'; + + if (this.saveToPc && this.copyToClipboard) { + // Both operations enabled + if (clipboardResult && clipboardResult.success) { + successMessage = 'Screenshot saved and copied to clipboard!'; + messageIcon = '✅'; + } else { + // Save succeeded but clipboard failed + const errorMessage = clipboardResult ? clipboardResult.error.toLowerCase() : 'clipboard copy failed'; + successMessage = `Screenshot saved successfully! However, ${errorMessage}`; + messageIcon = '⚠️'; + } + } else if (this.saveToPc && !this.copyToClipboard) { + // Only save to PC enabled + successMessage = 'Screenshot saved to downloads folder!'; + messageIcon = '💾'; + } else if (!this.saveToPc && this.copyToClipboard) { + // Only clipboard enabled + if (clipboardResult && clipboardResult.success) { + successMessage = 'Screenshot copied to clipboard!'; + messageIcon = '📋'; + } else { + // Clipboard failed - this is a complete failure since no other output method is enabled + const errorMessage = clipboardResult ? clipboardResult.error : 'clipboard copy failed'; + successMessage = `Screenshot capture failed: ${errorMessage}`; + messageIcon = '❌'; + } } this.overlay.innerHTML = `
${successMessage}
`; @@ -238,7 +270,7 @@ describe('Integration Scenarios - Clipboard with Different Backgrounds', () => { expect(result.success).toBe(true); expect(result.downloadSuccess).toBe(true); expect(result.clipboardResult.success).toBe(true); - expect(result.message).toBe('Screenshot downloaded and copied to clipboard!'); + expect(result.message).toBe('Screenshot saved and copied to clipboard!'); }); test('should capture with black background and copy to clipboard', async () => { @@ -254,7 +286,7 @@ describe('Integration Scenarios - Clipboard with Different Backgrounds', () => { expect(result.success).toBe(true); expect(result.clipboardResult.success).toBe(true); - expect(result.message).toBe('Screenshot downloaded and copied to clipboard!'); + expect(result.message).toBe('Screenshot saved and copied to clipboard!'); }); test('should capture with white background and copy to clipboard', async () => { @@ -270,24 +302,28 @@ describe('Integration Scenarios - Clipboard with Different Backgrounds', () => { expect(result.success).toBe(true); expect(result.clipboardResult.success).toBe(true); - expect(result.message).toBe('Screenshot downloaded and copied to clipboard!'); + expect(result.message).toBe('Screenshot saved and copied to clipboard!'); }); test('should capture without clipboard when disabled', async () => { - await screenshotSelector.init('black', false); + await screenshotSelector.init('black', false, true); // saveToPc = true, copyToClipboard = false const result = await screenshotSelector.captureElement(mockElement); expect(result.success).toBe(true); expect(result.downloadSuccess).toBe(true); expect(result.clipboardResult).toBe(null); // No clipboard operation attempted - expect(result.message).toBe('Screenshot downloaded successfully!'); - expect(global.navigator.clipboard.write).not.toHaveBeenCalled(); + expect(result.message).toBe('Screenshot saved to downloads folder!'); + if (global.navigator.clipboard?.write) { + expect(global.navigator.clipboard.write).not.toHaveBeenCalled(); + } }); test('should handle clipboard failure gracefully while maintaining download', async () => { - // Mock clipboard failure - global.navigator.clipboard.write.mockRejectedValue(new DOMException('Permission denied', 'NotAllowedError')); + // Reset and mock clipboard failure + global.navigator.clipboard = { + write: jest.fn().mockRejectedValue(new DOMException('Permission denied', 'NotAllowedError')) + }; await screenshotSelector.init('black', true); @@ -297,8 +333,8 @@ describe('Integration Scenarios - Clipboard with Different Backgrounds', () => { expect(result.downloadSuccess).toBe(true); expect(result.clipboardResult.success).toBe(false); expect(result.clipboardResult.errorType).toBe('permission_denied'); - expect(result.message).toContain('Screenshot downloaded successfully!'); - expect(result.message).toContain('Clipboard copy failed'); + expect(result.message).toContain('Screenshot saved successfully!'); + expect(result.message).toContain('permission denied'); }); test('should handle clipboard API unavailable scenario', async () => { @@ -313,8 +349,8 @@ describe('Integration Scenarios - Clipboard with Different Backgrounds', () => { expect(result.downloadSuccess).toBe(true); expect(result.clipboardResult.success).toBe(false); expect(result.clipboardResult.errorType).toBe('api_unavailable'); - expect(result.message).toContain('Screenshot downloaded successfully!'); - expect(result.message).toContain('Clipboard copy failed'); + expect(result.message).toContain('Screenshot saved successfully!'); + expect(result.message).toContain('clipboard'); }); }); diff --git a/tests/save-to-pc-clipboard-combinations.test.js b/tests/save-to-pc-clipboard-combinations.test.js new file mode 100644 index 0000000..7791096 --- /dev/null +++ b/tests/save-to-pc-clipboard-combinations.test.js @@ -0,0 +1,637 @@ +/** + * Comprehensive test suite for all combinations of save to PC and clipboard settings + * Tests all four combinations: both enabled, save-only, clipboard-only, both disabled + * Includes preference persistence testing across browser sessions + */ + +// Mock Chrome APIs +global.chrome = { + storage: { + sync: { + get: jest.fn(), + set: jest.fn() + } + }, + runtime: { + sendMessage: jest.fn(), + onMessage: { + addListener: jest.fn() + } + }, + tabs: { + query: jest.fn(), + sendMessage: jest.fn() + } +}; + +// Mock DOM APIs +const mockCreateElement = jest.fn(); +const mockLink = { + href: '', + download: '', + click: jest.fn(), + style: {}, + classList: { add: jest.fn(), remove: jest.fn() } +}; + +global.document = { + createElement: mockCreateElement, + head: { appendChild: jest.fn() }, + body: { appendChild: jest.fn() }, + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + querySelectorAll: jest.fn(() => []), + getElementById: jest.fn() +}; + +global.window = { + close: jest.fn(), + setTimeout: jest.fn((fn) => fn()), + getComputedStyle: jest.fn(() => ({})), + devicePixelRatio: 2, + isSecureContext: true, + CSS: { escape: jest.fn(str => str) } +}; + +// Mock html2canvas +global.html2canvas = jest.fn(); + +// Mock ScreenshotSelector class for testing +class MockScreenshotSelector { + constructor() { + this.copyToClipboard = true; + this.saveToPc = true; + this.background = 'black'; + this.isActive = false; + } + + async init(background = 'black', copyToClipboard = true, saveToPc = true) { + this.background = background; + this.copyToClipboard = copyToClipboard; + this.saveToPc = saveToPc; + this.isActive = true; + } + + checkClipboardAPIAvailability() { + if (!global.navigator.clipboard) { + return { available: false, reason: 'Clipboard API not available in this browser' }; + } + if (!global.window.ClipboardItem) { + return { available: false, reason: 'ClipboardItem not supported in this browser' }; + } + if (!global.window.isSecureContext) { + return { available: false, reason: 'Clipboard API requires a secure context (HTTPS)' }; + } + if (!global.navigator.clipboard.write) { + return { available: false, reason: 'Clipboard write functionality not available' }; + } + return { available: true }; + } + + async copyCanvasToClipboard(canvas) { + try { + const availability = this.checkClipboardAPIAvailability(); + if (!availability.available) { + return { success: false, error: availability.reason, errorType: 'api_unavailable' }; + } + + const blob = await new Promise((resolve, reject) => { + canvas.toBlob((blob) => { + if (blob) resolve(blob); + else reject(new Error('Canvas to blob conversion returned null')); + }, 'image/png'); + }); + + const clipboardItem = new global.window.ClipboardItem({ 'image/png': blob }); + await global.navigator.clipboard.write([clipboardItem]); + + return { success: true }; + } catch (error) { + if (error.name === 'NotAllowedError') { + return { success: false, error: 'Permission denied', errorType: 'permission_denied' }; + } else if (error.name === 'SecurityError') { + return { success: false, error: 'Security error', errorType: 'security_error' }; + } + return { success: false, error: error.message, errorType: 'unexpected' }; + } + } + + async captureElement(element) { + // Mock canvas creation + const mockCanvas = { + toBlob: jest.fn((callback) => { + const mockBlob = new Blob(['mock data'], { type: 'image/png' }); + callback(mockBlob); + }), + toDataURL: jest.fn(() => 'data:image/png;base64,mockdata') + }; + + // Mock html2canvas + global.html2canvas.mockResolvedValue(mockCanvas); + + const canvas = await global.html2canvas(element); + + // Conditionally download based on saveToPc setting + if (this.saveToPc) { + const link = global.document.createElement('a'); + link.href = canvas.toDataURL('image/png'); + link.download = `screenshot-${new Date().toISOString().slice(0, 19).replace(/[:.]/g, '-')}.png`; + link.click(); + } + + // Handle clipboard operations if enabled + let clipboardResult = null; + if (this.copyToClipboard) { + clipboardResult = await this.copyCanvasToClipboard(canvas); + } + + return { + success: true, + downloadPerformed: this.saveToPc, + clipboardResult: clipboardResult + }; + } +} + +describe('Save to PC and Clipboard Combinations', () => { + let screenshotSelector; + let mockElement; + + beforeEach(() => { + jest.clearAllMocks(); + + // Reset mocks + mockCreateElement.mockClear(); + mockCreateElement.mockReturnValue(mockLink); + mockLink.click.mockClear(); + + // Setup clipboard API mocks + global.navigator = { + clipboard: { + write: jest.fn().mockResolvedValue() + } + }; + global.window.ClipboardItem = jest.fn(); + global.window.isSecureContext = true; + + screenshotSelector = new MockScreenshotSelector(); + mockElement = { + classList: { remove: jest.fn() }, + style: {}, + getBoundingClientRect: () => ({ left: 0, top: 0, width: 100, height: 100 }), + scrollHeight: 100, + clientHeight: 100, + scrollWidth: 100, + clientWidth: 100 + }; + }); + + describe('Combination 1: Both Save to PC and Clipboard Enabled (saveToPc: true, clipboard: true)', () => { + beforeEach(() => { + screenshotSelector.saveToPc = true; + screenshotSelector.copyToClipboard = true; + }); + + test('should perform both download and clipboard operations when both are enabled', async () => { + const result = await screenshotSelector.captureElement(mockElement); + + expect(result.success).toBe(true); + expect(result.downloadPerformed).toBe(true); + expect(result.clipboardResult).toBeDefined(); + expect(result.clipboardResult.success).toBe(true); + + // Verify download was triggered + expect(mockCreateElement).toHaveBeenCalledWith('a'); + expect(mockLink.click).toHaveBeenCalled(); + + // Verify clipboard operation was attempted + expect(global.navigator.clipboard.write).toHaveBeenCalled(); + }); + + test('should succeed with download even if clipboard fails when both are enabled', async () => { + // Mock clipboard failure + if (global.navigator.clipboard && global.navigator.clipboard.write) { + global.navigator.clipboard.write.mockRejectedValue(new DOMException('Permission denied', 'NotAllowedError')); + } + + const result = await screenshotSelector.captureElement(mockElement); + + expect(result.success).toBe(true); + expect(result.downloadPerformed).toBe(true); + expect(result.clipboardResult.success).toBe(false); + expect(result.clipboardResult.errorType).toBe('permission_denied'); + + // Download should still work + expect(mockCreateElement).toHaveBeenCalledWith('a'); + expect(mockLink.click).toHaveBeenCalled(); + }); + + test('should initialize with both settings correctly', async () => { + await screenshotSelector.init('white', true, true); + + expect(screenshotSelector.background).toBe('white'); + expect(screenshotSelector.copyToClipboard).toBe(true); + expect(screenshotSelector.saveToPc).toBe(true); + expect(screenshotSelector.isActive).toBe(true); + }); + }); + + describe('Combination 2: Save-only Mode (saveToPc: true, clipboard: false)', () => { + beforeEach(() => { + screenshotSelector.saveToPc = true; + screenshotSelector.copyToClipboard = false; + }); + + test('should only perform download when save-only mode is enabled', async () => { + const result = await screenshotSelector.captureElement(mockElement); + + expect(result.success).toBe(true); + expect(result.downloadPerformed).toBe(true); + expect(result.clipboardResult).toBeNull(); + + // Verify download was triggered + expect(mockCreateElement).toHaveBeenCalledWith('a'); + expect(mockLink.click).toHaveBeenCalled(); + + // Verify clipboard operation was NOT attempted + expect(global.navigator.clipboard.write).not.toHaveBeenCalled(); + }); + + test('should initialize with save-only settings correctly', async () => { + await screenshotSelector.init('transparent', false, true); + + expect(screenshotSelector.background).toBe('transparent'); + expect(screenshotSelector.copyToClipboard).toBe(false); + expect(screenshotSelector.saveToPc).toBe(true); + expect(screenshotSelector.isActive).toBe(true); + }); + }); + + describe('Combination 3: Clipboard-only Mode (saveToPc: false, clipboard: true)', () => { + beforeEach(() => { + screenshotSelector.saveToPc = false; + screenshotSelector.copyToClipboard = true; + }); + + test('should only perform clipboard operation when clipboard-only mode is enabled', async () => { + const result = await screenshotSelector.captureElement(mockElement); + + expect(result.success).toBe(true); + expect(result.downloadPerformed).toBe(false); + expect(result.clipboardResult).toBeDefined(); + expect(result.clipboardResult.success).toBe(true); + + // Verify download was NOT triggered + expect(mockCreateElement).not.toHaveBeenCalledWith('a'); + + // Verify clipboard operation was attempted + expect(global.navigator.clipboard.write).toHaveBeenCalled(); + }); + + test('should initialize with clipboard-only settings correctly', async () => { + await screenshotSelector.init('black', true, false); + + expect(screenshotSelector.background).toBe('black'); + expect(screenshotSelector.copyToClipboard).toBe(true); + expect(screenshotSelector.saveToPc).toBe(false); + expect(screenshotSelector.isActive).toBe(true); + }); + }); + + describe('Combination 4: Both Disabled (saveToPc: false, clipboard: false)', () => { + beforeEach(() => { + screenshotSelector.saveToPc = false; + screenshotSelector.copyToClipboard = false; + }); + + test('should perform neither operation when both are disabled', async () => { + const result = await screenshotSelector.captureElement(mockElement); + + expect(result.success).toBe(true); + expect(result.downloadPerformed).toBe(false); + expect(result.clipboardResult).toBeNull(); + + // Verify download was NOT triggered + expect(mockCreateElement).not.toHaveBeenCalledWith('a'); + + // Verify clipboard operation was NOT attempted + expect(global.navigator.clipboard.write).not.toHaveBeenCalled(); + }); + + test('should initialize with both disabled settings correctly', async () => { + await screenshotSelector.init('white', false, false); + + expect(screenshotSelector.background).toBe('white'); + expect(screenshotSelector.copyToClipboard).toBe(false); + expect(screenshotSelector.saveToPc).toBe(false); + expect(screenshotSelector.isActive).toBe(true); + }); + + test('should still create canvas even when both outputs are disabled', async () => { + await screenshotSelector.captureElement(mockElement); + + // html2canvas should still be called (needed for potential future operations) + expect(global.html2canvas).toHaveBeenCalledWith(mockElement); + }); + }); +}); + +describe('Popup Validation Logic for All Combinations', () => { + let clipboardToggle; + let saveToPcToggle; + let startBtn; + let validationWarning; + let validateOutputMethods; + + beforeEach(() => { + jest.clearAllMocks(); + + // Mock DOM elements + clipboardToggle = { + checked: true, + addEventListener: jest.fn() + }; + + saveToPcToggle = { + checked: true, + addEventListener: jest.fn() + }; + + startBtn = { + disabled: false, + setAttribute: jest.fn(), + addEventListener: jest.fn() + }; + + validationWarning = { + style: { display: 'none' } + }; + + global.document.getElementById = jest.fn((id) => { + switch (id) { + case 'clipboard-toggle': return clipboardToggle; + case 'save-to-pc-toggle': return saveToPcToggle; + case 'start-selector': return startBtn; + case 'validation-warning': return validationWarning; + default: return null; + } + }); + + // Define validation function + validateOutputMethods = function() { + const hasValidOutput = clipboardToggle.checked || saveToPcToggle.checked; + + if (hasValidOutput) { + validationWarning.style.display = 'none'; + startBtn.disabled = false; + startBtn.setAttribute('aria-describedby', ''); + } else { + validationWarning.style.display = 'block'; + startBtn.disabled = true; + startBtn.setAttribute('aria-describedby', 'validation-warning'); + } + + return hasValidOutput; + }; + }); + + test('should validate successfully when both save to PC and clipboard are enabled', () => { + clipboardToggle.checked = true; + saveToPcToggle.checked = true; + + const result = validateOutputMethods(); + + expect(result).toBe(true); + expect(validationWarning.style.display).toBe('none'); + expect(startBtn.disabled).toBe(false); + expect(startBtn.setAttribute).toHaveBeenCalledWith('aria-describedby', ''); + }); + + test('should validate successfully when only save to PC is enabled', () => { + clipboardToggle.checked = false; + saveToPcToggle.checked = true; + + const result = validateOutputMethods(); + + expect(result).toBe(true); + expect(validationWarning.style.display).toBe('none'); + expect(startBtn.disabled).toBe(false); + expect(startBtn.setAttribute).toHaveBeenCalledWith('aria-describedby', ''); + }); + + test('should validate successfully when only clipboard is enabled', () => { + clipboardToggle.checked = true; + saveToPcToggle.checked = false; + + const result = validateOutputMethods(); + + expect(result).toBe(true); + expect(validationWarning.style.display).toBe('none'); + expect(startBtn.disabled).toBe(false); + expect(startBtn.setAttribute).toHaveBeenCalledWith('aria-describedby', ''); + }); + + test('should fail validation when both save to PC and clipboard are disabled', () => { + clipboardToggle.checked = false; + saveToPcToggle.checked = false; + + const result = validateOutputMethods(); + + expect(result).toBe(false); + expect(validationWarning.style.display).toBe('block'); + expect(startBtn.disabled).toBe(true); + expect(startBtn.setAttribute).toHaveBeenCalledWith('aria-describedby', 'validation-warning'); + }); +}); + +describe('Preference Persistence Across Browser Sessions', () => { + let mockClipboardToggle; + let mockSaveToPcToggle; + + beforeEach(() => { + jest.clearAllMocks(); + + // Mock DOM elements + mockClipboardToggle = { + checked: false, + addEventListener: jest.fn() + }; + + mockSaveToPcToggle = { + checked: false, + addEventListener: jest.fn() + }; + + global.document.getElementById = jest.fn((id) => { + switch (id) { + case 'clipboard-toggle': + return mockClipboardToggle; + case 'save-to-pc-toggle': + return mockSaveToPcToggle; + default: + return null; + } + }); + }); + + test('should default to both enabled for new installations', async () => { + // Mock no saved preferences (new installation) + global.chrome.storage.sync.get.mockResolvedValueOnce({}); + + const popupScript = ` + document.addEventListener('DOMContentLoaded', async () => { + const clipboardToggle = document.getElementById('clipboard-toggle'); + const saveToPcToggle = document.getElementById('save-to-pc-toggle'); + const saved = await chrome.storage.sync.get(['backgroundPreference', 'copyToClipboard', 'saveToPc']); + + clipboardToggle.checked = saved.copyToClipboard !== undefined ? saved.copyToClipboard : true; + saveToPcToggle.checked = saved.saveToPc !== undefined ? saved.saveToPc : true; + }); + `; + + await new Promise(resolve => { + global.document.addEventListener = jest.fn((event, callback) => { + if (event === 'DOMContentLoaded') { + callback(); + resolve(); + } + }); + eval(popupScript); + }); + + expect(mockClipboardToggle.checked).toBe(true); // Default + expect(mockSaveToPcToggle.checked).toBe(true); // Default + }); + + test('should persist save-only preferences across sessions', async () => { + // First session - user sets save-only mode + global.chrome.storage.sync.get.mockResolvedValueOnce({ + copyToClipboard: false, + saveToPc: true + }); + + const popupScript = ` + document.addEventListener('DOMContentLoaded', async () => { + const clipboardToggle = document.getElementById('clipboard-toggle'); + const saveToPcToggle = document.getElementById('save-to-pc-toggle'); + const saved = await chrome.storage.sync.get(['backgroundPreference', 'copyToClipboard', 'saveToPc']); + + clipboardToggle.checked = saved.copyToClipboard !== undefined ? saved.copyToClipboard : true; + saveToPcToggle.checked = saved.saveToPc !== undefined ? saved.saveToPc : true; + }); + `; + + await new Promise(resolve => { + global.document.addEventListener = jest.fn((event, callback) => { + if (event === 'DOMContentLoaded') { + callback(); + resolve(); + } + }); + eval(popupScript); + }); + + expect(mockClipboardToggle.checked).toBe(false); + expect(mockSaveToPcToggle.checked).toBe(true); + }); + + test('should persist clipboard-only preferences across sessions', async () => { + // Load clipboard-only mode + global.chrome.storage.sync.get.mockResolvedValueOnce({ + copyToClipboard: true, + saveToPc: false + }); + + const popupScript = ` + document.addEventListener('DOMContentLoaded', async () => { + const clipboardToggle = document.getElementById('clipboard-toggle'); + const saveToPcToggle = document.getElementById('save-to-pc-toggle'); + const saved = await chrome.storage.sync.get(['backgroundPreference', 'copyToClipboard', 'saveToPc']); + + clipboardToggle.checked = saved.copyToClipboard !== undefined ? saved.copyToClipboard : true; + saveToPcToggle.checked = saved.saveToPc !== undefined ? saved.saveToPc : true; + }); + `; + + await new Promise(resolve => { + global.document.addEventListener = jest.fn((event, callback) => { + if (event === 'DOMContentLoaded') { + callback(); + resolve(); + } + }); + eval(popupScript); + }); + + expect(mockClipboardToggle.checked).toBe(true); + expect(mockSaveToPcToggle.checked).toBe(false); + }); + + test('should persist both disabled preferences across sessions', async () => { + // Load both disabled state + global.chrome.storage.sync.get.mockResolvedValueOnce({ + copyToClipboard: false, + saveToPc: false + }); + + const popupScript = ` + document.addEventListener('DOMContentLoaded', async () => { + const clipboardToggle = document.getElementById('clipboard-toggle'); + const saveToPcToggle = document.getElementById('save-to-pc-toggle'); + const saved = await chrome.storage.sync.get(['backgroundPreference', 'copyToClipboard', 'saveToPc']); + + clipboardToggle.checked = saved.copyToClipboard !== undefined ? saved.copyToClipboard : true; + saveToPcToggle.checked = saved.saveToPc !== undefined ? saved.saveToPc : true; + }); + `; + + await new Promise(resolve => { + global.document.addEventListener = jest.fn((event, callback) => { + if (event === 'DOMContentLoaded') { + callback(); + resolve(); + } + }); + eval(popupScript); + }); + + expect(mockClipboardToggle.checked).toBe(false); + expect(mockSaveToPcToggle.checked).toBe(false); + + // Validation should fail with both disabled + const isValid = mockClipboardToggle.checked || mockSaveToPcToggle.checked; + expect(isValid).toBe(false); + }); + + test('should maintain existing behavior when upgrading from version without saveToPc', async () => { + // Mock existing installation with only clipboard preference + global.chrome.storage.sync.get.mockResolvedValueOnce({ + copyToClipboard: true + // saveToPc is undefined (not set in previous version) + }); + + const popupScript = ` + document.addEventListener('DOMContentLoaded', async () => { + const clipboardToggle = document.getElementById('clipboard-toggle'); + const saveToPcToggle = document.getElementById('save-to-pc-toggle'); + const saved = await chrome.storage.sync.get(['backgroundPreference', 'copyToClipboard', 'saveToPc']); + + clipboardToggle.checked = saved.copyToClipboard !== undefined ? saved.copyToClipboard : true; + saveToPcToggle.checked = saved.saveToPc !== undefined ? saved.saveToPc : true; + }); + `; + + await new Promise(resolve => { + global.document.addEventListener = jest.fn((event, callback) => { + if (event === 'DOMContentLoaded') { + callback(); + resolve(); + } + }); + eval(popupScript); + }); + + expect(mockClipboardToggle.checked).toBe(true); // Existing preference + expect(mockSaveToPcToggle.checked).toBe(true); // Default for backward compatibility + }); +}); \ No newline at end of file diff --git a/tests/validation-logic.test.js b/tests/validation-logic.test.js new file mode 100644 index 0000000..141c9eb --- /dev/null +++ b/tests/validation-logic.test.js @@ -0,0 +1,209 @@ +/** + * Tests for output method validation logic + */ + +// Mock DOM elements and Chrome APIs +const mockChrome = { + storage: { + sync: { + get: jest.fn(), + set: jest.fn() + } + }, + tabs: { + query: jest.fn(), + sendMessage: jest.fn() + }, + runtime: { + onMessage: { + addListener: jest.fn() + } + } +}; + +global.chrome = mockChrome; + +describe('Output Method Validation', () => { + let mockDocument; + let clipboardToggle; + let saveToPcToggle; + let startBtn; + let validationWarning; + let validateOutputMethods; + + beforeEach(() => { + // Reset mocks + jest.clearAllMocks(); + + // Mock DOM elements + clipboardToggle = { + checked: true, + addEventListener: jest.fn() + }; + + saveToPcToggle = { + checked: true, + addEventListener: jest.fn() + }; + + startBtn = { + disabled: false, + setAttribute: jest.fn(), + addEventListener: jest.fn() + }; + + validationWarning = { + style: { display: 'none' } + }; + + // Mock document.getElementById + const mockGetElementById = jest.fn((id) => { + switch (id) { + case 'clipboard-toggle': return clipboardToggle; + case 'save-to-pc-toggle': return saveToPcToggle; + case 'start-selector': return startBtn; + case 'validation-warning': return validationWarning; + default: return null; + } + }); + + mockDocument = { + getElementById: mockGetElementById, + addEventListener: jest.fn() + }; + + global.document = mockDocument; + + // Define validation function (extracted from popup.js logic) + validateOutputMethods = function() { + const hasValidOutput = clipboardToggle.checked || saveToPcToggle.checked; + + if (hasValidOutput) { + validationWarning.style.display = 'none'; + startBtn.disabled = false; + startBtn.setAttribute('aria-describedby', ''); + } else { + validationWarning.style.display = 'block'; + startBtn.disabled = true; + startBtn.setAttribute('aria-describedby', 'validation-warning'); + } + + return hasValidOutput; + }; + }); + + describe('Validation Logic', () => { + test('should return true when both save to PC and clipboard are enabled', () => { + clipboardToggle.checked = true; + saveToPcToggle.checked = true; + + const result = validateOutputMethods(); + + expect(result).toBe(true); + expect(validationWarning.style.display).toBe('none'); + expect(startBtn.disabled).toBe(false); + expect(startBtn.setAttribute).toHaveBeenCalledWith('aria-describedby', ''); + }); + + test('should return true when only save to PC is enabled', () => { + clipboardToggle.checked = false; + saveToPcToggle.checked = true; + + const result = validateOutputMethods(); + + expect(result).toBe(true); + expect(validationWarning.style.display).toBe('none'); + expect(startBtn.disabled).toBe(false); + expect(startBtn.setAttribute).toHaveBeenCalledWith('aria-describedby', ''); + }); + + test('should return true when only clipboard is enabled', () => { + clipboardToggle.checked = true; + saveToPcToggle.checked = false; + + const result = validateOutputMethods(); + + expect(result).toBe(true); + expect(validationWarning.style.display).toBe('none'); + expect(startBtn.disabled).toBe(false); + expect(startBtn.setAttribute).toHaveBeenCalledWith('aria-describedby', ''); + }); + + test('should return false when both save to PC and clipboard are disabled', () => { + clipboardToggle.checked = false; + saveToPcToggle.checked = false; + + const result = validateOutputMethods(); + + expect(result).toBe(false); + expect(validationWarning.style.display).toBe('block'); + expect(startBtn.disabled).toBe(true); + expect(startBtn.setAttribute).toHaveBeenCalledWith('aria-describedby', 'validation-warning'); + }); + }); + + describe('UI State Management', () => { + test('should hide warning and enable button when validation passes', () => { + clipboardToggle.checked = true; + saveToPcToggle.checked = false; + + validateOutputMethods(); + + expect(validationWarning.style.display).toBe('none'); + expect(startBtn.disabled).toBe(false); + expect(startBtn.setAttribute).toHaveBeenCalledWith('aria-describedby', ''); + }); + + test('should show warning and disable button when validation fails', () => { + clipboardToggle.checked = false; + saveToPcToggle.checked = false; + + validateOutputMethods(); + + expect(validationWarning.style.display).toBe('block'); + expect(startBtn.disabled).toBe(true); + expect(startBtn.setAttribute).toHaveBeenCalledWith('aria-describedby', 'validation-warning'); + }); + + test('should update accessibility attributes correctly', () => { + // Test valid state + clipboardToggle.checked = true; + saveToPcToggle.checked = true; + validateOutputMethods(); + expect(startBtn.setAttribute).toHaveBeenCalledWith('aria-describedby', ''); + + // Reset mock + startBtn.setAttribute.mockClear(); + + // Test invalid state + clipboardToggle.checked = false; + saveToPcToggle.checked = false; + validateOutputMethods(); + expect(startBtn.setAttribute).toHaveBeenCalledWith('aria-describedby', 'validation-warning'); + }); + }); + + describe('Edge Cases', () => { + test('should handle undefined checkbox states gracefully', () => { + clipboardToggle.checked = undefined; + saveToPcToggle.checked = true; + + const result = validateOutputMethods(); + + // undefined should be falsy, so only saveToPc (true) should make it valid + expect(result).toBe(true); + }); + + test('should handle null checkbox states gracefully', () => { + clipboardToggle.checked = null; + saveToPcToggle.checked = null; + + const result = validateOutputMethods(); + + // Both null should be falsy, so validation should fail + expect(result).toBeFalsy(); // Use toBeFalsy to handle null/false/undefined + expect(validationWarning.style.display).toBe('block'); + expect(startBtn.disabled).toBe(true); + }); + }); +}); \ No newline at end of file -- 2.49.1 From 0bbf4c2694bfc729303d6e9a9a7ee14321835463 Mon Sep 17 00:00:00 2001 From: KarthiDreamr Date: Wed, 13 Aug 2025 22:28:13 +0530 Subject: [PATCH 07/10] refactor: change toggle from '?' button to the 'switch' component --- popup.html | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/popup.html b/popup.html index 88317d4..66fa1e4 100644 --- a/popup.html +++ b/popup.html @@ -277,7 +277,7 @@
-
-
+
+
Automatically download screenshots as PNG files to your computer. Hover over the "?" for more details. @@ -310,7 +310,7 @@
-
-
+
+
Automatically copy screenshots to clipboard for quick pasting. Hover over the "?" for more details. -- 2.49.1 From 51d2595d4950cdd285bf59d469822ec97a8f275a Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Wed, 13 Aug 2025 13:08:06 -0600 Subject: [PATCH 08/10] Fix failing tests --- .../save-to-pc-clipboard-combinations.test.js | 4 + tests/setup.js | 184 ++++++++++++------ 2 files changed, 126 insertions(+), 62 deletions(-) diff --git a/tests/save-to-pc-clipboard-combinations.test.js b/tests/save-to-pc-clipboard-combinations.test.js index 7791096..4506c35 100644 --- a/tests/save-to-pc-clipboard-combinations.test.js +++ b/tests/save-to-pc-clipboard-combinations.test.js @@ -163,6 +163,10 @@ describe('Save to PC and Clipboard Combinations', () => { // 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 diff --git a/tests/setup.js b/tests/setup.js index 4ba46b1..e6a0b6e 100644 --- a/tests/setup.js +++ b/tests/setup.js @@ -3,6 +3,32 @@ * 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 }); @@ -56,30 +82,39 @@ global.createMockElement = (options = {}) => { // 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: jest.fn().mockResolvedValue({}), - set: jest.fn().mockResolvedValue() + get: existingStorage.get || jest.fn().mockResolvedValue({}), + set: existingStorage.set || jest.fn().mockResolvedValue() } }, runtime: { - sendMessage: jest.fn().mockResolvedValue(), + sendMessage: existingRuntime.sendMessage || jest.fn().mockResolvedValue(), onMessage: { - addListener: jest.fn() + addListener: (existingRuntime.onMessage && existingRuntime.onMessage.addListener) || jest.fn() } }, tabs: { - query: jest.fn().mockResolvedValue([{ id: 123, url: 'https://example.com' }]), - sendMessage: jest.fn().mockResolvedValue() + query: existingTabs.query || jest.fn().mockResolvedValue([{ id: 123, url: 'https://example.com' }]), + sendMessage: existingTabs.sendMessage || jest.fn().mockResolvedValue() } }; }; // Mock DOM APIs consistently global.mockDOMAPIs = () => { - global.document = { - createElement: jest.fn(() => ({ + // 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(), @@ -92,80 +127,105 @@ global.mockDOMAPIs = () => { className: '', checked: false, value: '' - })), - head: { appendChild: jest.fn() }, - body: { appendChild: jest.fn() }, - addEventListener: jest.fn(), - removeEventListener: jest.fn(), - querySelectorAll: jest.fn(() => []), - getElementById: jest.fn(() => null) - }; + })); + } + 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); - global.window = { - getComputedStyle: jest.fn(() => ({})), - devicePixelRatio: 2, - isSecureContext: true, - CSS: { escape: jest.fn(str => str) }, - setTimeout: jest.fn((fn, delay) => { + // 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; - }), - clearTimeout: jest.fn(), - close: jest.fn() - }; + }); + } + 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': - global.navigator = { - clipboard: { - write: jest.fn().mockResolvedValue() - } - }; - global.window.ClipboardItem = jest.fn(); - global.window.isSecureContext = true; + 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': - global.navigator = { - clipboard: { write: jest.fn() } - }; + 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': - global.navigator = { - clipboard: { write: jest.fn() } - }; - global.window.ClipboardItem = jest.fn(); + } + 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': - global.navigator = { - clipboard: { - write: jest.fn().mockRejectedValue(new DOMException('Permission denied', 'NotAllowedError')) - } - }; - global.window.ClipboardItem = jest.fn(); + } + 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': - global.navigator = { - clipboard: { - write: jest.fn().mockRejectedValue(new DOMException('Security error', 'SecurityError')) - } - }; - global.window.ClipboardItem = jest.fn(); + } + 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; + } } }; -- 2.49.1 From f5e4df10e4fff12f383a0c26aec2356717dc8f59 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Wed, 13 Aug 2025 13:09:59 -0600 Subject: [PATCH 09/10] Update gitignore and remove unnecessary files --- .gitignore | 4 +- .../specs/copy-to-clipboard-toggle/design.md | 165 - .../copy-to-clipboard-toggle/requirements.md | 51 - .kiro/specs/copy-to-clipboard-toggle/tasks.md | 64 - .kiro/specs/save-to-pc-toggle/design.md | 187 -- .kiro/specs/save-to-pc-toggle/requirements.md | 51 - .kiro/specs/save-to-pc-toggle/tasks.md | 71 - .vscode/settings.json | 3 - coverage/base.css | 224 -- coverage/block-navigation.js | 87 - coverage/content.js.html | 2893 ----------------- coverage/favicon.png | Bin 445 -> 0 bytes coverage/index.html | 131 - coverage/lcov-report/base.css | 224 -- coverage/lcov-report/block-navigation.js | 87 - coverage/lcov-report/content.js.html | 2893 ----------------- coverage/lcov-report/favicon.png | Bin 445 -> 0 bytes coverage/lcov-report/index.html | 131 - coverage/lcov-report/popup.js.html | 643 ---- coverage/lcov-report/prettify.css | 1 - coverage/lcov-report/prettify.js | 2 - coverage/lcov-report/sort-arrow-sprite.png | Bin 138 -> 0 bytes coverage/lcov-report/sorter.js | 196 -- coverage/lcov.info | 848 ----- coverage/popup.js.html | 643 ---- coverage/prettify.css | 1 - coverage/prettify.js | 2 - coverage/sort-arrow-sprite.png | Bin 138 -> 0 bytes coverage/sorter.js | 196 -- 29 files changed, 3 insertions(+), 9795 deletions(-) delete mode 100644 .kiro/specs/copy-to-clipboard-toggle/design.md delete mode 100644 .kiro/specs/copy-to-clipboard-toggle/requirements.md delete mode 100644 .kiro/specs/copy-to-clipboard-toggle/tasks.md delete mode 100644 .kiro/specs/save-to-pc-toggle/design.md delete mode 100644 .kiro/specs/save-to-pc-toggle/requirements.md delete mode 100644 .kiro/specs/save-to-pc-toggle/tasks.md delete mode 100644 .vscode/settings.json delete mode 100644 coverage/base.css delete mode 100644 coverage/block-navigation.js delete mode 100644 coverage/content.js.html delete mode 100644 coverage/favicon.png delete mode 100644 coverage/index.html delete mode 100644 coverage/lcov-report/base.css delete mode 100644 coverage/lcov-report/block-navigation.js delete mode 100644 coverage/lcov-report/content.js.html delete mode 100644 coverage/lcov-report/favicon.png delete mode 100644 coverage/lcov-report/index.html delete mode 100644 coverage/lcov-report/popup.js.html delete mode 100644 coverage/lcov-report/prettify.css delete mode 100644 coverage/lcov-report/prettify.js delete mode 100644 coverage/lcov-report/sort-arrow-sprite.png delete mode 100644 coverage/lcov-report/sorter.js delete mode 100644 coverage/lcov.info delete mode 100644 coverage/popup.js.html delete mode 100644 coverage/prettify.css delete mode 100644 coverage/prettify.js delete mode 100644 coverage/sort-arrow-sprite.png delete mode 100644 coverage/sorter.js diff --git a/.gitignore b/.gitignore index 40b878d..c06c56b 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ -node_modules/ \ No newline at end of file +node_modules/ +.kiro/ +.vscode/ \ No newline at end of file diff --git a/.kiro/specs/copy-to-clipboard-toggle/design.md b/.kiro/specs/copy-to-clipboard-toggle/design.md deleted file mode 100644 index 7d4af9a..0000000 --- a/.kiro/specs/copy-to-clipboard-toggle/design.md +++ /dev/null @@ -1,165 +0,0 @@ -# Design Document - -## Overview - -The copy to clipboard toggle feature will be implemented as an additional setting in the extension popup, similar to the existing background color selector. The feature will use the modern Clipboard API with appropriate fallbacks for older browsers. The implementation will integrate seamlessly with the existing screenshot capture workflow, adding clipboard functionality without disrupting the current download behavior. - -## Architecture - -The feature will be implemented across three main components: - -1. **Popup UI Enhancement**: Add toggle control and persistence logic -2. **Content Script Integration**: Modify screenshot capture to include clipboard operations -3. **Storage Management**: Extend existing preference storage system - -The clipboard functionality will be implemented as an optional enhancement to the existing screenshot workflow, ensuring backward compatibility and graceful degradation. - -## Components and Interfaces - -### Popup Interface (`popup.html` & `popup.js`) - -**New UI Elements:** -- Toggle switch control for "Copy to Clipboard" option -- Positioned between background selector and start button -- Consistent styling with existing controls - -**Storage Integration:** -- Extend existing `chrome.storage.sync` usage to include `copyToClipboard` preference -- Default value: `true` (enabled by default for better user experience) -- Load and save preferences alongside existing background preference - -**Message Passing:** -- Include `copyToClipboard` setting in the `startSelector` message to content script -- No changes needed to existing message structure, just additional property - -### Content Script Enhancement (`content.js`) - -**ScreenshotSelector Class Modifications:** -- Add `copyToClipboard` property to constructor -- Modify `init()` method to accept and store clipboard preference -- Enhance `captureElement()` method to include clipboard operations - -**Clipboard Implementation:** -```javascript -async copyCanvasToClipboard(canvas) { - try { - // Convert canvas to blob - const blob = await new Promise(resolve => - canvas.toBlob(resolve, 'image/png') - ); - - // Use Clipboard API - await navigator.clipboard.write([ - new ClipboardItem({ 'image/png': blob }) - ]); - - return { success: true }; - } catch (error) { - return { success: false, error: error.message }; - } -} -``` - -**Error Handling:** -- Graceful fallback when Clipboard API is unavailable -- User-friendly error messages for clipboard failures -- Ensure screenshot download continues even if clipboard fails - -### User Feedback System - -**Success States:** -- "Screenshot saved and copied to clipboard!" - when both operations succeed -- "Screenshot saved! (Clipboard copy failed: [reason])" - when download succeeds but clipboard fails - -**Loading States:** -- Update existing loading message to indicate clipboard operation when enabled -- "Capturing screenshot and preparing clipboard..." vs "Capturing screenshot..." - -## Data Models - -### Settings Storage Schema -```javascript -{ - backgroundPreference: 'black' | 'transparent' | 'white', - copyToClipboard: boolean // New addition -} -``` - -### Message Interface Extension -```javascript -// startSelector message -{ - action: 'startSelector', - background: string, - copyToClipboard: boolean // New addition -} -``` - -## Error Handling - -### Clipboard API Availability -- Check for `navigator.clipboard` and `ClipboardItem` support -- Provide clear error messages when API is unavailable -- Continue with download-only functionality as fallback - -### Permission Handling -- Clipboard write operations may require user gesture -- Handle permission denied scenarios gracefully -- Provide user guidance for enabling clipboard permissions if needed - -### Browser Compatibility -- Primary support: Chrome/Edge 76+, Firefox 87+, Safari 13.1+ -- Fallback behavior for older browsers: disable clipboard feature with notification -- No impact on core screenshot functionality - -## Testing Strategy - -### Unit Testing Approach -- Mock Clipboard API for testing clipboard operations -- Test preference storage and retrieval -- Verify message passing between popup and content script -- Test error handling scenarios - -### Integration Testing -- Test complete workflow: toggle setting → capture screenshot → verify clipboard content -- Test with different background settings and clipboard combinations -- Verify graceful degradation when Clipboard API unavailable - -### Browser Compatibility Testing -- Test across Chrome, Firefox, Safari, Edge -- Verify behavior on older browser versions -- Test permission scenarios and user gesture requirements - -### User Experience Testing -- Verify toggle state persistence across browser sessions -- Test visual feedback for success/error states -- Confirm tooltip and help text clarity - -## Implementation Considerations - -### Performance Impact -- Clipboard operations are asynchronous and won't block screenshot download -- Canvas-to-blob conversion adds minimal overhead -- No impact on screenshot capture performance - -### Security Considerations -- Clipboard API requires secure context (HTTPS) -- User gesture requirement for clipboard write operations -- No sensitive data exposure through clipboard operations - -### Accessibility -- Toggle control will include proper ARIA labels -- Keyboard navigation support for toggle control -- Screen reader compatible success/error messages - -## Browser API Dependencies - -### Required APIs -- `navigator.clipboard.write()` - Modern clipboard API -- `ClipboardItem` constructor - For image data handling -- `canvas.toBlob()` - Convert canvas to blob format - -### Fallback Strategy -- Detect API availability before attempting clipboard operations -- Provide clear user feedback when clipboard features unavailable -- Maintain full functionality for screenshot download regardless of clipboard support \ No newline at end of file diff --git a/.kiro/specs/copy-to-clipboard-toggle/requirements.md b/.kiro/specs/copy-to-clipboard-toggle/requirements.md deleted file mode 100644 index 2553626..0000000 --- a/.kiro/specs/copy-to-clipboard-toggle/requirements.md +++ /dev/null @@ -1,51 +0,0 @@ -# Requirements Document - -## Introduction - -This feature adds a copy to clipboard toggle option to the Full Screenshot Selector extension, allowing users to choose whether captured screenshots should be automatically copied to the clipboard in addition to being downloaded as files. This provides users with more flexibility in how they handle their screenshots, enabling quick pasting into other applications without needing to locate and open the downloaded file. - -## Requirements - -### Requirement 1 - -**User Story:** As a user of the screenshot extension, I want to toggle whether screenshots are copied to clipboard, so that I can quickly paste them into other applications without downloading files. - -#### Acceptance Criteria - -1. WHEN the popup is opened THEN the system SHALL display a toggle control for "Copy to Clipboard" -2. WHEN the user toggles the copy to clipboard option THEN the system SHALL save this preference to browser storage -3. WHEN the extension is reopened THEN the system SHALL restore the previously saved copy to clipboard preference -4. WHEN a screenshot is captured AND copy to clipboard is enabled THEN the system SHALL copy the image data to the clipboard -5. WHEN a screenshot is captured AND copy to clipboard is enabled THEN the system SHALL still download the file as before (both actions occur) - -### Requirement 2 - -**User Story:** As a user, I want the copy to clipboard feature to work reliably across different browsers, so that I can depend on this functionality regardless of my browser choice. - -#### Acceptance Criteria - -1. WHEN copy to clipboard is enabled THEN the system SHALL use the Clipboard API if available -2. IF the Clipboard API is not available THEN the system SHALL provide appropriate fallback behavior -3. WHEN clipboard copying fails THEN the system SHALL display an error message to the user -4. WHEN clipboard copying succeeds THEN the system SHALL display a success confirmation - -### Requirement 3 - -**User Story:** As a user, I want clear visual feedback about the copy to clipboard feature, so that I understand when it's enabled and when clipboard operations succeed or fail. - -#### Acceptance Criteria - -1. WHEN the copy to clipboard toggle is enabled THEN the system SHALL display visual indication of the enabled state -2. WHEN a screenshot is captured with clipboard copying enabled THEN the system SHALL show a success message indicating both download and clipboard copy -3. WHEN clipboard copying fails THEN the system SHALL show an error message while still confirming the download succeeded -4. WHEN hovering over the copy to clipboard toggle THEN the system SHALL display helpful tooltip text - -### Requirement 4 - -**User Story:** As a user, I want the copy to clipboard feature to handle different image formats appropriately, so that the clipboard content works well with various applications. - -#### Acceptance Criteria - -1. WHEN copying to clipboard THEN the system SHALL copy the image in PNG format -2. WHEN copying to clipboard THEN the system SHALL preserve the same image quality as the downloaded file -3. WHEN copying to clipboard THEN the system SHALL handle transparency settings according to the background preference \ No newline at end of file diff --git a/.kiro/specs/copy-to-clipboard-toggle/tasks.md b/.kiro/specs/copy-to-clipboard-toggle/tasks.md deleted file mode 100644 index 25d7b74..0000000 --- a/.kiro/specs/copy-to-clipboard-toggle/tasks.md +++ /dev/null @@ -1,64 +0,0 @@ -# Implementation Plan - -- [x] 1. Add copy to clipboard toggle UI to popup - - Add HTML toggle control between background selector and start button in popup.html - - Style the toggle control to match existing UI design patterns - - Include proper ARIA labels and accessibility attributes for the toggle - - _Requirements: 1.1, 3.1, 3.4_ - -- [x] 2. Implement clipboard preference storage in popup - - Extend existing storage logic in popup.js to handle copyToClipboard preference - - Set default value to true for better user experience - - Load saved clipboard preference on popup initialization - - Save clipboard preference changes to chrome.storage.sync - - _Requirements: 1.2, 1.3_ - -- [x] 3. Update message passing to include clipboard setting - - Modify startSelector message in popup.js to include copyToClipboard property - - Update content script message handler to receive and store clipboard preference - - Ensure backward compatibility with existing message structure - - _Requirements: 1.1, 1.4_ - -- [x] 4. Implement clipboard API functionality in content script - - Add copyCanvasToClipboard method to ScreenshotSelector class - - Implement canvas to blob conversion for PNG format - - Use navigator.clipboard.write() with ClipboardItem for image data - - Handle async clipboard operations with proper error catching - - _Requirements: 2.1, 4.1, 4.2, 4.3_ - -- [x] 5. Add clipboard API availability detection - - Create method to check for navigator.clipboard and ClipboardItem support - - Implement graceful fallback when Clipboard API is unavailable - - Provide user feedback when clipboard features are not supported - - _Requirements: 2.2_ - -- [x] 6. Integrate clipboard operations into screenshot capture workflow - - Modify captureElement method to include clipboard operations when enabled - - Ensure clipboard copying happens after successful canvas generation - - Maintain existing download functionality regardless of clipboard success/failure - - _Requirements: 1.4, 1.5_ - -- [x] 7. Implement comprehensive error handling for clipboard operations - - Handle clipboard write permission errors gracefully - - Provide specific error messages for different failure scenarios - - Ensure screenshot download continues even when clipboard operations fail - - _Requirements: 2.3_ - -- [x] 8. Update user feedback messages for clipboard operations - - Modify loading message to indicate clipboard preparation when enabled - - Update success message to confirm both download and clipboard copy - - Create error messages that distinguish between download and clipboard failures - - _Requirements: 2.4, 3.2, 3.3_ - -- [x] 9. Add tooltip and help text for clipboard toggle - - Implement tooltip showing clipboard feature explanation - - Add help text explaining clipboard functionality and browser requirements - - Ensure tooltip is accessible and keyboard navigable - - _Requirements: 3.4_ - -- [x] 10. Test clipboard functionality across different scenarios - - Write test cases for clipboard API availability detection - - Test clipboard operations with different background settings (transparent, black, white) - - Verify error handling when clipboard API is unavailable or permissions denied - - Test preference persistence across browser sessions - - _Requirements: 2.1, 2.2, 2.3, 4.3_ \ No newline at end of file diff --git a/.kiro/specs/save-to-pc-toggle/design.md b/.kiro/specs/save-to-pc-toggle/design.md deleted file mode 100644 index b877881..0000000 --- a/.kiro/specs/save-to-pc-toggle/design.md +++ /dev/null @@ -1,187 +0,0 @@ -# Design Document - -## Overview - -The save to PC toggle feature will be implemented as an additional setting in the extension popup, positioned alongside the existing clipboard toggle. The feature will modify the existing screenshot capture workflow to conditionally perform file downloads based on user preference. The implementation will ensure that users can independently control both clipboard copying and file downloading, providing maximum flexibility in their screenshot workflow. - -## Architecture - -The feature will be implemented across three main components: - -1. **Popup UI Enhancement**: Add toggle control and persistence logic -2. **Content Script Integration**: Modify screenshot capture to conditionally download files -3. **Storage Management**: Extend existing preference storage system -4. **Validation Logic**: Ensure at least one output method is selected - -The save to PC functionality will be implemented as a conditional enhancement to the existing screenshot workflow, maintaining backward compatibility while providing new flexibility. - -## Components and Interfaces - -### Popup Interface (`popup.html` & `popup.js`) - -**New UI Elements:** -- Toggle switch control for "Save to PC" option -- Positioned above the clipboard toggle for logical grouping -- Consistent styling with existing controls -- Warning message when both toggles are disabled - -**Storage Integration:** -- Extend existing `chrome.storage.sync` usage to include `saveToPc` preference -- Default value: `true` (enabled by default to maintain existing behavior) -- Load and save preferences alongside existing background and clipboard preferences - -**Validation Logic:** -- Check that at least one output method (save to PC or clipboard) is enabled -- Display warning message when both are disabled -- Prevent screenshot capture when no output method is selected - -**Message Passing:** -- Include `saveToPc` setting in the `startSelector` message to content script -- No changes needed to existing message structure, just additional property - -### Content Script Enhancement (`content.js`) - -**ScreenshotSelector Class Modifications:** -- Add `saveToPc` property to constructor -- Modify `init()` method to accept and store save to PC preference -- Enhance `captureElement()` method to conditionally perform file downloads - -**Conditional Download Implementation:** -```javascript -// In captureElement method, replace automatic download with conditional logic -if (this.saveToPc) { - const link = document.createElement('a'); - link.href = canvas.toDataURL('image/png'); - link.download = `screenshot-${new Date().toISOString().slice(0, 19).replace(/[:.]/g, '-')}.png`; - link.click(); -} -``` - -**User Feedback Enhancement:** -- Update success messages to reflect which operations were performed -- Handle cases where no output method is selected (should not occur due to popup validation) - -### User Feedback System - -**Success States:** -- "Screenshot saved and copied to clipboard!" - when both operations succeed -- "Screenshot saved!" - when only save to PC is enabled and succeeds -- "Screenshot copied to clipboard!" - when only clipboard is enabled and succeeds -- "Screenshot captured!" - fallback message for edge cases - -**Error States:** -- "Please enable at least one output method (Save to PC or Copy to Clipboard)" - when both are disabled -- Existing clipboard error messages remain unchanged - -**Loading States:** -- Update loading message to reflect enabled operations -- "Capturing screenshot..." - when only save to PC is enabled -- "Capturing screenshot and preparing for clipboard..." - when only clipboard is enabled -- "Capturing screenshot and preparing for clipboard..." - when both are enabled (clipboard preparation is the longer operation) - -## Data Models - -### Settings Storage Schema -```javascript -{ - backgroundPreference: 'black' | 'transparent' | 'white', - copyToClipboard: boolean, - saveToPc: boolean // New addition -} -``` - -### Message Interface Extension -```javascript -// startSelector message -{ - action: 'startSelector', - background: string, - copyToClipboard: boolean, - saveToPc: boolean // New addition -} -``` - -## Error Handling - -### Validation Errors -- Prevent screenshot capture when both save to PC and clipboard are disabled -- Display clear error message in popup when no output method is selected -- Provide guidance on enabling at least one option - -### Download Failures -- Handle file download failures gracefully (though these are rare in modern browsers) -- Ensure clipboard operations continue even if download fails (when both are enabled) -- Provide specific error messages for download-related issues - -### Backward Compatibility -- Ensure existing users see no change in behavior (save to PC enabled by default) -- Handle cases where preference is not yet set in storage -- Maintain existing error handling for clipboard operations - -## Testing Strategy - -### Unit Testing Approach -- Test preference storage and retrieval for save to PC setting -- Verify message passing includes new saveToPc property -- Test validation logic for ensuring at least one output method is enabled -- Mock file download operations for testing conditional download logic - -### Integration Testing -- Test complete workflow: toggle setting → capture screenshot → verify file download behavior -- Test all four combinations of save to PC and clipboard settings -- Verify error handling when both options are disabled -- Test preference persistence across browser sessions - -### User Experience Testing -- Verify toggle state persistence across browser sessions -- Test visual feedback for different operation combinations -- Confirm tooltip and help text clarity -- Test warning messages when both toggles are disabled - -## Implementation Considerations - -### Performance Impact -- No performance impact when save to PC is enabled (existing behavior) -- Slight performance improvement when save to PC is disabled (no file creation) -- No impact on clipboard operations - -### Security Considerations -- File downloads use existing browser download mechanisms -- No new security concerns introduced -- Maintains existing security model for clipboard operations - -### Accessibility -- Save to PC toggle will include proper ARIA labels -- Keyboard navigation support for toggle control -- Screen reader compatible success/error messages -- Warning messages will be announced to screen readers - -### User Experience Design -- Logical grouping of output method toggles -- Clear visual hierarchy with save to PC above clipboard (primary → secondary) -- Consistent styling with existing UI elements -- Intuitive default settings (both enabled initially) - -## Browser Compatibility - -### File Download Support -- File downloads work in all modern browsers -- No fallback needed as this is core browser functionality -- Existing implementation already handles download edge cases - -### Storage and UI -- Uses existing chrome.storage.sync API (already implemented) -- Toggle UI uses existing CSS patterns -- No new browser API dependencies - -## Migration Strategy - -### Existing Users -- Default `saveToPc` to `true` when preference doesn't exist -- No change in behavior for existing users -- Seamless upgrade experience - -### New Users -- Both save to PC and clipboard enabled by default -- Provides full functionality out of the box -- Users can customize based on their workflow preferences \ No newline at end of file diff --git a/.kiro/specs/save-to-pc-toggle/requirements.md b/.kiro/specs/save-to-pc-toggle/requirements.md deleted file mode 100644 index 20b9a19..0000000 --- a/.kiro/specs/save-to-pc-toggle/requirements.md +++ /dev/null @@ -1,51 +0,0 @@ -# Requirements Document - -## Introduction - -This feature adds a save to PC toggle option to the Full Screenshot Selector extension, allowing users to choose whether captured screenshots should be automatically downloaded as files to their computer. This provides users with more flexibility in how they handle their screenshots, enabling them to use only the clipboard functionality without cluttering their downloads folder with files they may not need. - -## Requirements - -### Requirement 1 - -**User Story:** As a user of the screenshot extension, I want to toggle whether screenshots are saved to my PC, so that I can avoid downloading files when I only need to paste the image elsewhere. - -#### Acceptance Criteria - -1. WHEN the popup is opened THEN the system SHALL display a toggle control for "Save to PC" -2. WHEN the user toggles the save to PC option THEN the system SHALL save this preference to browser storage -3. WHEN the extension is reopened THEN the system SHALL restore the previously saved save to PC preference -4. WHEN a screenshot is captured AND save to PC is enabled THEN the system SHALL download the file as before -5. WHEN a screenshot is captured AND save to PC is disabled THEN the system SHALL NOT download any file - -### Requirement 2 - -**User Story:** As a user, I want clear visual feedback about the save to PC feature, so that I understand when files will be downloaded and when they won't. - -#### Acceptance Criteria - -1. WHEN the save to PC toggle is enabled THEN the system SHALL display visual indication of the enabled state -2. WHEN a screenshot is captured with save to PC enabled THEN the system SHALL show a success message indicating the download -3. WHEN a screenshot is captured with save to PC disabled THEN the system SHALL show a success message without mentioning download -4. WHEN hovering over the save to PC toggle THEN the system SHALL display helpful tooltip text - -### Requirement 3 - -**User Story:** As a user, I want the save to PC feature to work independently of the clipboard feature, so that I can choose any combination of saving and copying that suits my workflow. - -#### Acceptance Criteria - -1. WHEN both save to PC and copy to clipboard are enabled THEN the system SHALL perform both operations -2. WHEN only save to PC is enabled THEN the system SHALL only download the file -3. WHEN only copy to clipboard is enabled THEN the system SHALL only copy to clipboard -4. WHEN both features are disabled THEN the system SHALL show an error message and not capture the screenshot - -### Requirement 4 - -**User Story:** As a user, I want sensible default settings for the save to PC feature, so that the extension works intuitively without requiring configuration. - -#### Acceptance Criteria - -1. WHEN the extension is used for the first time THEN the save to PC option SHALL be enabled by default -2. WHEN upgrading from a previous version THEN the save to PC option SHALL be enabled by default to maintain existing behavior -3. WHEN both save to PC and clipboard options are available THEN users SHALL be able to configure them independently \ No newline at end of file diff --git a/.kiro/specs/save-to-pc-toggle/tasks.md b/.kiro/specs/save-to-pc-toggle/tasks.md deleted file mode 100644 index f5d16d3..0000000 --- a/.kiro/specs/save-to-pc-toggle/tasks.md +++ /dev/null @@ -1,71 +0,0 @@ -# Implementation Plan - -- [x] 1. Add save to PC toggle UI to popup - - Add HTML toggle control above the clipboard toggle in popup.html - - Style the toggle control to match existing UI design patterns - - Include proper ARIA labels and accessibility attributes for the toggle - - Position the save to PC toggle logically above clipboard toggle - - _Requirements: 1.1, 2.1, 2.4_ - -- [x] 2. Implement save to PC preference storage in popup - - Extend existing storage logic in popup.js to handle saveToPc preference - - Set default value to true to maintain existing behavior for current users - - Load saved save to PC preference on popup initialization - - Save save to PC preference changes to chrome.storage.sync - - _Requirements: 1.2, 1.3, 4.1, 4.2_ - -- [x] 3. Add validation logic for output method selection - - Create validation function to ensure at least one output method is enabled - - Display warning message in popup when both save to PC and clipboard are disabled - - Prevent screenshot capture when no output method is selected - - Update start button state based on validation results - - _Requirements: 3.4_ - -- [x] 4. Update message passing to include save to PC setting - - Modify startSelector message in popup.js to include saveToPc property - - Update content script message handler to receive and store save to PC preference - - Ensure backward compatibility with existing message structure - - _Requirements: 1.1, 1.4_ - -- [x] 5. Implement conditional file download in content script - - Modify captureElement method in ScreenshotSelector class to conditionally download files - - Wrap existing download logic in conditional check for saveToPc preference - - Ensure canvas generation still occurs regardless of download preference (needed for clipboard) - - Maintain existing download filename generation and format - - _Requirements: 1.4, 1.5, 3.1, 3.2_ - -- [x] 6. Update user feedback messages for different operation combinations - - Modify success messages to reflect which operations were performed - - Create specific messages for save-only, clipboard-only, and both operations - - Update loading messages to reflect enabled operations - - Ensure error messages are appropriate for each operation combination - - _Requirements: 2.2, 2.3, 3.1, 3.2, 3.3_ - -- [x] 7. Add tooltip and help text for save to PC toggle - - Implement tooltip showing save to PC feature explanation - - Add help text explaining the relationship between save to PC and clipboard features - - Ensure tooltip is accessible and keyboard navigable - - Include information about default behavior and workflow flexibility - - _Requirements: 2.4_ - -- [x] 8. Implement warning system for disabled output methods - - Create visual warning when both save to PC and clipboard toggles are disabled - - Display clear guidance on enabling at least one output method - - Make warning accessible to screen readers - - Update warning state dynamically as toggles change - - _Requirements: 3.4_ - -- [x] 9. Test all combinations of save to PC and clipboard settings - - Write test cases for save-only mode (saveToPc: true, clipboard: false) - - Write test cases for clipboard-only mode (saveToPc: false, clipboard: true) - - Write test cases for both enabled (saveToPc: true, clipboard: true) - - Write test cases for validation when both disabled (saveToPc: false, clipboard: false) - - Verify preference persistence across browser sessions for all combinations - - _Requirements: 3.1, 3.2, 3.3, 3.4, 4.3_ - -- [ ] 10. Test backward compatibility and migration - - Test behavior when saveToPc preference doesn't exist in storage (new installation) - - Test behavior when upgrading from version without saveToPc preference - - Verify that existing users maintain current behavior (files still download by default) - - Test that new users get both features enabled by default - - _Requirements: 4.1, 4.2_ \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 5480842..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "kiroAgent.configureMCP": "Disabled" -} \ No newline at end of file diff --git a/coverage/base.css b/coverage/base.css deleted file mode 100644 index f418035..0000000 --- a/coverage/base.css +++ /dev/null @@ -1,224 +0,0 @@ -body, html { - margin:0; padding: 0; - height: 100%; -} -body { - font-family: Helvetica Neue, Helvetica, Arial; - font-size: 14px; - color:#333; -} -.small { font-size: 12px; } -*, *:after, *:before { - -webkit-box-sizing:border-box; - -moz-box-sizing:border-box; - box-sizing:border-box; - } -h1 { font-size: 20px; margin: 0;} -h2 { font-size: 14px; } -pre { - font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace; - margin: 0; - padding: 0; - -moz-tab-size: 2; - -o-tab-size: 2; - tab-size: 2; -} -a { color:#0074D9; text-decoration:none; } -a:hover { text-decoration:underline; } -.strong { font-weight: bold; } -.space-top1 { padding: 10px 0 0 0; } -.pad2y { padding: 20px 0; } -.pad1y { padding: 10px 0; } -.pad2x { padding: 0 20px; } -.pad2 { padding: 20px; } -.pad1 { padding: 10px; } -.space-left2 { padding-left:55px; } -.space-right2 { padding-right:20px; } -.center { text-align:center; } -.clearfix { display:block; } -.clearfix:after { - content:''; - display:block; - height:0; - clear:both; - visibility:hidden; - } -.fl { float: left; } -@media only screen and (max-width:640px) { - .col3 { width:100%; max-width:100%; } - .hide-mobile { display:none!important; } -} - -.quiet { - color: #7f7f7f; - color: rgba(0,0,0,0.5); -} -.quiet a { opacity: 0.7; } - -.fraction { - font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; - font-size: 10px; - color: #555; - background: #E8E8E8; - padding: 4px 5px; - border-radius: 3px; - vertical-align: middle; -} - -div.path a:link, div.path a:visited { color: #333; } -table.coverage { - border-collapse: collapse; - margin: 10px 0 0 0; - padding: 0; -} - -table.coverage td { - margin: 0; - padding: 0; - vertical-align: top; -} -table.coverage td.line-count { - text-align: right; - padding: 0 5px 0 20px; -} -table.coverage td.line-coverage { - text-align: right; - padding-right: 10px; - min-width:20px; -} - -table.coverage td span.cline-any { - display: inline-block; - padding: 0 5px; - width: 100%; -} -.missing-if-branch { - display: inline-block; - margin-right: 5px; - border-radius: 3px; - position: relative; - padding: 0 4px; - background: #333; - color: yellow; -} - -.skip-if-branch { - display: none; - margin-right: 10px; - position: relative; - padding: 0 4px; - background: #ccc; - color: white; -} -.missing-if-branch .typ, .skip-if-branch .typ { - color: inherit !important; -} -.coverage-summary { - border-collapse: collapse; - width: 100%; -} -.coverage-summary tr { border-bottom: 1px solid #bbb; } -.keyline-all { border: 1px solid #ddd; } -.coverage-summary td, .coverage-summary th { padding: 10px; } -.coverage-summary tbody { border: 1px solid #bbb; } -.coverage-summary td { border-right: 1px solid #bbb; } -.coverage-summary td:last-child { border-right: none; } -.coverage-summary th { - text-align: left; - font-weight: normal; - white-space: nowrap; -} -.coverage-summary th.file { border-right: none !important; } -.coverage-summary th.pct { } -.coverage-summary th.pic, -.coverage-summary th.abs, -.coverage-summary td.pct, -.coverage-summary td.abs { text-align: right; } -.coverage-summary td.file { white-space: nowrap; } -.coverage-summary td.pic { min-width: 120px !important; } -.coverage-summary tfoot td { } - -.coverage-summary .sorter { - height: 10px; - width: 7px; - display: inline-block; - margin-left: 0.5em; - background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; -} -.coverage-summary .sorted .sorter { - background-position: 0 -20px; -} -.coverage-summary .sorted-desc .sorter { - background-position: 0 -10px; -} -.status-line { height: 10px; } -/* yellow */ -.cbranch-no { background: yellow !important; color: #111; } -/* dark red */ -.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 } -.low .chart { border:1px solid #C21F39 } -.highlighted, -.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{ - background: #C21F39 !important; -} -/* medium red */ -.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE } -/* light red */ -.low, .cline-no { background:#FCE1E5 } -/* light green */ -.high, .cline-yes { background:rgb(230,245,208) } -/* medium green */ -.cstat-yes { background:rgb(161,215,106) } -/* dark green */ -.status-line.high, .high .cover-fill { background:rgb(77,146,33) } -.high .chart { border:1px solid rgb(77,146,33) } -/* dark yellow (gold) */ -.status-line.medium, .medium .cover-fill { background: #f9cd0b; } -.medium .chart { border:1px solid #f9cd0b; } -/* light yellow */ -.medium { background: #fff4c2; } - -.cstat-skip { background: #ddd; color: #111; } -.fstat-skip { background: #ddd; color: #111 !important; } -.cbranch-skip { background: #ddd !important; color: #111; } - -span.cline-neutral { background: #eaeaea; } - -.coverage-summary td.empty { - opacity: .5; - padding-top: 4px; - padding-bottom: 4px; - line-height: 1; - color: #888; -} - -.cover-fill, .cover-empty { - display:inline-block; - height: 12px; -} -.chart { - line-height: 0; -} -.cover-empty { - background: white; -} -.cover-full { - border-right: none !important; -} -pre.prettyprint { - border: none !important; - padding: 0 !important; - margin: 0 !important; -} -.com { color: #999 !important; } -.ignore-none { color: #999; font-weight: normal; } - -.wrapper { - min-height: 100%; - height: auto !important; - height: 100%; - margin: 0 auto -48px; -} -.footer, .push { - height: 48px; -} diff --git a/coverage/block-navigation.js b/coverage/block-navigation.js deleted file mode 100644 index cc12130..0000000 --- a/coverage/block-navigation.js +++ /dev/null @@ -1,87 +0,0 @@ -/* eslint-disable */ -var jumpToCode = (function init() { - // Classes of code we would like to highlight in the file view - var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no']; - - // Elements to highlight in the file listing view - var fileListingElements = ['td.pct.low']; - - // We don't want to select elements that are direct descendants of another match - var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > ` - - // Selecter that finds elements on the page to which we can jump - var selector = - fileListingElements.join(', ') + - ', ' + - notSelector + - missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b` - - // The NodeList of matching elements - var missingCoverageElements = document.querySelectorAll(selector); - - var currentIndex; - - function toggleClass(index) { - missingCoverageElements - .item(currentIndex) - .classList.remove('highlighted'); - missingCoverageElements.item(index).classList.add('highlighted'); - } - - function makeCurrent(index) { - toggleClass(index); - currentIndex = index; - missingCoverageElements.item(index).scrollIntoView({ - behavior: 'smooth', - block: 'center', - inline: 'center' - }); - } - - function goToPrevious() { - var nextIndex = 0; - if (typeof currentIndex !== 'number' || currentIndex === 0) { - nextIndex = missingCoverageElements.length - 1; - } else if (missingCoverageElements.length > 1) { - nextIndex = currentIndex - 1; - } - - makeCurrent(nextIndex); - } - - function goToNext() { - var nextIndex = 0; - - if ( - typeof currentIndex === 'number' && - currentIndex < missingCoverageElements.length - 1 - ) { - nextIndex = currentIndex + 1; - } - - makeCurrent(nextIndex); - } - - return function jump(event) { - if ( - document.getElementById('fileSearch') === document.activeElement && - document.activeElement != null - ) { - // if we're currently focused on the search input, we don't want to navigate - return; - } - - switch (event.which) { - case 78: // n - case 74: // j - goToNext(); - break; - case 66: // b - case 75: // k - case 80: // p - goToPrevious(); - break; - } - }; -})(); -window.addEventListener('keydown', jumpToCode); diff --git a/coverage/content.js.html b/coverage/content.js.html deleted file mode 100644 index 8519402..0000000 --- a/coverage/content.js.html +++ /dev/null @@ -1,2893 +0,0 @@ - - - - - - Code coverage report for content.js - - - - - - - - - -
-
-

All files content.js

-
- -
- 0% - Statements - 0/371 -
- - -
- 0% - Branches - 0/214 -
- - -
- 0% - Functions - 0/44 -
- - -
- 0% - Lines - 0/356 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255 -256 -257 -258 -259 -260 -261 -262 -263 -264 -265 -266 -267 -268 -269 -270 -271 -272 -273 -274 -275 -276 -277 -278 -279 -280 -281 -282 -283 -284 -285 -286 -287 -288 -289 -290 -291 -292 -293 -294 -295 -296 -297 -298 -299 -300 -301 -302 -303 -304 -305 -306 -307 -308 -309 -310 -311 -312 -313 -314 -315 -316 -317 -318 -319 -320 -321 -322 -323 -324 -325 -326 -327 -328 -329 -330 -331 -332 -333 -334 -335 -336 -337 -338 -339 -340 -341 -342 -343 -344 -345 -346 -347 -348 -349 -350 -351 -352 -353 -354 -355 -356 -357 -358 -359 -360 -361 -362 -363 -364 -365 -366 -367 -368 -369 -370 -371 -372 -373 -374 -375 -376 -377 -378 -379 -380 -381 -382 -383 -384 -385 -386 -387 -388 -389 -390 -391 -392 -393 -394 -395 -396 -397 -398 -399 -400 -401 -402 -403 -404 -405 -406 -407 -408 -409 -410 -411 -412 -413 -414 -415 -416 -417 -418 -419 -420 -421 -422 -423 -424 -425 -426 -427 -428 -429 -430 -431 -432 -433 -434 -435 -436 -437 -438 -439 -440 -441 -442 -443 -444 -445 -446 -447 -448 -449 -450 -451 -452 -453 -454 -455 -456 -457 -458 -459 -460 -461 -462 -463 -464 -465 -466 -467 -468 -469 -470 -471 -472 -473 -474 -475 -476 -477 -478 -479 -480 -481 -482 -483 -484 -485 -486 -487 -488 -489 -490 -491 -492 -493 -494 -495 -496 -497 -498 -499 -500 -501 -502 -503 -504 -505 -506 -507 -508 -509 -510 -511 -512 -513 -514 -515 -516 -517 -518 -519 -520 -521 -522 -523 -524 -525 -526 -527 -528 -529 -530 -531 -532 -533 -534 -535 -536 -537 -538 -539 -540 -541 -542 -543 -544 -545 -546 -547 -548 -549 -550 -551 -552 -553 -554 -555 -556 -557 -558 -559 -560 -561 -562 -563 -564 -565 -566 -567 -568 -569 -570 -571 -572 -573 -574 -575 -576 -577 -578 -579 -580 -581 -582 -583 -584 -585 -586 -587 -588 -589 -590 -591 -592 -593 -594 -595 -596 -597 -598 -599 -600 -601 -602 -603 -604 -605 -606 -607 -608 -609 -610 -611 -612 -613 -614 -615 -616 -617 -618 -619 -620 -621 -622 -623 -624 -625 -626 -627 -628 -629 -630 -631 -632 -633 -634 -635 -636 -637 -638 -639 -640 -641 -642 -643 -644 -645 -646 -647 -648 -649 -650 -651 -652 -653 -654 -655 -656 -657 -658 -659 -660 -661 -662 -663 -664 -665 -666 -667 -668 -669 -670 -671 -672 -673 -674 -675 -676 -677 -678 -679 -680 -681 -682 -683 -684 -685 -686 -687 -688 -689 -690 -691 -692 -693 -694 -695 -696 -697 -698 -699 -700 -701 -702 -703 -704 -705 -706 -707 -708 -709 -710 -711 -712 -713 -714 -715 -716 -717 -718 -719 -720 -721 -722 -723 -724 -725 -726 -727 -728 -729 -730 -731 -732 -733 -734 -735 -736 -737 -738 -739 -740 -741 -742 -743 -744 -745 -746 -747 -748 -749 -750 -751 -752 -753 -754 -755 -756 -757 -758 -759 -760 -761 -762 -763 -764 -765 -766 -767 -768 -769 -770 -771 -772 -773 -774 -775 -776 -777 -778 -779 -780 -781 -782 -783 -784 -785 -786 -787 -788 -789 -790 -791 -792 -793 -794 -795 -796 -797 -798 -799 -800 -801 -802 -803 -804 -805 -806 -807 -808 -809 -810 -811 -812 -813 -814 -815 -816 -817 -818 -819 -820 -821 -822 -823 -824 -825 -826 -827 -828 -829 -830 -831 -832 -833 -834 -835 -836 -837 -838 -839 -840 -841 -842 -843 -844 -845 -846 -847 -848 -849 -850 -851 -852 -853 -854 -855 -856 -857 -858 -859 -860 -861 -862 -863 -864 -865 -866 -867 -868 -869 -870 -871 -872 -873 -874 -875 -876 -877 -878 -879 -880 -881 -882 -883 -884 -885 -886 -887 -888 -889 -890 -891 -892 -893 -894 -895 -896 -897 -898 -899 -900 -901 -902 -903 -904 -905 -906 -907 -908 -909 -910 -911 -912 -913 -914 -915 -916 -917 -918 -919 -920 -921 -922 -923 -924 -925 -926 -927 -928 -929 -930 -931 -932 -933 -934 -935 -936 -937  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
// Content script for Full Screenshot Selector Chrome Extension
-class ScreenshotSelector {
-  constructor() {
-    this.isActive = false;
-    this.currentHighlight = null;
-    this.overlay = null;
-    this.style = null;
-    this.background = 'black';
-    this.copyToClipboard = true; // Default to true
-  }
- 
-  async init(background = 'black', copyToClipboard = true) {
-    if (this.isActive) {
-      console.log('Screenshot selector already active');
-      return;
-    }
- 
-    this.background = background;
-    this.copyToClipboard = copyToClipboard;
-    this.isActive = true;
- 
-    this.createUI();
-    this.addEventListeners();
-  }
- 
-  createUI() {
-    // Create overlay UI
-    this.overlay = document.createElement('div');
-    this.overlay.id = 'screenshot-selector-overlay';
-    this.overlay.innerHTML = `
-      <div style="position: fixed; top: 20px; left: 20px; background: rgba(0,0,0,0.9); color: white; padding: 12px 16px; border-radius: 8px; z-index: 2147483647; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 14px; box-shadow: 0 4px 20px rgba(0,0,0,0.3); backdrop-filter: blur(10px);">
-        <div style="display: flex; align-items: center; gap: 10px;">
-          <span>📸</span>
-          <span>Hover over elements and click to capture</span>
-          <button id="screenshot-cancel" style="margin-left: 10px; padding: 4px 8px; background: #ff4757; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px;">ESC</button>
-        </div>
-      </div>
-    `;
- 
-    // Add hover highlighting styles
-    this.style = document.createElement('style');
-    this.style.textContent = `
-      .screenshot-highlight {
-        outline: 3px solid #00d2ff !important;
-        outline-offset: 2px !important;
-        cursor: crosshair !important;
-        position: relative !important;
-      }
-      .screenshot-highlight::after {
-        content: '';
-        position: absolute;
-        top: -5px;
-        left: -5px;
-        right: -5px;
-        bottom: -5px;
-        background: rgba(0, 210, 255, 0.1) !important;
-        pointer-events: none !important;
-        z-index: 2147483646 !important;
-      }
-      .screenshot-scrollable-indicator {
-        position: absolute !important;
-        border: 2px dotted #ff6b6b !important;
-        background: rgba(255, 107, 107, 0.05) !important;
-        pointer-events: none !important;
-        z-index: 2147483645 !important;
-      }
-      #screenshot-selector-overlay * {
-        cursor: default !important;
-      }
-    `;
- 
-    document.head.appendChild(this.style);
-    document.body.appendChild(this.overlay);
- 
-    // Cancel button functionality
-    document.getElementById('screenshot-cancel').onclick = () => this.cleanup();
-  }
- 
-  addEventListeners() {
-    this.handleMouseOver = this.handleMouseOver.bind(this);
-    this.handleClick = this.handleClick.bind(this);
-    this.handleKeyPress = this.handleKeyPress.bind(this);
- 
-    document.addEventListener('mouseover', this.handleMouseOver);
-    document.addEventListener('click', this.handleClick, true);
-    document.addEventListener('keydown', this.handleKeyPress);
-  }
- 
-  handleMouseOver(e) {
-    if (e.target.closest('#screenshot-selector-overlay')) return;
- 
-    // Clean up previous highlights and indicators
-    if (this.currentHighlight) {
-      this.currentHighlight.classList.remove('screenshot-highlight');
-      this.removeScrollableIndicators();
-    }
- 
-    this.currentHighlight = e.target;
-    e.target.classList.add('screenshot-highlight');
- 
-    // Add scrollable content indicators
-    this.addScrollableIndicators(e.target);
-  }
- 
-  handleClick(e) {
-    if (e.target.closest('#screenshot-selector-overlay')) return;
-    e.preventDefault();
-    e.stopPropagation();
- 
-    const element = e.target;
-    this.captureElement(element);
-  }
- 
-  handleKeyPress(e) {
-    if (e.key === 'Escape') {
-      this.cleanup();
-    }
-  }
- 
-  addScrollableIndicators(element) {
-    const rect = element.getBoundingClientRect();
-    const hasVerticalScroll = element.scrollHeight > element.clientHeight;
-    const hasHorizontalScroll = element.scrollWidth > element.clientWidth;
- 
-    if (!hasVerticalScroll && !hasHorizontalScroll) return;
- 
-    // Calculate the full content dimensions
-    const fullHeight = element.scrollHeight;
-    const fullWidth = element.scrollWidth;
-    const visibleHeight = element.clientHeight;
-    const visibleWidth = element.clientWidth;
- 
-    // Create indicator for full scrollable area
-    if (hasVerticalScroll || hasHorizontalScroll) {
-      const indicator = document.createElement('div');
-      indicator.className = 'screenshot-scrollable-indicator';
-      indicator.setAttribute('data-screenshot-indicator', 'true');
- 
-      // Position it relative to the viewport
-      const style = {
-        left: `${rect.left}px`,
-        top: `${rect.top}px`,
-        width: hasHorizontalScroll ? `${fullWidth}px` : `${rect.width}px`,
-        height: hasVerticalScroll ? `${fullHeight}px` : `${rect.height}px`,
-      };
- 
-      Object.assign(indicator.style, style);
-      document.body.appendChild(indicator);
- 
-      // Add a label to show the full dimensions
-      const label = document.createElement('div');
-      label.setAttribute('data-screenshot-indicator', 'true');
-      label.style.cssText = `
-        position: fixed;
-        top: ${rect.top - 25}px;
-        left: ${rect.left}px;
-        background: rgba(255, 107, 107, 0.9);
-        color: white;
-        padding: 2px 6px;
-        border-radius: 3px;
-        font-size: 11px;
-        font-family: monospace;
-        z-index: 2147483647;
-        pointer-events: none;
-      `;
- 
-      const scrollInfo = [];
-      if (hasVerticalScroll) scrollInfo.push(`H: ${fullHeight}px (visible: ${visibleHeight}px)`);
-      if (hasHorizontalScroll) scrollInfo.push(`W: ${fullWidth}px (visible: ${visibleWidth}px)`);
-      label.textContent = `Full content - ${scrollInfo.join(', ')}`;
- 
-      document.body.appendChild(label);
-    }
-  }
- 
-  removeScrollableIndicators() {
-    const indicators = document.querySelectorAll('[data-screenshot-indicator]');
-    indicators.forEach(indicator => indicator.remove());
-  }
- 
-  // Helper method to generate a unique selector for an element
-  getElementSelector(element) {
-    // Escape helper for CSS identifiers (handles Tailwind classes like lg:pr-0)
-    const escapeIdent = (ident) => {
-      try {
-        if (window.CSS && typeof window.CSS.escape === 'function') {
-          return window.CSS.escape(ident);
-        }
-      } catch (_) {}
-      // Fallback: escape most punctuation characters
-      return String(ident).replace(/([ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, '\\$1');
-    };
- 
-    if (element.id) {
-      return `#${escapeIdent(element.id)}`;
-    }
- 
-    if (element.classList && element.classList.length) {
-      const classSelector = Array.from(element.classList)
-        .filter(Boolean)
-        .map(cls => `.${escapeIdent(cls)}`)
-        .join('');
-      if (classSelector) {
-        return `${element.tagName.toLowerCase()}${classSelector}`;
-      }
-    }
- 
-    // Fallback to tag name and position
-    const siblings = Array.from(element.parentNode?.children || []);
-    const index = siblings.indexOf(element);
-    return `${element.tagName.toLowerCase()}:nth-child(${index + 1})`;
-  }
- 
-    // Preserve computed styles for better rendering
-  preserveComputedStyles(clonedElement, originalElement) {
-    if (!originalElement || !clonedElement) return;
- 
-    const computedStyle = window.getComputedStyle(originalElement);
- 
-    // Comprehensive style properties including layout and icon-related ones
-    const importantStyles = [
-      // Typography and fonts
-      'font-family', 'font-size', 'font-weight', 'font-style', 'font-variant',
-      'text-align', 'text-decoration', 'text-transform', 'text-indent',
-      'line-height', 'letter-spacing', 'word-spacing', 'white-space',
- 
-      // Colors and backgrounds
-      'color', 'background-color', 'background-image', 'background-size',
-      'background-position', 'background-repeat', 'background-attachment',
- 
-      // Layout and positioning
-      'display', 'position', 'top', 'left', 'right', 'bottom',
-      'width', 'height', 'min-width', 'min-height', 'max-width', 'max-height',
-      'margin', 'padding', 'border', 'border-radius',
-      'float', 'clear', 'vertical-align',
- 
-      // Flexbox and Grid
-      'flex', 'flex-direction', 'flex-wrap', 'flex-basis', 'flex-grow', 'flex-shrink',
-      'justify-content', 'align-items', 'align-self', 'align-content',
-      'grid', 'grid-template', 'grid-area', 'grid-column', 'grid-row',
- 
-      // Visual effects
-      'opacity', 'visibility', 'overflow', 'overflow-x', 'overflow-y',
-      'box-shadow', 'text-shadow', 'transform', 'filter',
-      'z-index', 'cursor'
-    ];
- 
-    // Apply computed styles
-    importantStyles.forEach(property => {
-      const value = computedStyle.getPropertyValue(property);
-      if (value && value !== 'none' && value !== 'normal' && value !== 'auto' && value !== 'initial') {
-        try {
-          clonedElement.style.setProperty(property, value, 'important');
-        } catch (e) {
-          // Skip properties that can't be set
-        }
-      }
-    });
- 
-    // Handle pseudo-elements (::before and ::after) which often contain icons
-    this.preservePseudoElements(clonedElement, originalElement);
- 
-    // Recursively apply to children
-    const originalChildren = Array.from(originalElement.children);
-    const clonedChildren = Array.from(clonedElement.children);
- 
-    for (let i = 0; i < Math.min(originalChildren.length, clonedChildren.length); i++) {
-      this.preserveComputedStyles(clonedChildren[i], originalChildren[i]);
-    }
-  }
- 
-     // Handle pseudo-elements that often contain icons
-   preservePseudoElements(clonedElement, originalElement) {
-     try {
-       ['::before', '::after'].forEach(pseudo => {
-         const pseudoStyle = window.getComputedStyle(originalElement, pseudo);
-         const content = pseudoStyle.getPropertyValue('content');
- 
-         // If pseudo-element has content, try to preserve it
-         if (content && content !== 'none' && content !== '""') {
-           const pseudoProperties = [
-             'content', 'display', 'position', 'top', 'left', 'right', 'bottom',
-             'width', 'height', 'font-family', 'font-size', 'color',
-             'background-color', 'background-image', 'border', 'border-radius',
-             'transform', 'opacity'
-           ];
- 
-           // Create inline style for pseudo-element
-           let selector = this.getElementSelector(clonedElement);
-           let pseudoCSS = `${selector}${pseudo} {`;
-           pseudoProperties.forEach(prop => {
-             const value = pseudoStyle.getPropertyValue(prop);
-             if (value && value !== 'none' && value !== 'normal') {
-               pseudoCSS += `${prop}: ${value} !important;`;
-             }
-           });
-           pseudoCSS += '}';
- 
-           // Add to document head
-           const style = document.createElement('style');
-           style.textContent = pseudoCSS;
-           document.head.appendChild(style);
-         }
-       });
-     } catch (e) {
-       // Pseudo-element handling failed, continue without it
-     }
-   }
- 
-   // Wait for fonts and resources to load properly
-   async waitForFontsAndResources(element) {
-     // Wait for document fonts to load
-     if (document.fonts && document.fonts.ready) {
-       try {
-         await document.fonts.ready;
-       } catch (e) {
-         // Font loading API not available or failed
-       }
-     }
- 
-     // Check for icon fonts (Font Awesome, Material Icons, etc.)
-     const iconFontFamilies = [
-       'FontAwesome', 'Font Awesome', 'Font Awesome 5', 'Font Awesome 6',
-       'Material Icons', 'Material Icons Outlined', 'Material Icons Sharp',
-       'Ionicons', 'Feather', 'Lucide', 'Tabler Icons'
-     ];
- 
-     // Force load any icon fonts found in the element
-     const allElements = [element, ...element.querySelectorAll('*')];
-     const fontPromises = [];
- 
-     allElements.forEach(el => {
-       const computedStyle = window.getComputedStyle(el);
-       const fontFamily = computedStyle.fontFamily;
- 
-       iconFontFamilies.forEach(iconFont => {
-         if (fontFamily.includes(iconFont)) {
-           // Create a test element to ensure font is loaded
-           const testEl = document.createElement('span');
-           testEl.style.fontFamily = iconFont;
-           testEl.style.position = 'absolute';
-           testEl.style.left = '-9999px';
-           testEl.textContent = '■'; // Use a test character
-           document.body.appendChild(testEl);
- 
-           const fontPromise = new Promise(resolve => {
-             setTimeout(() => {
-               document.body.removeChild(testEl);
-               resolve();
-             }, 100);
-           });
-           fontPromises.push(fontPromise);
-         }
-       });
-     });
- 
-     // Wait for all font loading attempts
-     await Promise.all(fontPromises);
- 
-         // Additional wait for any remaining resources
-    await new Promise(resolve => setTimeout(resolve, 500));
-  }
- 
- 
- 
-  async captureElement(element) {
-    try {
-      // Determine loading message based on clipboard settings
-      let loadingMessage = 'Capturing screenshot...';
-      if (this.copyToClipboard) {
-        const availability = this.checkClipboardAPIAvailability();
-        if (availability.available) {
-          loadingMessage = 'Capturing screenshot and preparing for clipboard...';
-        } else {
-          loadingMessage = 'Capturing screenshot... (Clipboard not available)';
-        }
-      }
- 
-      // Show loading state
-      this.overlay.innerHTML = `
-        <div style="position: fixed; top: 20px; left: 20px; background: rgba(0,0,0,0.9); color: white; padding: 12px 16px; border-radius: 8px; z-index: 2147483647; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 14px; box-shadow: 0 4px 20px rgba(0,0,0,0.3);">
-          <div style="display: flex; align-items: center; gap: 10px;">
-            <div style="width: 16px; height: 16px; border: 2px solid #fff; border-top: 2px solid transparent; border-radius: 50%; animation: spin 1s linear infinite;"></div>
-            <span>${loadingMessage}</span>
-          </div>
-        </div>
-        <style>
-          @keyframes spin {
-            0% { transform: rotate(0deg); }
-            100% { transform: rotate(360deg); }
-          }
-        </style>
-      `;
- 
-      // Remove highlight for clean capture first
-      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);
- 
-            // Enhanced html2canvas configuration
-      const canvas = await html2canvas(element, {
-        useCORS: true,
-        allowTaint: false,
-        backgroundColor: this.background === 'transparent' ? null :
-                        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
- 
-        // 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);
- 
-      // Always download the image first (core functionality)
-      const link = 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 (after successful canvas generation and download)
-      let clipboardResult = null;
-      if (this.copyToClipboard) {
-        const availability = this.checkClipboardAPIAvailability();
-        if (availability.available) {
-          try {
-            // Attempt clipboard operation with comprehensive error handling
-            clipboardResult = await this.copyCanvasToClipboard(canvas);
-          } catch (error) {
-            // Ensure clipboard errors don't break the workflow - this is a safety net
-            // The copyCanvasToClipboard method should handle all errors internally
-            console.error('Unexpected clipboard error caught in captureElement:', error);
-            clipboardResult = { 
-              success: false, 
-              error: 'Unexpected clipboard error occurred',
-              errorType: 'unexpected'
-            };
-          }
-        } else {
-          clipboardResult = { 
-            success: false, 
-            error: availability.reason,
-            errorType: 'api_unavailable'
-          };
-        }
-      }
- 
-      // Generate user feedback message based on clipboard results
-      let successMessage = 'Screenshot downloaded successfully!';
-      let messageColor = 'rgba(39, 174, 96, 0.95)'; // Green for success
-      
-      if (this.copyToClipboard) {
-        if (clipboardResult && clipboardResult.success) {
-          successMessage = 'Screenshot downloaded and copied to clipboard!';
-          // Keep green color for full success
-        } else {
-          // Download succeeded but clipboard failed - provide specific error feedback
-          const errorMessage = this.getClipboardErrorMessage(clipboardResult);
-          successMessage = `Screenshot downloaded successfully! ${errorMessage}`;
-          messageColor = 'rgba(243, 156, 18, 0.95)'; // Orange for partial success
-        }
-      }
- 
-      this.overlay.innerHTML = `
-        <div style="position: fixed; top: 20px; left: 20px; background: ${messageColor}; color: white; padding: 12px 16px; border-radius: 8px; z-index: 2147483647; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 14px; box-shadow: 0 4px 20px rgba(0,0,0,0.3);">
-          <div style="display: flex; align-items: center; gap: 10px;">
-            <span>✅</span>
-            <span>${successMessage}</span>
-          </div>
-        </div>
-      `;
- 
-      // Notify popup
-      chrome.runtime.sendMessage({ action: 'selectorStopped', reason: 'screenshot-taken' });
- 
-      setTimeout(() => this.cleanup(), 2000);
- 
-    } catch (error) {
-      console.error('Screenshot failed:', error);
-      
-      // Provide specific error messages for screenshot failures
-      let errorMessage = 'Screenshot capture failed. Please try again.';
-      
-      if (error.message) {
-        if (error.message.includes('html2canvas')) {
-          errorMessage = 'Screenshot rendering failed. Try selecting a different element or refresh the page.';
-        } else if (error.message.includes('timeout')) {
-          errorMessage = 'Screenshot capture timed out. Try selecting a smaller area or simpler element.';
-        } else if (error.message.includes('network') || error.message.includes('CORS')) {
-          errorMessage = 'Screenshot failed: Some content is blocked by security restrictions.';
-        } else if (error.message.includes('memory') || error.message.includes('quota')) {
-          errorMessage = 'Screenshot failed: Not enough memory. Try capturing a smaller area.';
-        } else if (error.message.includes('canvas')) {
-          errorMessage = 'Screenshot failed: Unable to create image. Try a different element.';
-        }
-      }
-      
-      this.overlay.innerHTML = `
-        <div style="position: fixed; top: 20px; left: 20px; background: rgba(231, 76, 60, 0.95); color: white; padding: 12px 16px; border-radius: 8px; z-index: 2147483647; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 14px; box-shadow: 0 4px 20px rgba(0,0,0,0.3);">
-          <div style="display: flex; align-items: center; gap: 10px;">
-            <span>❌</span>
-            <span>${errorMessage}</span>
-          </div>
-        </div>
-      `;
-      
-      // Notify popup about the failure
-      chrome.runtime.sendMessage({ action: 'selectorStopped', reason: 'screenshot-failed' });
-      
-      setTimeout(() => this.cleanup(), 3000);
-    }
-  }
- 
-  /**
-   * Check if Clipboard API is available and supported
-   * @returns {{available: boolean, reason?: string}} Clipboard API availability status
-   */
-  checkClipboardAPIAvailability() {
-    // Check for basic Clipboard API support
-    if (!navigator.clipboard) {
-      return {
-        available: false,
-        reason: 'Clipboard API not available in this browser'
-      };
-    }
- 
-    // Check for ClipboardItem constructor (required for image data)
-    if (!window.ClipboardItem) {
-      return {
-        available: false,
-        reason: 'ClipboardItem not supported in this browser'
-      };
-    }
- 
-    // Check if we're in a secure context (HTTPS required for clipboard)
-    if (!window.isSecureContext) {
-      return {
-        available: false,
-        reason: 'Clipboard API requires a secure context (HTTPS)'
-      };
-    }
- 
-    // Check for write permission (this is the basic check, actual permission is checked during write)
-    if (!navigator.clipboard.write) {
-      return {
-        available: false,
-        reason: 'Clipboard write functionality not available'
-      };
-    }
- 
-    return {
-      available: true
-    };
-  }
- 
-  /**
-   * Get user-friendly feedback message about clipboard feature availability
-   * @returns {{supported: boolean, message: string}} User feedback about clipboard support
-   */
-  getClipboardSupportFeedback() {
-    const availability = this.checkClipboardAPIAvailability();
-    
-    if (availability.available) {
-      return {
-        supported: true,
-        message: 'Clipboard copying is available and ready to use.'
-      };
-    }
- 
-    // Provide user-friendly messages for different scenarios
-    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
-    };
-  }
- 
-  /**
-   * Generate user-friendly error message for clipboard operation failures
-   * @param {Object} clipboardResult - Result object from clipboard operation
-   * @returns {string} User-friendly error message
-   */
-  getClipboardErrorMessage(clipboardResult) {
-    if (!clipboardResult || !clipboardResult.error) {
-      return 'Clipboard copy failed due to unknown error.';
-    }
- 
-    const errorType = clipboardResult.errorType;
-    const errorMessage = clipboardResult.error;
- 
-    // Provide contextual error messages based on error type
-    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.';
-      
-      case 'network_error':
-        return 'Clipboard copy failed due to network error. Please try again.';
-      
-      case 'canvas_conversion':
-        return 'Clipboard copy failed: Unable to prepare image. Try capturing a different element.';
-      
-      case 'clipboard_item_creation':
-        return 'Clipboard copy not supported: Your browser doesn\'t support image clipboard operations.';
-      
-      case 'invalid_state':
-        return 'Clipboard copy failed: Browser clipboard is busy. Please try again.';
-      
-      case 'data_error':
-        return 'Clipboard copy failed: Invalid image data. Please try capturing again.';
-      
-      case 'unexpected':
-        return 'Clipboard copy failed due to unexpected error. Screenshot was still downloaded.';
-      
-      default:
-        // For unknown error types, try to extract meaningful info from the error message
-        if (errorMessage.includes('permission') || errorMessage.includes('denied')) {
-          return 'Clipboard copy failed: Access denied. Check browser permissions.';
-        } else if (errorMessage.includes('not supported') || errorMessage.includes('unavailable')) {
-          return 'Clipboard copy not supported in this browser.';
-        } else if (errorMessage.includes('timeout') || errorMessage.includes('timed out')) {
-          return 'Clipboard copy timed out. Try capturing a smaller area.';
-        } else if (errorMessage.includes('large') || errorMessage.includes('size')) {
-          return 'Clipboard copy failed: Image too large for clipboard.';
-        } else {
-          return `Clipboard copy failed: ${errorMessage}`;
-        }
-    }
-  }
- 
-  /**
-   * Copy canvas content to clipboard using the Clipboard API
-   * @param {HTMLCanvasElement} canvas - The canvas element to copy
-   * @returns {Promise<{success: boolean, error?: string, errorType?: string}>} Result of clipboard operation
-   */
-  async copyCanvasToClipboard(canvas) {
-    try {
-      // Check clipboard API availability first
-      const availability = this.checkClipboardAPIAvailability();
-      if (!availability.available) {
-        return { 
-          success: false, 
-          error: availability.reason,
-          errorType: 'api_unavailable'
-        };
-      }
- 
-      // Convert canvas to blob in PNG format with timeout
-      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)
-        )
-      ]);
- 
-      // Validate blob size (prevent extremely large clipboard operations)
-      const maxBlobSize = 50 * 1024 * 1024; // 50MB limit
-      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'
-        };
-      }
- 
-      // Create ClipboardItem with PNG image data
-      let clipboardItem;
-      try {
-        clipboardItem = new 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'
-        };
-      }
- 
-      // Write to clipboard with timeout
-      await Promise.race([
-        navigator.clipboard.write([clipboardItem]),
-        new Promise((_, reject) => 
-          setTimeout(() => reject(new Error('Clipboard write operation timed out')), 15000)
-        )
-      ]);
- 
-      return { success: true };
- 
-    } catch (error) {
-      console.error('Clipboard operation failed:', error);
-      
-      // Comprehensive error handling with specific error types and user-friendly messages
-      let errorMessage = error.message || 'Unknown clipboard error';
-      let errorType = 'unknown';
-      
-      // Permission-related errors
-      if (error.name === 'NotAllowedError') {
-        errorMessage = 'Clipboard access denied. Please allow clipboard permissions in your browser settings.';
-        errorType = 'permission_denied';
-      } 
-      // API support errors
-      else if (error.name === 'NotSupportedError') {
-        errorMessage = 'Clipboard API not supported in this browser context.';
-        errorType = 'not_supported';
-      }
-      // Security context errors
-      else if (error.name === 'SecurityError') {
-        errorMessage = 'Clipboard access blocked by security policy. Try using HTTPS.';
-        errorType = 'security_error';
-      }
-      // Network or resource errors
-      else if (error.name === 'NetworkError') {
-        errorMessage = 'Network error during clipboard operation. Please try again.';
-        errorType = 'network_error';
-      }
-      // Timeout errors
-      else if (error.message.includes('timed out')) {
-        errorMessage = 'Clipboard operation timed out. The image may be too large.';
-        errorType = 'timeout';
-      }
-      // Canvas conversion errors
-      else if (error.message.includes('canvas') || error.message.includes('blob')) {
-        errorMessage = 'Failed to prepare image for clipboard. Try capturing a different area.';
-        errorType = 'canvas_conversion';
-      }
-      // Quota or storage errors
-      else if (error.name === 'QuotaExceededError' || error.message.includes('quota')) {
-        errorMessage = 'Clipboard storage quota exceeded. Try capturing a smaller image.';
-        errorType = 'quota_exceeded';
-      }
-      // DOM or state errors
-      else if (error.name === 'InvalidStateError') {
-        errorMessage = 'Clipboard is in an invalid state. Please try again.';
-        errorType = 'invalid_state';
-      }
-      // Data errors
-      else if (error.name === 'DataError') {
-        errorMessage = 'Invalid image data for clipboard. Please try again.';
-        errorType = 'data_error';
-      }
-      // Generic fallback for unknown errors
-      else {
-        errorMessage = `Clipboard operation failed: ${error.message}`;
-        errorType = 'unknown';
-      }
- 
-      return { 
-        success: false, 
-        error: errorMessage,
-        errorType: errorType
-      };
-    }
-  }
- 
-  cleanup() {
-    document.removeEventListener('mouseover', this.handleMouseOver);
-    document.removeEventListener('click', this.handleClick, true);
-    document.removeEventListener('keydown', this.handleKeyPress);
- 
-    if (this.currentHighlight) {
-      this.currentHighlight.classList.remove('screenshot-highlight');
-    }
- 
-    // Remove any scrollable indicators
-    this.removeScrollableIndicators();
- 
-    if (this.overlay) {
-      this.overlay.remove();
-      this.overlay = null;
-    }
- 
-    if (this.style) {
-      this.style.remove();
-      this.style = null;
-    }
- 
-    this.isActive = false;
-    this.currentHighlight = null;
-  }
-}
- 
-// Global instance
-let screenshotSelector = new ScreenshotSelector();
- 
-// Add load indicator
-console.log('Full Screenshot Selector content script loaded');
- 
-// Listen for messages from popup
-chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
-  try {
-  switch (message.action) {
-    case 'startSelector':
-      // Extract clipboard preference from message, default to true for backward compatibility
-      const copyToClipboard = message.copyToClipboard !== undefined ? message.copyToClipboard : true;
-      screenshotSelector.init(message.background, copyToClipboard);
-      sendResponse({ success: true });
-      break;
- 
-    case 'stopSelector':
-      screenshotSelector.cleanup();
-      sendResponse({ success: true });
-      break;
- 
-    case 'checkState':
-      sendResponse({ isActive: screenshotSelector.isActive });
-      break;
- 
-    case 'checkClipboardSupport':
-      const availability = screenshotSelector.checkClipboardAPIAvailability();
-      const feedback = screenshotSelector.getClipboardSupportFeedback();
-      sendResponse({ 
-        availability: availability,
-        feedback: feedback
-      });
-      break;
- 
-      default:
-        sendResponse({ error: 'Unknown action: ' + message.action });
-    }
-  } catch (error) {
-    console.error('Content script error:', error);
-    sendResponse({ error: error.message });
-  }
- 
-  // Important: return true to indicate async response
-  return true;
-});
- 
-// Handle keyboard shortcut
-chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
-  if (message.action === 'activateFromShortcut') {
-    if (!screenshotSelector.isActive) {
-      screenshotSelector.init('black', true); // Default background and clipboard enabled for shortcut
-    }
-  }
-});
- -
-
- - - - - - - - \ No newline at end of file diff --git a/coverage/favicon.png b/coverage/favicon.png deleted file mode 100644 index c1525b811a167671e9de1fa78aab9f5c0b61cef7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 445 zcmV;u0Yd(XP))rP{nL}Ln%S7`m{0DjX9TLF* zFCb$4Oi7vyLOydb!7n&^ItCzb-%BoB`=x@N2jll2Nj`kauio%aw_@fe&*}LqlFT43 z8doAAe))z_%=P%v^@JHp3Hjhj^6*Kr_h|g_Gr?ZAa&y>wxHE99Gk>A)2MplWz2xdG zy8VD2J|Uf#EAw*bo5O*PO_}X2Tob{%bUoO2G~T`@%S6qPyc}VkhV}UifBuRk>%5v( z)x7B{I~z*k<7dv#5tC+m{km(D087J4O%+<<;K|qwefb6@GSX45wCK}Sn*> - - - - Code coverage report for All files - - - - - - - - - -
-
-

All files

-
- -
- 0% - Statements - 0/463 -
- - -
- 0% - Branches - 0/263 -
- - -
- 0% - Functions - 0/61 -
- - -
- 0% - Lines - 0/445 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FileStatementsBranchesFunctionsLines
content.js -
-
0%0/3710%0/2140%0/440%0/356
popup.js -
-
0%0/920%0/490%0/170%0/89
-
-
-
- - - - - - - - \ No newline at end of file diff --git a/coverage/lcov-report/base.css b/coverage/lcov-report/base.css deleted file mode 100644 index f418035..0000000 --- a/coverage/lcov-report/base.css +++ /dev/null @@ -1,224 +0,0 @@ -body, html { - margin:0; padding: 0; - height: 100%; -} -body { - font-family: Helvetica Neue, Helvetica, Arial; - font-size: 14px; - color:#333; -} -.small { font-size: 12px; } -*, *:after, *:before { - -webkit-box-sizing:border-box; - -moz-box-sizing:border-box; - box-sizing:border-box; - } -h1 { font-size: 20px; margin: 0;} -h2 { font-size: 14px; } -pre { - font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace; - margin: 0; - padding: 0; - -moz-tab-size: 2; - -o-tab-size: 2; - tab-size: 2; -} -a { color:#0074D9; text-decoration:none; } -a:hover { text-decoration:underline; } -.strong { font-weight: bold; } -.space-top1 { padding: 10px 0 0 0; } -.pad2y { padding: 20px 0; } -.pad1y { padding: 10px 0; } -.pad2x { padding: 0 20px; } -.pad2 { padding: 20px; } -.pad1 { padding: 10px; } -.space-left2 { padding-left:55px; } -.space-right2 { padding-right:20px; } -.center { text-align:center; } -.clearfix { display:block; } -.clearfix:after { - content:''; - display:block; - height:0; - clear:both; - visibility:hidden; - } -.fl { float: left; } -@media only screen and (max-width:640px) { - .col3 { width:100%; max-width:100%; } - .hide-mobile { display:none!important; } -} - -.quiet { - color: #7f7f7f; - color: rgba(0,0,0,0.5); -} -.quiet a { opacity: 0.7; } - -.fraction { - font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; - font-size: 10px; - color: #555; - background: #E8E8E8; - padding: 4px 5px; - border-radius: 3px; - vertical-align: middle; -} - -div.path a:link, div.path a:visited { color: #333; } -table.coverage { - border-collapse: collapse; - margin: 10px 0 0 0; - padding: 0; -} - -table.coverage td { - margin: 0; - padding: 0; - vertical-align: top; -} -table.coverage td.line-count { - text-align: right; - padding: 0 5px 0 20px; -} -table.coverage td.line-coverage { - text-align: right; - padding-right: 10px; - min-width:20px; -} - -table.coverage td span.cline-any { - display: inline-block; - padding: 0 5px; - width: 100%; -} -.missing-if-branch { - display: inline-block; - margin-right: 5px; - border-radius: 3px; - position: relative; - padding: 0 4px; - background: #333; - color: yellow; -} - -.skip-if-branch { - display: none; - margin-right: 10px; - position: relative; - padding: 0 4px; - background: #ccc; - color: white; -} -.missing-if-branch .typ, .skip-if-branch .typ { - color: inherit !important; -} -.coverage-summary { - border-collapse: collapse; - width: 100%; -} -.coverage-summary tr { border-bottom: 1px solid #bbb; } -.keyline-all { border: 1px solid #ddd; } -.coverage-summary td, .coverage-summary th { padding: 10px; } -.coverage-summary tbody { border: 1px solid #bbb; } -.coverage-summary td { border-right: 1px solid #bbb; } -.coverage-summary td:last-child { border-right: none; } -.coverage-summary th { - text-align: left; - font-weight: normal; - white-space: nowrap; -} -.coverage-summary th.file { border-right: none !important; } -.coverage-summary th.pct { } -.coverage-summary th.pic, -.coverage-summary th.abs, -.coverage-summary td.pct, -.coverage-summary td.abs { text-align: right; } -.coverage-summary td.file { white-space: nowrap; } -.coverage-summary td.pic { min-width: 120px !important; } -.coverage-summary tfoot td { } - -.coverage-summary .sorter { - height: 10px; - width: 7px; - display: inline-block; - margin-left: 0.5em; - background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; -} -.coverage-summary .sorted .sorter { - background-position: 0 -20px; -} -.coverage-summary .sorted-desc .sorter { - background-position: 0 -10px; -} -.status-line { height: 10px; } -/* yellow */ -.cbranch-no { background: yellow !important; color: #111; } -/* dark red */ -.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 } -.low .chart { border:1px solid #C21F39 } -.highlighted, -.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{ - background: #C21F39 !important; -} -/* medium red */ -.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE } -/* light red */ -.low, .cline-no { background:#FCE1E5 } -/* light green */ -.high, .cline-yes { background:rgb(230,245,208) } -/* medium green */ -.cstat-yes { background:rgb(161,215,106) } -/* dark green */ -.status-line.high, .high .cover-fill { background:rgb(77,146,33) } -.high .chart { border:1px solid rgb(77,146,33) } -/* dark yellow (gold) */ -.status-line.medium, .medium .cover-fill { background: #f9cd0b; } -.medium .chart { border:1px solid #f9cd0b; } -/* light yellow */ -.medium { background: #fff4c2; } - -.cstat-skip { background: #ddd; color: #111; } -.fstat-skip { background: #ddd; color: #111 !important; } -.cbranch-skip { background: #ddd !important; color: #111; } - -span.cline-neutral { background: #eaeaea; } - -.coverage-summary td.empty { - opacity: .5; - padding-top: 4px; - padding-bottom: 4px; - line-height: 1; - color: #888; -} - -.cover-fill, .cover-empty { - display:inline-block; - height: 12px; -} -.chart { - line-height: 0; -} -.cover-empty { - background: white; -} -.cover-full { - border-right: none !important; -} -pre.prettyprint { - border: none !important; - padding: 0 !important; - margin: 0 !important; -} -.com { color: #999 !important; } -.ignore-none { color: #999; font-weight: normal; } - -.wrapper { - min-height: 100%; - height: auto !important; - height: 100%; - margin: 0 auto -48px; -} -.footer, .push { - height: 48px; -} diff --git a/coverage/lcov-report/block-navigation.js b/coverage/lcov-report/block-navigation.js deleted file mode 100644 index cc12130..0000000 --- a/coverage/lcov-report/block-navigation.js +++ /dev/null @@ -1,87 +0,0 @@ -/* eslint-disable */ -var jumpToCode = (function init() { - // Classes of code we would like to highlight in the file view - var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no']; - - // Elements to highlight in the file listing view - var fileListingElements = ['td.pct.low']; - - // We don't want to select elements that are direct descendants of another match - var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > ` - - // Selecter that finds elements on the page to which we can jump - var selector = - fileListingElements.join(', ') + - ', ' + - notSelector + - missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b` - - // The NodeList of matching elements - var missingCoverageElements = document.querySelectorAll(selector); - - var currentIndex; - - function toggleClass(index) { - missingCoverageElements - .item(currentIndex) - .classList.remove('highlighted'); - missingCoverageElements.item(index).classList.add('highlighted'); - } - - function makeCurrent(index) { - toggleClass(index); - currentIndex = index; - missingCoverageElements.item(index).scrollIntoView({ - behavior: 'smooth', - block: 'center', - inline: 'center' - }); - } - - function goToPrevious() { - var nextIndex = 0; - if (typeof currentIndex !== 'number' || currentIndex === 0) { - nextIndex = missingCoverageElements.length - 1; - } else if (missingCoverageElements.length > 1) { - nextIndex = currentIndex - 1; - } - - makeCurrent(nextIndex); - } - - function goToNext() { - var nextIndex = 0; - - if ( - typeof currentIndex === 'number' && - currentIndex < missingCoverageElements.length - 1 - ) { - nextIndex = currentIndex + 1; - } - - makeCurrent(nextIndex); - } - - return function jump(event) { - if ( - document.getElementById('fileSearch') === document.activeElement && - document.activeElement != null - ) { - // if we're currently focused on the search input, we don't want to navigate - return; - } - - switch (event.which) { - case 78: // n - case 74: // j - goToNext(); - break; - case 66: // b - case 75: // k - case 80: // p - goToPrevious(); - break; - } - }; -})(); -window.addEventListener('keydown', jumpToCode); diff --git a/coverage/lcov-report/content.js.html b/coverage/lcov-report/content.js.html deleted file mode 100644 index adf9f32..0000000 --- a/coverage/lcov-report/content.js.html +++ /dev/null @@ -1,2893 +0,0 @@ - - - - - - Code coverage report for content.js - - - - - - - - - -
-
-

All files content.js

-
- -
- 0% - Statements - 0/371 -
- - -
- 0% - Branches - 0/214 -
- - -
- 0% - Functions - 0/44 -
- - -
- 0% - Lines - 0/356 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255 -256 -257 -258 -259 -260 -261 -262 -263 -264 -265 -266 -267 -268 -269 -270 -271 -272 -273 -274 -275 -276 -277 -278 -279 -280 -281 -282 -283 -284 -285 -286 -287 -288 -289 -290 -291 -292 -293 -294 -295 -296 -297 -298 -299 -300 -301 -302 -303 -304 -305 -306 -307 -308 -309 -310 -311 -312 -313 -314 -315 -316 -317 -318 -319 -320 -321 -322 -323 -324 -325 -326 -327 -328 -329 -330 -331 -332 -333 -334 -335 -336 -337 -338 -339 -340 -341 -342 -343 -344 -345 -346 -347 -348 -349 -350 -351 -352 -353 -354 -355 -356 -357 -358 -359 -360 -361 -362 -363 -364 -365 -366 -367 -368 -369 -370 -371 -372 -373 -374 -375 -376 -377 -378 -379 -380 -381 -382 -383 -384 -385 -386 -387 -388 -389 -390 -391 -392 -393 -394 -395 -396 -397 -398 -399 -400 -401 -402 -403 -404 -405 -406 -407 -408 -409 -410 -411 -412 -413 -414 -415 -416 -417 -418 -419 -420 -421 -422 -423 -424 -425 -426 -427 -428 -429 -430 -431 -432 -433 -434 -435 -436 -437 -438 -439 -440 -441 -442 -443 -444 -445 -446 -447 -448 -449 -450 -451 -452 -453 -454 -455 -456 -457 -458 -459 -460 -461 -462 -463 -464 -465 -466 -467 -468 -469 -470 -471 -472 -473 -474 -475 -476 -477 -478 -479 -480 -481 -482 -483 -484 -485 -486 -487 -488 -489 -490 -491 -492 -493 -494 -495 -496 -497 -498 -499 -500 -501 -502 -503 -504 -505 -506 -507 -508 -509 -510 -511 -512 -513 -514 -515 -516 -517 -518 -519 -520 -521 -522 -523 -524 -525 -526 -527 -528 -529 -530 -531 -532 -533 -534 -535 -536 -537 -538 -539 -540 -541 -542 -543 -544 -545 -546 -547 -548 -549 -550 -551 -552 -553 -554 -555 -556 -557 -558 -559 -560 -561 -562 -563 -564 -565 -566 -567 -568 -569 -570 -571 -572 -573 -574 -575 -576 -577 -578 -579 -580 -581 -582 -583 -584 -585 -586 -587 -588 -589 -590 -591 -592 -593 -594 -595 -596 -597 -598 -599 -600 -601 -602 -603 -604 -605 -606 -607 -608 -609 -610 -611 -612 -613 -614 -615 -616 -617 -618 -619 -620 -621 -622 -623 -624 -625 -626 -627 -628 -629 -630 -631 -632 -633 -634 -635 -636 -637 -638 -639 -640 -641 -642 -643 -644 -645 -646 -647 -648 -649 -650 -651 -652 -653 -654 -655 -656 -657 -658 -659 -660 -661 -662 -663 -664 -665 -666 -667 -668 -669 -670 -671 -672 -673 -674 -675 -676 -677 -678 -679 -680 -681 -682 -683 -684 -685 -686 -687 -688 -689 -690 -691 -692 -693 -694 -695 -696 -697 -698 -699 -700 -701 -702 -703 -704 -705 -706 -707 -708 -709 -710 -711 -712 -713 -714 -715 -716 -717 -718 -719 -720 -721 -722 -723 -724 -725 -726 -727 -728 -729 -730 -731 -732 -733 -734 -735 -736 -737 -738 -739 -740 -741 -742 -743 -744 -745 -746 -747 -748 -749 -750 -751 -752 -753 -754 -755 -756 -757 -758 -759 -760 -761 -762 -763 -764 -765 -766 -767 -768 -769 -770 -771 -772 -773 -774 -775 -776 -777 -778 -779 -780 -781 -782 -783 -784 -785 -786 -787 -788 -789 -790 -791 -792 -793 -794 -795 -796 -797 -798 -799 -800 -801 -802 -803 -804 -805 -806 -807 -808 -809 -810 -811 -812 -813 -814 -815 -816 -817 -818 -819 -820 -821 -822 -823 -824 -825 -826 -827 -828 -829 -830 -831 -832 -833 -834 -835 -836 -837 -838 -839 -840 -841 -842 -843 -844 -845 -846 -847 -848 -849 -850 -851 -852 -853 -854 -855 -856 -857 -858 -859 -860 -861 -862 -863 -864 -865 -866 -867 -868 -869 -870 -871 -872 -873 -874 -875 -876 -877 -878 -879 -880 -881 -882 -883 -884 -885 -886 -887 -888 -889 -890 -891 -892 -893 -894 -895 -896 -897 -898 -899 -900 -901 -902 -903 -904 -905 -906 -907 -908 -909 -910 -911 -912 -913 -914 -915 -916 -917 -918 -919 -920 -921 -922 -923 -924 -925 -926 -927 -928 -929 -930 -931 -932 -933 -934 -935 -936 -937  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
// Content script for Full Screenshot Selector Chrome Extension
-class ScreenshotSelector {
-  constructor() {
-    this.isActive = false;
-    this.currentHighlight = null;
-    this.overlay = null;
-    this.style = null;
-    this.background = 'black';
-    this.copyToClipboard = true; // Default to true
-  }
- 
-  async init(background = 'black', copyToClipboard = true) {
-    if (this.isActive) {
-      console.log('Screenshot selector already active');
-      return;
-    }
- 
-    this.background = background;
-    this.copyToClipboard = copyToClipboard;
-    this.isActive = true;
- 
-    this.createUI();
-    this.addEventListeners();
-  }
- 
-  createUI() {
-    // Create overlay UI
-    this.overlay = document.createElement('div');
-    this.overlay.id = 'screenshot-selector-overlay';
-    this.overlay.innerHTML = `
-      <div style="position: fixed; top: 20px; left: 20px; background: rgba(0,0,0,0.9); color: white; padding: 12px 16px; border-radius: 8px; z-index: 2147483647; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 14px; box-shadow: 0 4px 20px rgba(0,0,0,0.3); backdrop-filter: blur(10px);">
-        <div style="display: flex; align-items: center; gap: 10px;">
-          <span>📸</span>
-          <span>Hover over elements and click to capture</span>
-          <button id="screenshot-cancel" style="margin-left: 10px; padding: 4px 8px; background: #ff4757; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px;">ESC</button>
-        </div>
-      </div>
-    `;
- 
-    // Add hover highlighting styles
-    this.style = document.createElement('style');
-    this.style.textContent = `
-      .screenshot-highlight {
-        outline: 3px solid #00d2ff !important;
-        outline-offset: 2px !important;
-        cursor: crosshair !important;
-        position: relative !important;
-      }
-      .screenshot-highlight::after {
-        content: '';
-        position: absolute;
-        top: -5px;
-        left: -5px;
-        right: -5px;
-        bottom: -5px;
-        background: rgba(0, 210, 255, 0.1) !important;
-        pointer-events: none !important;
-        z-index: 2147483646 !important;
-      }
-      .screenshot-scrollable-indicator {
-        position: absolute !important;
-        border: 2px dotted #ff6b6b !important;
-        background: rgba(255, 107, 107, 0.05) !important;
-        pointer-events: none !important;
-        z-index: 2147483645 !important;
-      }
-      #screenshot-selector-overlay * {
-        cursor: default !important;
-      }
-    `;
- 
-    document.head.appendChild(this.style);
-    document.body.appendChild(this.overlay);
- 
-    // Cancel button functionality
-    document.getElementById('screenshot-cancel').onclick = () => this.cleanup();
-  }
- 
-  addEventListeners() {
-    this.handleMouseOver = this.handleMouseOver.bind(this);
-    this.handleClick = this.handleClick.bind(this);
-    this.handleKeyPress = this.handleKeyPress.bind(this);
- 
-    document.addEventListener('mouseover', this.handleMouseOver);
-    document.addEventListener('click', this.handleClick, true);
-    document.addEventListener('keydown', this.handleKeyPress);
-  }
- 
-  handleMouseOver(e) {
-    if (e.target.closest('#screenshot-selector-overlay')) return;
- 
-    // Clean up previous highlights and indicators
-    if (this.currentHighlight) {
-      this.currentHighlight.classList.remove('screenshot-highlight');
-      this.removeScrollableIndicators();
-    }
- 
-    this.currentHighlight = e.target;
-    e.target.classList.add('screenshot-highlight');
- 
-    // Add scrollable content indicators
-    this.addScrollableIndicators(e.target);
-  }
- 
-  handleClick(e) {
-    if (e.target.closest('#screenshot-selector-overlay')) return;
-    e.preventDefault();
-    e.stopPropagation();
- 
-    const element = e.target;
-    this.captureElement(element);
-  }
- 
-  handleKeyPress(e) {
-    if (e.key === 'Escape') {
-      this.cleanup();
-    }
-  }
- 
-  addScrollableIndicators(element) {
-    const rect = element.getBoundingClientRect();
-    const hasVerticalScroll = element.scrollHeight > element.clientHeight;
-    const hasHorizontalScroll = element.scrollWidth > element.clientWidth;
- 
-    if (!hasVerticalScroll && !hasHorizontalScroll) return;
- 
-    // Calculate the full content dimensions
-    const fullHeight = element.scrollHeight;
-    const fullWidth = element.scrollWidth;
-    const visibleHeight = element.clientHeight;
-    const visibleWidth = element.clientWidth;
- 
-    // Create indicator for full scrollable area
-    if (hasVerticalScroll || hasHorizontalScroll) {
-      const indicator = document.createElement('div');
-      indicator.className = 'screenshot-scrollable-indicator';
-      indicator.setAttribute('data-screenshot-indicator', 'true');
- 
-      // Position it relative to the viewport
-      const style = {
-        left: `${rect.left}px`,
-        top: `${rect.top}px`,
-        width: hasHorizontalScroll ? `${fullWidth}px` : `${rect.width}px`,
-        height: hasVerticalScroll ? `${fullHeight}px` : `${rect.height}px`,
-      };
- 
-      Object.assign(indicator.style, style);
-      document.body.appendChild(indicator);
- 
-      // Add a label to show the full dimensions
-      const label = document.createElement('div');
-      label.setAttribute('data-screenshot-indicator', 'true');
-      label.style.cssText = `
-        position: fixed;
-        top: ${rect.top - 25}px;
-        left: ${rect.left}px;
-        background: rgba(255, 107, 107, 0.9);
-        color: white;
-        padding: 2px 6px;
-        border-radius: 3px;
-        font-size: 11px;
-        font-family: monospace;
-        z-index: 2147483647;
-        pointer-events: none;
-      `;
- 
-      const scrollInfo = [];
-      if (hasVerticalScroll) scrollInfo.push(`H: ${fullHeight}px (visible: ${visibleHeight}px)`);
-      if (hasHorizontalScroll) scrollInfo.push(`W: ${fullWidth}px (visible: ${visibleWidth}px)`);
-      label.textContent = `Full content - ${scrollInfo.join(', ')}`;
- 
-      document.body.appendChild(label);
-    }
-  }
- 
-  removeScrollableIndicators() {
-    const indicators = document.querySelectorAll('[data-screenshot-indicator]');
-    indicators.forEach(indicator => indicator.remove());
-  }
- 
-  // Helper method to generate a unique selector for an element
-  getElementSelector(element) {
-    // Escape helper for CSS identifiers (handles Tailwind classes like lg:pr-0)
-    const escapeIdent = (ident) => {
-      try {
-        if (window.CSS && typeof window.CSS.escape === 'function') {
-          return window.CSS.escape(ident);
-        }
-      } catch (_) {}
-      // Fallback: escape most punctuation characters
-      return String(ident).replace(/([ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, '\\$1');
-    };
- 
-    if (element.id) {
-      return `#${escapeIdent(element.id)}`;
-    }
- 
-    if (element.classList && element.classList.length) {
-      const classSelector = Array.from(element.classList)
-        .filter(Boolean)
-        .map(cls => `.${escapeIdent(cls)}`)
-        .join('');
-      if (classSelector) {
-        return `${element.tagName.toLowerCase()}${classSelector}`;
-      }
-    }
- 
-    // Fallback to tag name and position
-    const siblings = Array.from(element.parentNode?.children || []);
-    const index = siblings.indexOf(element);
-    return `${element.tagName.toLowerCase()}:nth-child(${index + 1})`;
-  }
- 
-    // Preserve computed styles for better rendering
-  preserveComputedStyles(clonedElement, originalElement) {
-    if (!originalElement || !clonedElement) return;
- 
-    const computedStyle = window.getComputedStyle(originalElement);
- 
-    // Comprehensive style properties including layout and icon-related ones
-    const importantStyles = [
-      // Typography and fonts
-      'font-family', 'font-size', 'font-weight', 'font-style', 'font-variant',
-      'text-align', 'text-decoration', 'text-transform', 'text-indent',
-      'line-height', 'letter-spacing', 'word-spacing', 'white-space',
- 
-      // Colors and backgrounds
-      'color', 'background-color', 'background-image', 'background-size',
-      'background-position', 'background-repeat', 'background-attachment',
- 
-      // Layout and positioning
-      'display', 'position', 'top', 'left', 'right', 'bottom',
-      'width', 'height', 'min-width', 'min-height', 'max-width', 'max-height',
-      'margin', 'padding', 'border', 'border-radius',
-      'float', 'clear', 'vertical-align',
- 
-      // Flexbox and Grid
-      'flex', 'flex-direction', 'flex-wrap', 'flex-basis', 'flex-grow', 'flex-shrink',
-      'justify-content', 'align-items', 'align-self', 'align-content',
-      'grid', 'grid-template', 'grid-area', 'grid-column', 'grid-row',
- 
-      // Visual effects
-      'opacity', 'visibility', 'overflow', 'overflow-x', 'overflow-y',
-      'box-shadow', 'text-shadow', 'transform', 'filter',
-      'z-index', 'cursor'
-    ];
- 
-    // Apply computed styles
-    importantStyles.forEach(property => {
-      const value = computedStyle.getPropertyValue(property);
-      if (value && value !== 'none' && value !== 'normal' && value !== 'auto' && value !== 'initial') {
-        try {
-          clonedElement.style.setProperty(property, value, 'important');
-        } catch (e) {
-          // Skip properties that can't be set
-        }
-      }
-    });
- 
-    // Handle pseudo-elements (::before and ::after) which often contain icons
-    this.preservePseudoElements(clonedElement, originalElement);
- 
-    // Recursively apply to children
-    const originalChildren = Array.from(originalElement.children);
-    const clonedChildren = Array.from(clonedElement.children);
- 
-    for (let i = 0; i < Math.min(originalChildren.length, clonedChildren.length); i++) {
-      this.preserveComputedStyles(clonedChildren[i], originalChildren[i]);
-    }
-  }
- 
-     // Handle pseudo-elements that often contain icons
-   preservePseudoElements(clonedElement, originalElement) {
-     try {
-       ['::before', '::after'].forEach(pseudo => {
-         const pseudoStyle = window.getComputedStyle(originalElement, pseudo);
-         const content = pseudoStyle.getPropertyValue('content');
- 
-         // If pseudo-element has content, try to preserve it
-         if (content && content !== 'none' && content !== '""') {
-           const pseudoProperties = [
-             'content', 'display', 'position', 'top', 'left', 'right', 'bottom',
-             'width', 'height', 'font-family', 'font-size', 'color',
-             'background-color', 'background-image', 'border', 'border-radius',
-             'transform', 'opacity'
-           ];
- 
-           // Create inline style for pseudo-element
-           let selector = this.getElementSelector(clonedElement);
-           let pseudoCSS = `${selector}${pseudo} {`;
-           pseudoProperties.forEach(prop => {
-             const value = pseudoStyle.getPropertyValue(prop);
-             if (value && value !== 'none' && value !== 'normal') {
-               pseudoCSS += `${prop}: ${value} !important;`;
-             }
-           });
-           pseudoCSS += '}';
- 
-           // Add to document head
-           const style = document.createElement('style');
-           style.textContent = pseudoCSS;
-           document.head.appendChild(style);
-         }
-       });
-     } catch (e) {
-       // Pseudo-element handling failed, continue without it
-     }
-   }
- 
-   // Wait for fonts and resources to load properly
-   async waitForFontsAndResources(element) {
-     // Wait for document fonts to load
-     if (document.fonts && document.fonts.ready) {
-       try {
-         await document.fonts.ready;
-       } catch (e) {
-         // Font loading API not available or failed
-       }
-     }
- 
-     // Check for icon fonts (Font Awesome, Material Icons, etc.)
-     const iconFontFamilies = [
-       'FontAwesome', 'Font Awesome', 'Font Awesome 5', 'Font Awesome 6',
-       'Material Icons', 'Material Icons Outlined', 'Material Icons Sharp',
-       'Ionicons', 'Feather', 'Lucide', 'Tabler Icons'
-     ];
- 
-     // Force load any icon fonts found in the element
-     const allElements = [element, ...element.querySelectorAll('*')];
-     const fontPromises = [];
- 
-     allElements.forEach(el => {
-       const computedStyle = window.getComputedStyle(el);
-       const fontFamily = computedStyle.fontFamily;
- 
-       iconFontFamilies.forEach(iconFont => {
-         if (fontFamily.includes(iconFont)) {
-           // Create a test element to ensure font is loaded
-           const testEl = document.createElement('span');
-           testEl.style.fontFamily = iconFont;
-           testEl.style.position = 'absolute';
-           testEl.style.left = '-9999px';
-           testEl.textContent = '■'; // Use a test character
-           document.body.appendChild(testEl);
- 
-           const fontPromise = new Promise(resolve => {
-             setTimeout(() => {
-               document.body.removeChild(testEl);
-               resolve();
-             }, 100);
-           });
-           fontPromises.push(fontPromise);
-         }
-       });
-     });
- 
-     // Wait for all font loading attempts
-     await Promise.all(fontPromises);
- 
-         // Additional wait for any remaining resources
-    await new Promise(resolve => setTimeout(resolve, 500));
-  }
- 
- 
- 
-  async captureElement(element) {
-    try {
-      // Determine loading message based on clipboard settings
-      let loadingMessage = 'Capturing screenshot...';
-      if (this.copyToClipboard) {
-        const availability = this.checkClipboardAPIAvailability();
-        if (availability.available) {
-          loadingMessage = 'Capturing screenshot and preparing for clipboard...';
-        } else {
-          loadingMessage = 'Capturing screenshot... (Clipboard not available)';
-        }
-      }
- 
-      // Show loading state
-      this.overlay.innerHTML = `
-        <div style="position: fixed; top: 20px; left: 20px; background: rgba(0,0,0,0.9); color: white; padding: 12px 16px; border-radius: 8px; z-index: 2147483647; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 14px; box-shadow: 0 4px 20px rgba(0,0,0,0.3);">
-          <div style="display: flex; align-items: center; gap: 10px;">
-            <div style="width: 16px; height: 16px; border: 2px solid #fff; border-top: 2px solid transparent; border-radius: 50%; animation: spin 1s linear infinite;"></div>
-            <span>${loadingMessage}</span>
-          </div>
-        </div>
-        <style>
-          @keyframes spin {
-            0% { transform: rotate(0deg); }
-            100% { transform: rotate(360deg); }
-          }
-        </style>
-      `;
- 
-      // Remove highlight for clean capture first
-      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);
- 
-            // Enhanced html2canvas configuration
-      const canvas = await html2canvas(element, {
-        useCORS: true,
-        allowTaint: false,
-        backgroundColor: this.background === 'transparent' ? null :
-                        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
- 
-        // 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);
- 
-      // Always download the image first (core functionality)
-      const link = 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 (after successful canvas generation and download)
-      let clipboardResult = null;
-      if (this.copyToClipboard) {
-        const availability = this.checkClipboardAPIAvailability();
-        if (availability.available) {
-          try {
-            // Attempt clipboard operation with comprehensive error handling
-            clipboardResult = await this.copyCanvasToClipboard(canvas);
-          } catch (error) {
-            // Ensure clipboard errors don't break the workflow - this is a safety net
-            // The copyCanvasToClipboard method should handle all errors internally
-            console.error('Unexpected clipboard error caught in captureElement:', error);
-            clipboardResult = { 
-              success: false, 
-              error: 'Unexpected clipboard error occurred',
-              errorType: 'unexpected'
-            };
-          }
-        } else {
-          clipboardResult = { 
-            success: false, 
-            error: availability.reason,
-            errorType: 'api_unavailable'
-          };
-        }
-      }
- 
-      // Generate user feedback message based on clipboard results
-      let successMessage = 'Screenshot downloaded successfully!';
-      let messageColor = 'rgba(39, 174, 96, 0.95)'; // Green for success
-      
-      if (this.copyToClipboard) {
-        if (clipboardResult && clipboardResult.success) {
-          successMessage = 'Screenshot downloaded and copied to clipboard!';
-          // Keep green color for full success
-        } else {
-          // Download succeeded but clipboard failed - provide specific error feedback
-          const errorMessage = this.getClipboardErrorMessage(clipboardResult);
-          successMessage = `Screenshot downloaded successfully! ${errorMessage}`;
-          messageColor = 'rgba(243, 156, 18, 0.95)'; // Orange for partial success
-        }
-      }
- 
-      this.overlay.innerHTML = `
-        <div style="position: fixed; top: 20px; left: 20px; background: ${messageColor}; color: white; padding: 12px 16px; border-radius: 8px; z-index: 2147483647; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 14px; box-shadow: 0 4px 20px rgba(0,0,0,0.3);">
-          <div style="display: flex; align-items: center; gap: 10px;">
-            <span>✅</span>
-            <span>${successMessage}</span>
-          </div>
-        </div>
-      `;
- 
-      // Notify popup
-      chrome.runtime.sendMessage({ action: 'selectorStopped', reason: 'screenshot-taken' });
- 
-      setTimeout(() => this.cleanup(), 2000);
- 
-    } catch (error) {
-      console.error('Screenshot failed:', error);
-      
-      // Provide specific error messages for screenshot failures
-      let errorMessage = 'Screenshot capture failed. Please try again.';
-      
-      if (error.message) {
-        if (error.message.includes('html2canvas')) {
-          errorMessage = 'Screenshot rendering failed. Try selecting a different element or refresh the page.';
-        } else if (error.message.includes('timeout')) {
-          errorMessage = 'Screenshot capture timed out. Try selecting a smaller area or simpler element.';
-        } else if (error.message.includes('network') || error.message.includes('CORS')) {
-          errorMessage = 'Screenshot failed: Some content is blocked by security restrictions.';
-        } else if (error.message.includes('memory') || error.message.includes('quota')) {
-          errorMessage = 'Screenshot failed: Not enough memory. Try capturing a smaller area.';
-        } else if (error.message.includes('canvas')) {
-          errorMessage = 'Screenshot failed: Unable to create image. Try a different element.';
-        }
-      }
-      
-      this.overlay.innerHTML = `
-        <div style="position: fixed; top: 20px; left: 20px; background: rgba(231, 76, 60, 0.95); color: white; padding: 12px 16px; border-radius: 8px; z-index: 2147483647; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 14px; box-shadow: 0 4px 20px rgba(0,0,0,0.3);">
-          <div style="display: flex; align-items: center; gap: 10px;">
-            <span>❌</span>
-            <span>${errorMessage}</span>
-          </div>
-        </div>
-      `;
-      
-      // Notify popup about the failure
-      chrome.runtime.sendMessage({ action: 'selectorStopped', reason: 'screenshot-failed' });
-      
-      setTimeout(() => this.cleanup(), 3000);
-    }
-  }
- 
-  /**
-   * Check if Clipboard API is available and supported
-   * @returns {{available: boolean, reason?: string}} Clipboard API availability status
-   */
-  checkClipboardAPIAvailability() {
-    // Check for basic Clipboard API support
-    if (!navigator.clipboard) {
-      return {
-        available: false,
-        reason: 'Clipboard API not available in this browser'
-      };
-    }
- 
-    // Check for ClipboardItem constructor (required for image data)
-    if (!window.ClipboardItem) {
-      return {
-        available: false,
-        reason: 'ClipboardItem not supported in this browser'
-      };
-    }
- 
-    // Check if we're in a secure context (HTTPS required for clipboard)
-    if (!window.isSecureContext) {
-      return {
-        available: false,
-        reason: 'Clipboard API requires a secure context (HTTPS)'
-      };
-    }
- 
-    // Check for write permission (this is the basic check, actual permission is checked during write)
-    if (!navigator.clipboard.write) {
-      return {
-        available: false,
-        reason: 'Clipboard write functionality not available'
-      };
-    }
- 
-    return {
-      available: true
-    };
-  }
- 
-  /**
-   * Get user-friendly feedback message about clipboard feature availability
-   * @returns {{supported: boolean, message: string}} User feedback about clipboard support
-   */
-  getClipboardSupportFeedback() {
-    const availability = this.checkClipboardAPIAvailability();
-    
-    if (availability.available) {
-      return {
-        supported: true,
-        message: 'Clipboard copying is available and ready to use.'
-      };
-    }
- 
-    // Provide user-friendly messages for different scenarios
-    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
-    };
-  }
- 
-  /**
-   * Generate user-friendly error message for clipboard operation failures
-   * @param {Object} clipboardResult - Result object from clipboard operation
-   * @returns {string} User-friendly error message
-   */
-  getClipboardErrorMessage(clipboardResult) {
-    if (!clipboardResult || !clipboardResult.error) {
-      return 'Clipboard copy failed due to unknown error.';
-    }
- 
-    const errorType = clipboardResult.errorType;
-    const errorMessage = clipboardResult.error;
- 
-    // Provide contextual error messages based on error type
-    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.';
-      
-      case 'network_error':
-        return 'Clipboard copy failed due to network error. Please try again.';
-      
-      case 'canvas_conversion':
-        return 'Clipboard copy failed: Unable to prepare image. Try capturing a different element.';
-      
-      case 'clipboard_item_creation':
-        return 'Clipboard copy not supported: Your browser doesn\'t support image clipboard operations.';
-      
-      case 'invalid_state':
-        return 'Clipboard copy failed: Browser clipboard is busy. Please try again.';
-      
-      case 'data_error':
-        return 'Clipboard copy failed: Invalid image data. Please try capturing again.';
-      
-      case 'unexpected':
-        return 'Clipboard copy failed due to unexpected error. Screenshot was still downloaded.';
-      
-      default:
-        // For unknown error types, try to extract meaningful info from the error message
-        if (errorMessage.includes('permission') || errorMessage.includes('denied')) {
-          return 'Clipboard copy failed: Access denied. Check browser permissions.';
-        } else if (errorMessage.includes('not supported') || errorMessage.includes('unavailable')) {
-          return 'Clipboard copy not supported in this browser.';
-        } else if (errorMessage.includes('timeout') || errorMessage.includes('timed out')) {
-          return 'Clipboard copy timed out. Try capturing a smaller area.';
-        } else if (errorMessage.includes('large') || errorMessage.includes('size')) {
-          return 'Clipboard copy failed: Image too large for clipboard.';
-        } else {
-          return `Clipboard copy failed: ${errorMessage}`;
-        }
-    }
-  }
- 
-  /**
-   * Copy canvas content to clipboard using the Clipboard API
-   * @param {HTMLCanvasElement} canvas - The canvas element to copy
-   * @returns {Promise<{success: boolean, error?: string, errorType?: string}>} Result of clipboard operation
-   */
-  async copyCanvasToClipboard(canvas) {
-    try {
-      // Check clipboard API availability first
-      const availability = this.checkClipboardAPIAvailability();
-      if (!availability.available) {
-        return { 
-          success: false, 
-          error: availability.reason,
-          errorType: 'api_unavailable'
-        };
-      }
- 
-      // Convert canvas to blob in PNG format with timeout
-      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)
-        )
-      ]);
- 
-      // Validate blob size (prevent extremely large clipboard operations)
-      const maxBlobSize = 50 * 1024 * 1024; // 50MB limit
-      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'
-        };
-      }
- 
-      // Create ClipboardItem with PNG image data
-      let clipboardItem;
-      try {
-        clipboardItem = new 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'
-        };
-      }
- 
-      // Write to clipboard with timeout
-      await Promise.race([
-        navigator.clipboard.write([clipboardItem]),
-        new Promise((_, reject) => 
-          setTimeout(() => reject(new Error('Clipboard write operation timed out')), 15000)
-        )
-      ]);
- 
-      return { success: true };
- 
-    } catch (error) {
-      console.error('Clipboard operation failed:', error);
-      
-      // Comprehensive error handling with specific error types and user-friendly messages
-      let errorMessage = error.message || 'Unknown clipboard error';
-      let errorType = 'unknown';
-      
-      // Permission-related errors
-      if (error.name === 'NotAllowedError') {
-        errorMessage = 'Clipboard access denied. Please allow clipboard permissions in your browser settings.';
-        errorType = 'permission_denied';
-      } 
-      // API support errors
-      else if (error.name === 'NotSupportedError') {
-        errorMessage = 'Clipboard API not supported in this browser context.';
-        errorType = 'not_supported';
-      }
-      // Security context errors
-      else if (error.name === 'SecurityError') {
-        errorMessage = 'Clipboard access blocked by security policy. Try using HTTPS.';
-        errorType = 'security_error';
-      }
-      // Network or resource errors
-      else if (error.name === 'NetworkError') {
-        errorMessage = 'Network error during clipboard operation. Please try again.';
-        errorType = 'network_error';
-      }
-      // Timeout errors
-      else if (error.message.includes('timed out')) {
-        errorMessage = 'Clipboard operation timed out. The image may be too large.';
-        errorType = 'timeout';
-      }
-      // Canvas conversion errors
-      else if (error.message.includes('canvas') || error.message.includes('blob')) {
-        errorMessage = 'Failed to prepare image for clipboard. Try capturing a different area.';
-        errorType = 'canvas_conversion';
-      }
-      // Quota or storage errors
-      else if (error.name === 'QuotaExceededError' || error.message.includes('quota')) {
-        errorMessage = 'Clipboard storage quota exceeded. Try capturing a smaller image.';
-        errorType = 'quota_exceeded';
-      }
-      // DOM or state errors
-      else if (error.name === 'InvalidStateError') {
-        errorMessage = 'Clipboard is in an invalid state. Please try again.';
-        errorType = 'invalid_state';
-      }
-      // Data errors
-      else if (error.name === 'DataError') {
-        errorMessage = 'Invalid image data for clipboard. Please try again.';
-        errorType = 'data_error';
-      }
-      // Generic fallback for unknown errors
-      else {
-        errorMessage = `Clipboard operation failed: ${error.message}`;
-        errorType = 'unknown';
-      }
- 
-      return { 
-        success: false, 
-        error: errorMessage,
-        errorType: errorType
-      };
-    }
-  }
- 
-  cleanup() {
-    document.removeEventListener('mouseover', this.handleMouseOver);
-    document.removeEventListener('click', this.handleClick, true);
-    document.removeEventListener('keydown', this.handleKeyPress);
- 
-    if (this.currentHighlight) {
-      this.currentHighlight.classList.remove('screenshot-highlight');
-    }
- 
-    // Remove any scrollable indicators
-    this.removeScrollableIndicators();
- 
-    if (this.overlay) {
-      this.overlay.remove();
-      this.overlay = null;
-    }
- 
-    if (this.style) {
-      this.style.remove();
-      this.style = null;
-    }
- 
-    this.isActive = false;
-    this.currentHighlight = null;
-  }
-}
- 
-// Global instance
-let screenshotSelector = new ScreenshotSelector();
- 
-// Add load indicator
-console.log('Full Screenshot Selector content script loaded');
- 
-// Listen for messages from popup
-chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
-  try {
-  switch (message.action) {
-    case 'startSelector':
-      // Extract clipboard preference from message, default to true for backward compatibility
-      const copyToClipboard = message.copyToClipboard !== undefined ? message.copyToClipboard : true;
-      screenshotSelector.init(message.background, copyToClipboard);
-      sendResponse({ success: true });
-      break;
- 
-    case 'stopSelector':
-      screenshotSelector.cleanup();
-      sendResponse({ success: true });
-      break;
- 
-    case 'checkState':
-      sendResponse({ isActive: screenshotSelector.isActive });
-      break;
- 
-    case 'checkClipboardSupport':
-      const availability = screenshotSelector.checkClipboardAPIAvailability();
-      const feedback = screenshotSelector.getClipboardSupportFeedback();
-      sendResponse({ 
-        availability: availability,
-        feedback: feedback
-      });
-      break;
- 
-      default:
-        sendResponse({ error: 'Unknown action: ' + message.action });
-    }
-  } catch (error) {
-    console.error('Content script error:', error);
-    sendResponse({ error: error.message });
-  }
- 
-  // Important: return true to indicate async response
-  return true;
-});
- 
-// Handle keyboard shortcut
-chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
-  if (message.action === 'activateFromShortcut') {
-    if (!screenshotSelector.isActive) {
-      screenshotSelector.init('black', true); // Default background and clipboard enabled for shortcut
-    }
-  }
-});
- -
-
- - - - - - - - \ No newline at end of file diff --git a/coverage/lcov-report/favicon.png b/coverage/lcov-report/favicon.png deleted file mode 100644 index c1525b811a167671e9de1fa78aab9f5c0b61cef7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 445 zcmV;u0Yd(XP))rP{nL}Ln%S7`m{0DjX9TLF* zFCb$4Oi7vyLOydb!7n&^ItCzb-%BoB`=x@N2jll2Nj`kauio%aw_@fe&*}LqlFT43 z8doAAe))z_%=P%v^@JHp3Hjhj^6*Kr_h|g_Gr?ZAa&y>wxHE99Gk>A)2MplWz2xdG zy8VD2J|Uf#EAw*bo5O*PO_}X2Tob{%bUoO2G~T`@%S6qPyc}VkhV}UifBuRk>%5v( z)x7B{I~z*k<7dv#5tC+m{km(D087J4O%+<<;K|qwefb6@GSX45wCK}Sn*> - - - - Code coverage report for All files - - - - - - - - - -
-
-

All files

-
- -
- 0% - Statements - 0/463 -
- - -
- 0% - Branches - 0/263 -
- - -
- 0% - Functions - 0/61 -
- - -
- 0% - Lines - 0/445 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FileStatementsBranchesFunctionsLines
content.js -
-
0%0/3710%0/2140%0/440%0/356
popup.js -
-
0%0/920%0/490%0/170%0/89
-
-
-
- - - - - - - - \ No newline at end of file diff --git a/coverage/lcov-report/popup.js.html b/coverage/lcov-report/popup.js.html deleted file mode 100644 index 5f1a6a1..0000000 --- a/coverage/lcov-report/popup.js.html +++ /dev/null @@ -1,643 +0,0 @@ - - - - - - Code coverage report for popup.js - - - - - - - - - -
-
-

All files popup.js

-
- -
- 0% - Statements - 0/92 -
- - -
- 0% - Branches - 0/49 -
- - -
- 0% - Functions - 0/17 -
- - -
- 0% - Lines - 0/89 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
// Popup script for Chrome extension
-document.addEventListener('DOMContentLoaded', async () => {
-  const backgroundSelect = document.getElementById('background-select');
-  const clipboardToggle = document.getElementById('clipboard-toggle');
-  const startBtn = document.getElementById('start-selector');
-  const stopBtn = document.getElementById('stop-selector');
-  const status = document.getElementById('status');
- 
-  // Load saved preferences
-  const saved = await chrome.storage.sync.get(['backgroundPreference', 'copyToClipboard']);
-  if (saved.backgroundPreference) {
-    backgroundSelect.value = saved.backgroundPreference;
-  }
-  
-  // Set clipboard preference (default to true if not set)
-  clipboardToggle.checked = saved.copyToClipboard !== undefined ? saved.copyToClipboard : true;
- 
-  // Save background preference when changed
-  backgroundSelect.addEventListener('change', () => {
-    chrome.storage.sync.set({ backgroundPreference: backgroundSelect.value });
-  });
- 
-  // Save clipboard preference when changed
-  clipboardToggle.addEventListener('change', () => {
-    chrome.storage.sync.set({ copyToClipboard: clipboardToggle.checked });
-  });
- 
-  // Start selector
-  startBtn.addEventListener('click', async () => {
-    const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
- 
-    // Check if page is compatible
-    if (tab.url.startsWith('chrome://') ||
-        tab.url.startsWith('chrome-extension://') ||
-        tab.url.startsWith('edge://') ||
-        tab.url.startsWith('about:') ||
-        tab.url === 'chrome://newtab/' ||
-        tab.url === 'edge://newtab/') {
-      showStatus('Error: Cannot capture screenshots on this page type.', 'error');
-      return;
-    }
- 
-    try {
-      await chrome.tabs.sendMessage(tab.id, {
-        action: 'startSelector',
-        background: backgroundSelect.value,
-        copyToClipboard: clipboardToggle.checked
-      });
- 
-      showStatus('Selector activated! Hover over elements and click to capture.', 'success');
-      toggleButtons(true);
- 
-      // Auto-close popup after starting
-      setTimeout(() => window.close(), 1500);
- 
-    } catch (error) {
-      console.error('Full error details:', error);
-      if (error.message.includes('Could not establish connection')) {
-        showStatus('Error: Content script not loaded. Try refreshing the page.', 'error');
-      } else {
-        showStatus('Error: Could not activate on this page.', 'error');
-      }
-    }
-  });
- 
-  // Stop selector
-  stopBtn.addEventListener('click', async () => {
-    const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
- 
-    try {
-      await chrome.tabs.sendMessage(tab.id, { action: 'stopSelector' });
-      showStatus('Selector stopped.', 'success');
-      toggleButtons(false);
-    } catch (error) {
-      console.error('Error stopping selector:', error);
-    }
-  });
- 
-  // Check current state
-  checkSelectorState();
- 
-  // Handle tooltip keyboard navigation
-  setupTooltipAccessibility();
- 
-  function toggleButtons(isActive) {
-    if (isActive) {
-      startBtn.style.display = 'none';
-      stopBtn.style.display = 'block';
-    } else {
-      startBtn.style.display = 'block';
-      stopBtn.style.display = 'none';
-    }
-  }
- 
-  function showStatus(message, type = 'success') {
-    status.textContent = message;
-    status.className = `status ${type}`;
-    status.style.display = 'block';
- 
-    setTimeout(() => {
-      status.style.display = 'none';
-    }, 3000);
-  }
- 
-  async function checkSelectorState() {
-    const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
- 
-    // Skip check for incompatible pages
-    if (tab.url.startsWith('chrome://') ||
-        tab.url.startsWith('chrome-extension://') ||
-        tab.url.startsWith('edge://') ||
-        tab.url.startsWith('about:')) {
-      return;
-    }
- 
-    try {
-      const response = await chrome.tabs.sendMessage(tab.id, { action: 'checkState' });
-      if (response && response.isActive) {
-        toggleButtons(true);
-        showStatus('Selector is currently active', 'success');
-      }
-    } catch (error) {
-      // Content script not ready or selector not active
-      console.log('Content script not ready or selector inactive:', error.message);
-    }
-  }
- 
-  function setupTooltipAccessibility() {
-    const tooltipIcon = document.querySelector('.tooltip-icon');
-    const tooltipContent = document.querySelector('.tooltip-content');
-    
-    if (!tooltipIcon || !tooltipContent) return;
- 
-    // Handle keyboard events for tooltip
-    tooltipIcon.addEventListener('keydown', (e) => {
-      if (e.key === 'Enter' || e.key === ' ') {
-        e.preventDefault();
-        // Toggle tooltip visibility on Enter/Space
-        const isVisible = tooltipContent.style.visibility === 'visible';
-        tooltipContent.style.visibility = isVisible ? 'hidden' : 'visible';
-        tooltipContent.style.opacity = isVisible ? '0' : '1';
-      } else if (e.key === 'Escape') {
-        // Hide tooltip on Escape
-        tooltipContent.style.visibility = 'hidden';
-        tooltipContent.style.opacity = '0';
-      }
-    });
- 
-    // Hide tooltip when clicking outside
-    document.addEventListener('click', (e) => {
-      if (!tooltipIcon.contains(e.target)) {
-        tooltipContent.style.visibility = 'hidden';
-        tooltipContent.style.opacity = '0';
-      }
-    });
- 
-    // Handle focus loss
-    tooltipIcon.addEventListener('blur', (e) => {
-      // Small delay to allow for focus to move to tooltip content if needed
-      setTimeout(() => {
-        if (!tooltipIcon.matches(':focus-within')) {
-          tooltipContent.style.visibility = 'hidden';
-          tooltipContent.style.opacity = '0';
-        }
-      }, 100);
-    });
-  }
-});
- 
-// Listen for messages from content script
-chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
-  if (message.action === 'selectorStopped') {
-    const startBtn = document.getElementById('start-selector');
-    const stopBtn = document.getElementById('stop-selector');
-    const status = document.getElementById('status');
- 
-    startBtn.style.display = 'block';
-    stopBtn.style.display = 'none';
- 
-    if (message.reason === 'screenshot-taken') {
-      status.textContent = 'Screenshot captured!';
-      status.className = 'status success';
-      status.style.display = 'block';
-      setTimeout(() => status.style.display = 'none', 2000);
-    }
-  }
-});
- -
-
- - - - - - - - \ No newline at end of file diff --git a/coverage/lcov-report/prettify.css b/coverage/lcov-report/prettify.css deleted file mode 100644 index b317a7c..0000000 --- a/coverage/lcov-report/prettify.css +++ /dev/null @@ -1 +0,0 @@ -.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} diff --git a/coverage/lcov-report/prettify.js b/coverage/lcov-report/prettify.js deleted file mode 100644 index b322523..0000000 --- a/coverage/lcov-report/prettify.js +++ /dev/null @@ -1,2 +0,0 @@ -/* eslint-disable */ -window.PR_SHOULD_USE_CONTINUATION=true;(function(){var h=["break,continue,do,else,for,if,return,while"];var u=[h,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"];var p=[u,"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"];var l=[p,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"];var x=[p,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"];var R=[x,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"];var r="all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes";var w=[p,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"];var s="caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END";var I=[h,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"];var f=[h,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"];var H=[h,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"];var A=[l,R,w,s+I,f,H];var e=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/;var C="str";var z="kwd";var j="com";var O="typ";var G="lit";var L="pun";var F="pln";var m="tag";var E="dec";var J="src";var P="atn";var n="atv";var N="nocode";var M="(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*";function k(Z){var ad=0;var S=false;var ac=false;for(var V=0,U=Z.length;V122)){if(!(al<65||ag>90)){af.push([Math.max(65,ag)|32,Math.min(al,90)|32])}if(!(al<97||ag>122)){af.push([Math.max(97,ag)&~32,Math.min(al,122)&~32])}}}}af.sort(function(av,au){return(av[0]-au[0])||(au[1]-av[1])});var ai=[];var ap=[NaN,NaN];for(var ar=0;arat[0]){if(at[1]+1>at[0]){an.push("-")}an.push(T(at[1]))}}an.push("]");return an.join("")}function W(al){var aj=al.source.match(new RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g"));var ah=aj.length;var an=[];for(var ak=0,am=0;ak=2&&ai==="["){aj[ak]=X(ag)}else{if(ai!=="\\"){aj[ak]=ag.replace(/[a-zA-Z]/g,function(ao){var ap=ao.charCodeAt(0);return"["+String.fromCharCode(ap&~32,ap|32)+"]"})}}}}return aj.join("")}var aa=[];for(var V=0,U=Z.length;V=0;){S[ac.charAt(ae)]=Y}}var af=Y[1];var aa=""+af;if(!ag.hasOwnProperty(aa)){ah.push(af);ag[aa]=null}}ah.push(/[\0-\uffff]/);V=k(ah)})();var X=T.length;var W=function(ah){var Z=ah.sourceCode,Y=ah.basePos;var ad=[Y,F];var af=0;var an=Z.match(V)||[];var aj={};for(var ae=0,aq=an.length;ae=5&&"lang-"===ap.substring(0,5);if(am&&!(ai&&typeof ai[1]==="string")){am=false;ap=J}if(!am){aj[ag]=ap}}var ab=af;af+=ag.length;if(!am){ad.push(Y+ab,ap)}else{var al=ai[1];var ak=ag.indexOf(al);var ac=ak+al.length;if(ai[2]){ac=ag.length-ai[2].length;ak=ac-al.length}var ar=ap.substring(5);B(Y+ab,ag.substring(0,ak),W,ad);B(Y+ab+ak,al,q(ar,al),ad);B(Y+ab+ac,ag.substring(ac),W,ad)}}ah.decorations=ad};return W}function i(T){var W=[],S=[];if(T.tripleQuotedStrings){W.push([C,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,null,"'\""])}else{if(T.multiLineStrings){W.push([C,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"])}else{W.push([C,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"])}}if(T.verbatimStrings){S.push([C,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null])}var Y=T.hashComments;if(Y){if(T.cStyleComments){if(Y>1){W.push([j,/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,null,"#"])}else{W.push([j,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"])}S.push([C,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,null])}else{W.push([j,/^#[^\r\n]*/,null,"#"])}}if(T.cStyleComments){S.push([j,/^\/\/[^\r\n]*/,null]);S.push([j,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}if(T.regexLiterals){var X=("/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/");S.push(["lang-regex",new RegExp("^"+M+"("+X+")")])}var V=T.types;if(V){S.push([O,V])}var U=(""+T.keywords).replace(/^ | $/g,"");if(U.length){S.push([z,new RegExp("^(?:"+U.replace(/[\s,]+/g,"|")+")\\b"),null])}W.push([F,/^\s+/,null," \r\n\t\xA0"]);S.push([G,/^@[a-z_$][a-z_$@0-9]*/i,null],[O,/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],[F,/^[a-z_$][a-z_$@0-9]*/i,null],[G,new RegExp("^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*","i"),null,"0123456789"],[F,/^\\[\s\S]?/,null],[L,/^.[^\s\w\.$@\'\"\`\/\#\\]*/,null]);return g(W,S)}var K=i({keywords:A,hashComments:true,cStyleComments:true,multiLineStrings:true,regexLiterals:true});function Q(V,ag){var U=/(?:^|\s)nocode(?:\s|$)/;var ab=/\r\n?|\n/;var ac=V.ownerDocument;var S;if(V.currentStyle){S=V.currentStyle.whiteSpace}else{if(window.getComputedStyle){S=ac.defaultView.getComputedStyle(V,null).getPropertyValue("white-space")}}var Z=S&&"pre"===S.substring(0,3);var af=ac.createElement("LI");while(V.firstChild){af.appendChild(V.firstChild)}var W=[af];function ae(al){switch(al.nodeType){case 1:if(U.test(al.className)){break}if("BR"===al.nodeName){ad(al);if(al.parentNode){al.parentNode.removeChild(al)}}else{for(var an=al.firstChild;an;an=an.nextSibling){ae(an)}}break;case 3:case 4:if(Z){var am=al.nodeValue;var aj=am.match(ab);if(aj){var ai=am.substring(0,aj.index);al.nodeValue=ai;var ah=am.substring(aj.index+aj[0].length);if(ah){var ak=al.parentNode;ak.insertBefore(ac.createTextNode(ah),al.nextSibling)}ad(al);if(!ai){al.parentNode.removeChild(al)}}}break}}function ad(ak){while(!ak.nextSibling){ak=ak.parentNode;if(!ak){return}}function ai(al,ar){var aq=ar?al.cloneNode(false):al;var ao=al.parentNode;if(ao){var ap=ai(ao,1);var an=al.nextSibling;ap.appendChild(aq);for(var am=an;am;am=an){an=am.nextSibling;ap.appendChild(am)}}return aq}var ah=ai(ak.nextSibling,0);for(var aj;(aj=ah.parentNode)&&aj.nodeType===1;){ah=aj}W.push(ah)}for(var Y=0;Y=S){ah+=2}if(V>=ap){Z+=2}}}var t={};function c(U,V){for(var S=V.length;--S>=0;){var T=V[S];if(!t.hasOwnProperty(T)){t[T]=U}else{if(window.console){console.warn("cannot override language handler %s",T)}}}}function q(T,S){if(!(T&&t.hasOwnProperty(T))){T=/^\s*]*(?:>|$)/],[j,/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],[L,/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);c(g([[F,/^[\s]+/,null," \t\r\n"],[n,/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[[m,/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],[P,/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],[L,/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);c(g([],[[n,/^[\s\S]+/]]),["uq.val"]);c(i({keywords:l,hashComments:true,cStyleComments:true,types:e}),["c","cc","cpp","cxx","cyc","m"]);c(i({keywords:"null,true,false"}),["json"]);c(i({keywords:R,hashComments:true,cStyleComments:true,verbatimStrings:true,types:e}),["cs"]);c(i({keywords:x,cStyleComments:true}),["java"]);c(i({keywords:H,hashComments:true,multiLineStrings:true}),["bsh","csh","sh"]);c(i({keywords:I,hashComments:true,multiLineStrings:true,tripleQuotedStrings:true}),["cv","py"]);c(i({keywords:s,hashComments:true,multiLineStrings:true,regexLiterals:true}),["perl","pl","pm"]);c(i({keywords:f,hashComments:true,multiLineStrings:true,regexLiterals:true}),["rb"]);c(i({keywords:w,cStyleComments:true,regexLiterals:true}),["js"]);c(i({keywords:r,hashComments:3,cStyleComments:true,multilineStrings:true,tripleQuotedStrings:true,regexLiterals:true}),["coffee"]);c(g([],[[C,/^[\s\S]+/]]),["regex"]);function d(V){var U=V.langExtension;try{var S=a(V.sourceNode);var T=S.sourceCode;V.sourceCode=T;V.spans=S.spans;V.basePos=0;q(U,T)(V);D(V)}catch(W){if("console" in window){console.log(W&&W.stack?W.stack:W)}}}function y(W,V,U){var S=document.createElement("PRE");S.innerHTML=W;if(U){Q(S,U)}var T={langExtension:V,numberLines:U,sourceNode:S};d(T);return S.innerHTML}function b(ad){function Y(af){return document.getElementsByTagName(af)}var ac=[Y("pre"),Y("code"),Y("xmp")];var T=[];for(var aa=0;aa=0){var ah=ai.match(ab);var am;if(!ah&&(am=o(aj))&&"CODE"===am.tagName){ah=am.className.match(ab)}if(ah){ah=ah[1]}var al=false;for(var ak=aj.parentNode;ak;ak=ak.parentNode){if((ak.tagName==="pre"||ak.tagName==="code"||ak.tagName==="xmp")&&ak.className&&ak.className.indexOf("prettyprint")>=0){al=true;break}}if(!al){var af=aj.className.match(/\blinenums\b(?::(\d+))?/);af=af?af[1]&&af[1].length?+af[1]:true:false;if(af){Q(aj,af)}S={langExtension:ah,sourceNode:aj,numberLines:af};d(S)}}}if(X]*(?:>|$)/],[PR.PR_COMMENT,/^<\!--[\s\S]*?(?:-\->|$)/],[PR.PR_PUNCTUATION,/^(?:<[%?]|[%?]>)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-handlebars",/^]*type\s*=\s*['"]?text\/x-handlebars-template['"]?\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i],[PR.PR_DECLARATION,/^{{[#^>/]?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{&?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{{>?\s*[\w.][^}]*}}}/],[PR.PR_COMMENT,/^{{![^}]*}}/]]),["handlebars","hbs"]);PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[ \t\r\n\f]+/,null," \t\r\n\f"]],[[PR.PR_STRING,/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],[PR.PR_STRING,/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']*)\)/i],[PR.PR_KEYWORD,/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],[PR.PR_COMMENT,/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//],[PR.PR_COMMENT,/^(?:)/],[PR.PR_LITERAL,/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],[PR.PR_LITERAL,/^#(?:[0-9a-f]{3}){1,2}/i],[PR.PR_PLAIN,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i],[PR.PR_PUNCTUATION,/^[^\s\w\'\"]+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_KEYWORD,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_STRING,/^[^\)\"\']+/]]),["css-str"]); diff --git a/coverage/lcov-report/sort-arrow-sprite.png b/coverage/lcov-report/sort-arrow-sprite.png deleted file mode 100644 index 6ed68316eb3f65dec9063332d2f69bf3093bbfab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 138 zcmeAS@N?(olHy`uVBq!ia0vp^>_9Bd!3HEZxJ@+%Qh}Z>jv*C{$p!i!8j}?a+@3A= zIAGwzjijN=FBi!|L1t?LM;Q;gkwn>2cAy-KV{dn nf0J1DIvEHQu*n~6U}x}qyky7vi4|9XhBJ7&`njxgN@xNA8m%nc diff --git a/coverage/lcov-report/sorter.js b/coverage/lcov-report/sorter.js deleted file mode 100644 index 2bb296a..0000000 --- a/coverage/lcov-report/sorter.js +++ /dev/null @@ -1,196 +0,0 @@ -/* eslint-disable */ -var addSorting = (function() { - 'use strict'; - var cols, - currentSort = { - index: 0, - desc: false - }; - - // returns the summary table element - function getTable() { - return document.querySelector('.coverage-summary'); - } - // returns the thead element of the summary table - function getTableHeader() { - return getTable().querySelector('thead tr'); - } - // returns the tbody element of the summary table - function getTableBody() { - return getTable().querySelector('tbody'); - } - // returns the th element for nth column - function getNthColumn(n) { - return getTableHeader().querySelectorAll('th')[n]; - } - - function onFilterInput() { - const searchValue = document.getElementById('fileSearch').value; - const rows = document.getElementsByTagName('tbody')[0].children; - for (let i = 0; i < rows.length; i++) { - const row = rows[i]; - if ( - row.textContent - .toLowerCase() - .includes(searchValue.toLowerCase()) - ) { - row.style.display = ''; - } else { - row.style.display = 'none'; - } - } - } - - // loads the search box - function addSearchBox() { - var template = document.getElementById('filterTemplate'); - var templateClone = template.content.cloneNode(true); - templateClone.getElementById('fileSearch').oninput = onFilterInput; - template.parentElement.appendChild(templateClone); - } - - // loads all columns - function loadColumns() { - var colNodes = getTableHeader().querySelectorAll('th'), - colNode, - cols = [], - col, - i; - - for (i = 0; i < colNodes.length; i += 1) { - colNode = colNodes[i]; - col = { - key: colNode.getAttribute('data-col'), - sortable: !colNode.getAttribute('data-nosort'), - type: colNode.getAttribute('data-type') || 'string' - }; - cols.push(col); - if (col.sortable) { - col.defaultDescSort = col.type === 'number'; - colNode.innerHTML = - colNode.innerHTML + ''; - } - } - return cols; - } - // attaches a data attribute to every tr element with an object - // of data values keyed by column name - function loadRowData(tableRow) { - var tableCols = tableRow.querySelectorAll('td'), - colNode, - col, - data = {}, - i, - val; - for (i = 0; i < tableCols.length; i += 1) { - colNode = tableCols[i]; - col = cols[i]; - val = colNode.getAttribute('data-value'); - if (col.type === 'number') { - val = Number(val); - } - data[col.key] = val; - } - return data; - } - // loads all row data - function loadData() { - var rows = getTableBody().querySelectorAll('tr'), - i; - - for (i = 0; i < rows.length; i += 1) { - rows[i].data = loadRowData(rows[i]); - } - } - // sorts the table using the data for the ith column - function sortByIndex(index, desc) { - var key = cols[index].key, - sorter = function(a, b) { - a = a.data[key]; - b = b.data[key]; - return a < b ? -1 : a > b ? 1 : 0; - }, - finalSorter = sorter, - tableBody = document.querySelector('.coverage-summary tbody'), - rowNodes = tableBody.querySelectorAll('tr'), - rows = [], - i; - - if (desc) { - finalSorter = function(a, b) { - return -1 * sorter(a, b); - }; - } - - for (i = 0; i < rowNodes.length; i += 1) { - rows.push(rowNodes[i]); - tableBody.removeChild(rowNodes[i]); - } - - rows.sort(finalSorter); - - for (i = 0; i < rows.length; i += 1) { - tableBody.appendChild(rows[i]); - } - } - // removes sort indicators for current column being sorted - function removeSortIndicators() { - var col = getNthColumn(currentSort.index), - cls = col.className; - - cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, ''); - col.className = cls; - } - // adds sort indicators for current column being sorted - function addSortIndicators() { - getNthColumn(currentSort.index).className += currentSort.desc - ? ' sorted-desc' - : ' sorted'; - } - // adds event listeners for all sorter widgets - function enableUI() { - var i, - el, - ithSorter = function ithSorter(i) { - var col = cols[i]; - - return function() { - var desc = col.defaultDescSort; - - if (currentSort.index === i) { - desc = !currentSort.desc; - } - sortByIndex(i, desc); - removeSortIndicators(); - currentSort.index = i; - currentSort.desc = desc; - addSortIndicators(); - }; - }; - for (i = 0; i < cols.length; i += 1) { - if (cols[i].sortable) { - // add the click event handler on the th so users - // dont have to click on those tiny arrows - el = getNthColumn(i).querySelector('.sorter').parentElement; - if (el.addEventListener) { - el.addEventListener('click', ithSorter(i)); - } else { - el.attachEvent('onclick', ithSorter(i)); - } - } - } - } - // adds sorting functionality to the UI - return function() { - if (!getTable()) { - return; - } - cols = loadColumns(); - loadData(); - addSearchBox(); - addSortIndicators(); - enableUI(); - }; -})(); - -window.addEventListener('load', addSorting); diff --git a/coverage/lcov.info b/coverage/lcov.info deleted file mode 100644 index 374cf82..0000000 --- a/coverage/lcov.info +++ /dev/null @@ -1,848 +0,0 @@ -TN: -SF:content.js -FN:3,(anonymous_0) -FN:12,(anonymous_1) -FN:26,(anonymous_2) -FN:76,(anonymous_3) -FN:79,(anonymous_4) -FN:89,(anonymous_5) -FN:105,(anonymous_6) -FN:114,(anonymous_7) -FN:120,(anonymous_8) -FN:176,(anonymous_9) -FN:178,(anonymous_10) -FN:182,(anonymous_11) -FN:184,(anonymous_12) -FN:201,(anonymous_13) -FN:215,(anonymous_14) -FN:249,(anonymous_15) -FN:273,(anonymous_16) -FN:275,(anonymous_17) -FN:291,(anonymous_18) -FN:311,(anonymous_19) -FN:332,(anonymous_20) -FN:336,(anonymous_21) -FN:346,(anonymous_22) -FN:347,(anonymous_23) -FN:361,(anonymous_24) -FN:366,(anonymous_25) -FN:435,(anonymous_26) -FN:441,(anonymous_27) -FN:455,(anonymous_28) -FN:544,(anonymous_29) -FN:578,(anonymous_30) -FN:586,(anonymous_31) -FN:628,(anonymous_32) -FN:662,(anonymous_33) -FN:728,(anonymous_34) -FN:742,(anonymous_35) -FN:743,(anonymous_36) -FN:751,(anonymous_37) -FN:752,(anonymous_38) -FN:783,(anonymous_39) -FN:784,(anonymous_40) -FN:856,(anonymous_41) -FN:890,(anonymous_42) -FN:931,(anonymous_43) -FNF:44 -FNH:0 -FNDA:0,(anonymous_0) -FNDA:0,(anonymous_1) -FNDA:0,(anonymous_2) -FNDA:0,(anonymous_3) -FNDA:0,(anonymous_4) -FNDA:0,(anonymous_5) -FNDA:0,(anonymous_6) -FNDA:0,(anonymous_7) -FNDA:0,(anonymous_8) -FNDA:0,(anonymous_9) -FNDA:0,(anonymous_10) -FNDA:0,(anonymous_11) -FNDA:0,(anonymous_12) -FNDA:0,(anonymous_13) -FNDA:0,(anonymous_14) -FNDA:0,(anonymous_15) -FNDA:0,(anonymous_16) -FNDA:0,(anonymous_17) -FNDA:0,(anonymous_18) -FNDA:0,(anonymous_19) -FNDA:0,(anonymous_20) -FNDA:0,(anonymous_21) -FNDA:0,(anonymous_22) -FNDA:0,(anonymous_23) -FNDA:0,(anonymous_24) -FNDA:0,(anonymous_25) -FNDA:0,(anonymous_26) -FNDA:0,(anonymous_27) -FNDA:0,(anonymous_28) -FNDA:0,(anonymous_29) -FNDA:0,(anonymous_30) -FNDA:0,(anonymous_31) -FNDA:0,(anonymous_32) -FNDA:0,(anonymous_33) -FNDA:0,(anonymous_34) -FNDA:0,(anonymous_35) -FNDA:0,(anonymous_36) -FNDA:0,(anonymous_37) -FNDA:0,(anonymous_38) -FNDA:0,(anonymous_39) -FNDA:0,(anonymous_40) -FNDA:0,(anonymous_41) -FNDA:0,(anonymous_42) -FNDA:0,(anonymous_43) -DA:4,0 -DA:5,0 -DA:6,0 -DA:7,0 -DA:8,0 -DA:9,0 -DA:13,0 -DA:14,0 -DA:15,0 -DA:18,0 -DA:19,0 -DA:20,0 -DA:22,0 -DA:23,0 -DA:28,0 -DA:29,0 -DA:30,0 -DA:41,0 -DA:42,0 -DA:72,0 -DA:73,0 -DA:76,0 -DA:80,0 -DA:81,0 -DA:82,0 -DA:84,0 -DA:85,0 -DA:86,0 -DA:90,0 -DA:93,0 -DA:94,0 -DA:95,0 -DA:98,0 -DA:99,0 -DA:102,0 -DA:106,0 -DA:107,0 -DA:108,0 -DA:110,0 -DA:111,0 -DA:115,0 -DA:116,0 -DA:121,0 -DA:122,0 -DA:123,0 -DA:125,0 -DA:128,0 -DA:129,0 -DA:130,0 -DA:131,0 -DA:134,0 -DA:135,0 -DA:136,0 -DA:137,0 -DA:140,0 -DA:147,0 -DA:148,0 -DA:151,0 -DA:152,0 -DA:153,0 -DA:167,0 -DA:168,0 -DA:169,0 -DA:170,0 -DA:172,0 -DA:177,0 -DA:178,0 -DA:184,0 -DA:185,0 -DA:186,0 -DA:187,0 -DA:191,0 -DA:194,0 -DA:195,0 -DA:198,0 -DA:199,0 -DA:201,0 -DA:203,0 -DA:204,0 -DA:209,0 -DA:210,0 -DA:211,0 -DA:216,0 -DA:218,0 -DA:221,0 -DA:249,0 -DA:250,0 -DA:251,0 -DA:252,0 -DA:253,0 -DA:261,0 -DA:264,0 -DA:265,0 -DA:267,0 -DA:268,0 -DA:274,0 -DA:275,0 -DA:276,0 -DA:277,0 -DA:280,0 -DA:281,0 -DA:289,0 -DA:290,0 -DA:291,0 -DA:292,0 -DA:293,0 -DA:294,0 -DA:297,0 -DA:300,0 -DA:301,0 -DA:302,0 -DA:313,0 -DA:314,0 -DA:315,0 -DA:322,0 -DA:329,0 -DA:330,0 -DA:332,0 -DA:333,0 -DA:334,0 -DA:336,0 -DA:337,0 -DA:339,0 -DA:340,0 -DA:341,0 -DA:342,0 -DA:343,0 -DA:344,0 -DA:346,0 -DA:347,0 -DA:348,0 -DA:349,0 -DA:352,0 -DA:358,0 -DA:361,0 -DA:367,0 -DA:369,0 -DA:370,0 -DA:371,0 -DA:372,0 -DA:373,0 -DA:375,0 -DA:380,0 -DA:396,0 -DA:397,0 -DA:400,0 -DA:411,0 -DA:412,0 -DA:413,0 -DA:414,0 -DA:415,0 -DA:418,0 -DA:421,0 -DA:437,0 -DA:438,0 -DA:441,0 -DA:442,0 -DA:443,0 -DA:445,0 -DA:446,0 -DA:447,0 -DA:448,0 -DA:449,0 -DA:450,0 -DA:452,0 -DA:453,0 -DA:454,0 -DA:455,0 -DA:456,0 -DA:459,0 -DA:461,0 -DA:465,0 -DA:470,0 -DA:471,0 -DA:472,0 -DA:473,0 -DA:476,0 -DA:481,0 -DA:484,0 -DA:485,0 -DA:486,0 -DA:487,0 -DA:490,0 -DA:491,0 -DA:492,0 -DA:493,0 -DA:494,0 -DA:496,0 -DA:500,0 -DA:501,0 -DA:508,0 -DA:517,0 -DA:518,0 -DA:520,0 -DA:521,0 -DA:522,0 -DA:526,0 -DA:527,0 -DA:528,0 -DA:532,0 -DA:542,0 -DA:544,0 -DA:547,0 -DA:550,0 -DA:552,0 -DA:553,0 -DA:554,0 -DA:555,0 -DA:556,0 -DA:557,0 -DA:558,0 -DA:559,0 -DA:560,0 -DA:561,0 -DA:562,0 -DA:566,0 -DA:576,0 -DA:578,0 -DA:588,0 -DA:589,0 -DA:596,0 -DA:597,0 -DA:604,0 -DA:605,0 -DA:612,0 -DA:613,0 -DA:619,0 -DA:629,0 -DA:631,0 -DA:632,0 -DA:639,0 -DA:641,0 -DA:642,0 -DA:643,0 -DA:644,0 -DA:645,0 -DA:646,0 -DA:648,0 -DA:651,0 -DA:663,0 -DA:664,0 -DA:667,0 -DA:668,0 -DA:671,0 -DA:673,0 -DA:677,0 -DA:680,0 -DA:684,0 -DA:687,0 -DA:690,0 -DA:693,0 -DA:696,0 -DA:699,0 -DA:702,0 -DA:705,0 -DA:709,0 -DA:710,0 -DA:711,0 -DA:712,0 -DA:713,0 -DA:714,0 -DA:715,0 -DA:716,0 -DA:718,0 -DA:729,0 -DA:731,0 -DA:732,0 -DA:733,0 -DA:741,0 -DA:743,0 -DA:744,0 -DA:745,0 -DA:747,0 -DA:752,0 -DA:757,0 -DA:758,0 -DA:759,0 -DA:768,0 -DA:769,0 -DA:773,0 -DA:781,0 -DA:784,0 -DA:788,0 -DA:791,0 -DA:794,0 -DA:795,0 -DA:798,0 -DA:799,0 -DA:800,0 -DA:803,0 -DA:804,0 -DA:805,0 -DA:808,0 -DA:809,0 -DA:810,0 -DA:813,0 -DA:814,0 -DA:815,0 -DA:818,0 -DA:819,0 -DA:820,0 -DA:823,0 -DA:824,0 -DA:825,0 -DA:828,0 -DA:829,0 -DA:830,0 -DA:833,0 -DA:834,0 -DA:835,0 -DA:838,0 -DA:839,0 -DA:840,0 -DA:844,0 -DA:845,0 -DA:848,0 -DA:857,0 -DA:858,0 -DA:859,0 -DA:861,0 -DA:862,0 -DA:866,0 -DA:868,0 -DA:869,0 -DA:870,0 -DA:873,0 -DA:874,0 -DA:875,0 -DA:878,0 -DA:879,0 -DA:884,0 -DA:887,0 -DA:890,0 -DA:891,0 -DA:892,0 -DA:895,0 -DA:896,0 -DA:897,0 -DA:898,0 -DA:901,0 -DA:902,0 -DA:903,0 -DA:906,0 -DA:907,0 -DA:910,0 -DA:911,0 -DA:912,0 -DA:916,0 -DA:919,0 -DA:922,0 -DA:923,0 -DA:927,0 -DA:931,0 -DA:932,0 -DA:933,0 -DA:934,0 -LF:356 -LH:0 -BRDA:12,0,0,0 -BRDA:12,1,0,0 -BRDA:13,2,0,0 -BRDA:13,2,1,0 -BRDA:90,3,0,0 -BRDA:90,3,1,0 -BRDA:93,4,0,0 -BRDA:93,4,1,0 -BRDA:106,5,0,0 -BRDA:106,5,1,0 -BRDA:115,6,0,0 -BRDA:115,6,1,0 -BRDA:125,7,0,0 -BRDA:125,7,1,0 -BRDA:125,8,0,0 -BRDA:125,8,1,0 -BRDA:134,9,0,0 -BRDA:134,9,1,0 -BRDA:134,10,0,0 -BRDA:134,10,1,0 -BRDA:143,11,0,0 -BRDA:143,11,1,0 -BRDA:144,12,0,0 -BRDA:144,12,1,0 -BRDA:168,13,0,0 -BRDA:168,13,1,0 -BRDA:169,14,0,0 -BRDA:169,14,1,0 -BRDA:186,15,0,0 -BRDA:186,15,1,0 -BRDA:186,16,0,0 -BRDA:186,16,1,0 -BRDA:194,17,0,0 -BRDA:194,17,1,0 -BRDA:198,18,0,0 -BRDA:198,18,1,0 -BRDA:198,19,0,0 -BRDA:198,19,1,0 -BRDA:203,20,0,0 -BRDA:203,20,1,0 -BRDA:209,21,0,0 -BRDA:209,21,1,0 -BRDA:216,22,0,0 -BRDA:216,22,1,0 -BRDA:216,23,0,0 -BRDA:216,23,1,0 -BRDA:251,24,0,0 -BRDA:251,24,1,0 -BRDA:251,25,0,0 -BRDA:251,25,1,0 -BRDA:251,25,2,0 -BRDA:251,25,3,0 -BRDA:251,25,4,0 -BRDA:280,26,0,0 -BRDA:280,26,1,0 -BRDA:280,27,0,0 -BRDA:280,27,1,0 -BRDA:280,27,2,0 -BRDA:293,28,0,0 -BRDA:293,28,1,0 -BRDA:293,29,0,0 -BRDA:293,29,1,0 -BRDA:293,29,2,0 -BRDA:313,30,0,0 -BRDA:313,30,1,0 -BRDA:313,31,0,0 -BRDA:313,31,1,0 -BRDA:337,32,0,0 -BRDA:337,32,1,0 -BRDA:370,33,0,0 -BRDA:370,33,1,0 -BRDA:372,34,0,0 -BRDA:372,34,1,0 -BRDA:424,35,0,0 -BRDA:424,35,1,0 -BRDA:425,36,0,0 -BRDA:425,36,1,0 -BRDA:428,37,0,0 -BRDA:428,37,1,0 -BRDA:443,38,0,0 -BRDA:443,38,1,0 -BRDA:450,39,0,0 -BRDA:450,39,1,0 -BRDA:450,40,0,0 -BRDA:450,40,1,0 -BRDA:472,41,0,0 -BRDA:472,41,1,0 -BRDA:472,42,0,0 -BRDA:472,42,1,0 -BRDA:491,43,0,0 -BRDA:491,43,1,0 -BRDA:493,44,0,0 -BRDA:493,44,1,0 -BRDA:520,45,0,0 -BRDA:520,45,1,0 -BRDA:521,46,0,0 -BRDA:521,46,1,0 -BRDA:521,47,0,0 -BRDA:521,47,1,0 -BRDA:552,48,0,0 -BRDA:552,48,1,0 -BRDA:553,49,0,0 -BRDA:553,49,1,0 -BRDA:555,50,0,0 -BRDA:555,50,1,0 -BRDA:557,51,0,0 -BRDA:557,51,1,0 -BRDA:557,52,0,0 -BRDA:557,52,1,0 -BRDA:559,53,0,0 -BRDA:559,53,1,0 -BRDA:559,54,0,0 -BRDA:559,54,1,0 -BRDA:561,55,0,0 -BRDA:561,55,1,0 -BRDA:588,56,0,0 -BRDA:588,56,1,0 -BRDA:596,57,0,0 -BRDA:596,57,1,0 -BRDA:604,58,0,0 -BRDA:604,58,1,0 -BRDA:612,59,0,0 -BRDA:612,59,1,0 -BRDA:631,60,0,0 -BRDA:631,60,1,0 -BRDA:641,61,0,0 -BRDA:641,61,1,0 -BRDA:641,62,0,0 -BRDA:641,62,1,0 -BRDA:643,63,0,0 -BRDA:643,63,1,0 -BRDA:645,64,0,0 -BRDA:645,64,1,0 -BRDA:663,65,0,0 -BRDA:663,65,1,0 -BRDA:663,66,0,0 -BRDA:663,66,1,0 -BRDA:671,67,0,0 -BRDA:671,67,1,0 -BRDA:671,67,2,0 -BRDA:671,67,3,0 -BRDA:671,67,4,0 -BRDA:671,67,5,0 -BRDA:671,67,6,0 -BRDA:671,67,7,0 -BRDA:671,67,8,0 -BRDA:671,67,9,0 -BRDA:671,67,10,0 -BRDA:671,67,11,0 -BRDA:671,67,12,0 -BRDA:671,67,13,0 -BRDA:709,68,0,0 -BRDA:709,68,1,0 -BRDA:709,69,0,0 -BRDA:709,69,1,0 -BRDA:711,70,0,0 -BRDA:711,70,1,0 -BRDA:711,71,0,0 -BRDA:711,71,1,0 -BRDA:713,72,0,0 -BRDA:713,72,1,0 -BRDA:713,73,0,0 -BRDA:713,73,1,0 -BRDA:715,74,0,0 -BRDA:715,74,1,0 -BRDA:715,75,0,0 -BRDA:715,75,1,0 -BRDA:732,76,0,0 -BRDA:732,76,1,0 -BRDA:744,77,0,0 -BRDA:744,77,1,0 -BRDA:758,78,0,0 -BRDA:758,78,1,0 -BRDA:794,79,0,0 -BRDA:794,79,1,0 -BRDA:798,80,0,0 -BRDA:798,80,1,0 -BRDA:803,81,0,0 -BRDA:803,81,1,0 -BRDA:808,82,0,0 -BRDA:808,82,1,0 -BRDA:813,83,0,0 -BRDA:813,83,1,0 -BRDA:818,84,0,0 -BRDA:818,84,1,0 -BRDA:823,85,0,0 -BRDA:823,85,1,0 -BRDA:823,86,0,0 -BRDA:823,86,1,0 -BRDA:828,87,0,0 -BRDA:828,87,1,0 -BRDA:828,88,0,0 -BRDA:828,88,1,0 -BRDA:833,89,0,0 -BRDA:833,89,1,0 -BRDA:838,90,0,0 -BRDA:838,90,1,0 -BRDA:861,91,0,0 -BRDA:861,91,1,0 -BRDA:868,92,0,0 -BRDA:868,92,1,0 -BRDA:873,93,0,0 -BRDA:873,93,1,0 -BRDA:892,94,0,0 -BRDA:892,94,1,0 -BRDA:892,94,2,0 -BRDA:892,94,3,0 -BRDA:892,94,4,0 -BRDA:895,95,0,0 -BRDA:895,95,1,0 -BRDA:932,96,0,0 -BRDA:932,96,1,0 -BRDA:933,97,0,0 -BRDA:933,97,1,0 -BRF:214 -BRH:0 -end_of_record -TN: -SF:popup.js -FN:2,(anonymous_0) -FN:19,(anonymous_1) -FN:24,(anonymous_2) -FN:29,(anonymous_3) -FN:54,(anonymous_4) -FN:67,(anonymous_5) -FN:85,toggleButtons -FN:95,showStatus -FN:100,(anonymous_8) -FN:105,checkSelectorState -FN:128,setupTooltipAccessibility -FN:135,(anonymous_11) -FN:150,(anonymous_12) -FN:158,(anonymous_13) -FN:160,(anonymous_14) -FN:171,(anonymous_15) -FN:184,(anonymous_16) -FNF:17 -FNH:0 -FNDA:0,(anonymous_0) -FNDA:0,(anonymous_1) -FNDA:0,(anonymous_2) -FNDA:0,(anonymous_3) -FNDA:0,(anonymous_4) -FNDA:0,(anonymous_5) -FNDA:0,toggleButtons -FNDA:0,showStatus -FNDA:0,(anonymous_8) -FNDA:0,checkSelectorState -FNDA:0,setupTooltipAccessibility -FNDA:0,(anonymous_11) -FNDA:0,(anonymous_12) -FNDA:0,(anonymous_13) -FNDA:0,(anonymous_14) -FNDA:0,(anonymous_15) -FNDA:0,(anonymous_16) -DA:2,0 -DA:3,0 -DA:4,0 -DA:5,0 -DA:6,0 -DA:7,0 -DA:10,0 -DA:11,0 -DA:12,0 -DA:16,0 -DA:19,0 -DA:20,0 -DA:24,0 -DA:25,0 -DA:29,0 -DA:30,0 -DA:33,0 -DA:39,0 -DA:40,0 -DA:43,0 -DA:44,0 -DA:50,0 -DA:51,0 -DA:54,0 -DA:57,0 -DA:58,0 -DA:59,0 -DA:61,0 -DA:67,0 -DA:68,0 -DA:70,0 -DA:71,0 -DA:72,0 -DA:73,0 -DA:75,0 -DA:80,0 -DA:83,0 -DA:86,0 -DA:87,0 -DA:88,0 -DA:90,0 -DA:91,0 -DA:96,0 -DA:97,0 -DA:98,0 -DA:100,0 -DA:101,0 -DA:106,0 -DA:109,0 -DA:113,0 -DA:116,0 -DA:117,0 -DA:118,0 -DA:119,0 -DA:120,0 -DA:124,0 -DA:129,0 -DA:130,0 -DA:132,0 -DA:135,0 -DA:136,0 -DA:137,0 -DA:139,0 -DA:140,0 -DA:141,0 -DA:142,0 -DA:144,0 -DA:145,0 -DA:150,0 -DA:151,0 -DA:152,0 -DA:153,0 -DA:158,0 -DA:160,0 -DA:161,0 -DA:162,0 -DA:163,0 -DA:171,0 -DA:172,0 -DA:173,0 -DA:174,0 -DA:175,0 -DA:177,0 -DA:178,0 -DA:180,0 -DA:181,0 -DA:182,0 -DA:183,0 -DA:184,0 -LF:89 -LH:0 -BRDA:11,0,0,0 -BRDA:11,0,1,0 -BRDA:16,1,0,0 -BRDA:16,1,1,0 -BRDA:33,2,0,0 -BRDA:33,2,1,0 -BRDA:33,3,0,0 -BRDA:33,3,1,0 -BRDA:33,3,2,0 -BRDA:33,3,3,0 -BRDA:33,3,4,0 -BRDA:33,3,5,0 -BRDA:58,4,0,0 -BRDA:58,4,1,0 -BRDA:86,5,0,0 -BRDA:86,5,1,0 -BRDA:95,6,0,0 -BRDA:109,7,0,0 -BRDA:109,7,1,0 -BRDA:109,8,0,0 -BRDA:109,8,1,0 -BRDA:109,8,2,0 -BRDA:109,8,3,0 -BRDA:118,9,0,0 -BRDA:118,9,1,0 -BRDA:118,10,0,0 -BRDA:118,10,1,0 -BRDA:132,11,0,0 -BRDA:132,11,1,0 -BRDA:132,12,0,0 -BRDA:132,12,1,0 -BRDA:136,13,0,0 -BRDA:136,13,1,0 -BRDA:136,14,0,0 -BRDA:136,14,1,0 -BRDA:140,15,0,0 -BRDA:140,15,1,0 -BRDA:141,16,0,0 -BRDA:141,16,1,0 -BRDA:142,17,0,0 -BRDA:142,17,1,0 -BRDA:151,18,0,0 -BRDA:151,18,1,0 -BRDA:161,19,0,0 -BRDA:161,19,1,0 -BRDA:172,20,0,0 -BRDA:172,20,1,0 -BRDA:180,21,0,0 -BRDA:180,21,1,0 -BRF:49 -BRH:0 -end_of_record diff --git a/coverage/popup.js.html b/coverage/popup.js.html deleted file mode 100644 index c932cc4..0000000 --- a/coverage/popup.js.html +++ /dev/null @@ -1,643 +0,0 @@ - - - - - - Code coverage report for popup.js - - - - - - - - - -
-
-

All files popup.js

-
- -
- 0% - Statements - 0/92 -
- - -
- 0% - Branches - 0/49 -
- - -
- 0% - Functions - 0/17 -
- - -
- 0% - Lines - 0/89 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
// Popup script for Chrome extension
-document.addEventListener('DOMContentLoaded', async () => {
-  const backgroundSelect = document.getElementById('background-select');
-  const clipboardToggle = document.getElementById('clipboard-toggle');
-  const startBtn = document.getElementById('start-selector');
-  const stopBtn = document.getElementById('stop-selector');
-  const status = document.getElementById('status');
- 
-  // Load saved preferences
-  const saved = await chrome.storage.sync.get(['backgroundPreference', 'copyToClipboard']);
-  if (saved.backgroundPreference) {
-    backgroundSelect.value = saved.backgroundPreference;
-  }
-  
-  // Set clipboard preference (default to true if not set)
-  clipboardToggle.checked = saved.copyToClipboard !== undefined ? saved.copyToClipboard : true;
- 
-  // Save background preference when changed
-  backgroundSelect.addEventListener('change', () => {
-    chrome.storage.sync.set({ backgroundPreference: backgroundSelect.value });
-  });
- 
-  // Save clipboard preference when changed
-  clipboardToggle.addEventListener('change', () => {
-    chrome.storage.sync.set({ copyToClipboard: clipboardToggle.checked });
-  });
- 
-  // Start selector
-  startBtn.addEventListener('click', async () => {
-    const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
- 
-    // Check if page is compatible
-    if (tab.url.startsWith('chrome://') ||
-        tab.url.startsWith('chrome-extension://') ||
-        tab.url.startsWith('edge://') ||
-        tab.url.startsWith('about:') ||
-        tab.url === 'chrome://newtab/' ||
-        tab.url === 'edge://newtab/') {
-      showStatus('Error: Cannot capture screenshots on this page type.', 'error');
-      return;
-    }
- 
-    try {
-      await chrome.tabs.sendMessage(tab.id, {
-        action: 'startSelector',
-        background: backgroundSelect.value,
-        copyToClipboard: clipboardToggle.checked
-      });
- 
-      showStatus('Selector activated! Hover over elements and click to capture.', 'success');
-      toggleButtons(true);
- 
-      // Auto-close popup after starting
-      setTimeout(() => window.close(), 1500);
- 
-    } catch (error) {
-      console.error('Full error details:', error);
-      if (error.message.includes('Could not establish connection')) {
-        showStatus('Error: Content script not loaded. Try refreshing the page.', 'error');
-      } else {
-        showStatus('Error: Could not activate on this page.', 'error');
-      }
-    }
-  });
- 
-  // Stop selector
-  stopBtn.addEventListener('click', async () => {
-    const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
- 
-    try {
-      await chrome.tabs.sendMessage(tab.id, { action: 'stopSelector' });
-      showStatus('Selector stopped.', 'success');
-      toggleButtons(false);
-    } catch (error) {
-      console.error('Error stopping selector:', error);
-    }
-  });
- 
-  // Check current state
-  checkSelectorState();
- 
-  // Handle tooltip keyboard navigation
-  setupTooltipAccessibility();
- 
-  function toggleButtons(isActive) {
-    if (isActive) {
-      startBtn.style.display = 'none';
-      stopBtn.style.display = 'block';
-    } else {
-      startBtn.style.display = 'block';
-      stopBtn.style.display = 'none';
-    }
-  }
- 
-  function showStatus(message, type = 'success') {
-    status.textContent = message;
-    status.className = `status ${type}`;
-    status.style.display = 'block';
- 
-    setTimeout(() => {
-      status.style.display = 'none';
-    }, 3000);
-  }
- 
-  async function checkSelectorState() {
-    const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
- 
-    // Skip check for incompatible pages
-    if (tab.url.startsWith('chrome://') ||
-        tab.url.startsWith('chrome-extension://') ||
-        tab.url.startsWith('edge://') ||
-        tab.url.startsWith('about:')) {
-      return;
-    }
- 
-    try {
-      const response = await chrome.tabs.sendMessage(tab.id, { action: 'checkState' });
-      if (response && response.isActive) {
-        toggleButtons(true);
-        showStatus('Selector is currently active', 'success');
-      }
-    } catch (error) {
-      // Content script not ready or selector not active
-      console.log('Content script not ready or selector inactive:', error.message);
-    }
-  }
- 
-  function setupTooltipAccessibility() {
-    const tooltipIcon = document.querySelector('.tooltip-icon');
-    const tooltipContent = document.querySelector('.tooltip-content');
-    
-    if (!tooltipIcon || !tooltipContent) return;
- 
-    // Handle keyboard events for tooltip
-    tooltipIcon.addEventListener('keydown', (e) => {
-      if (e.key === 'Enter' || e.key === ' ') {
-        e.preventDefault();
-        // Toggle tooltip visibility on Enter/Space
-        const isVisible = tooltipContent.style.visibility === 'visible';
-        tooltipContent.style.visibility = isVisible ? 'hidden' : 'visible';
-        tooltipContent.style.opacity = isVisible ? '0' : '1';
-      } else if (e.key === 'Escape') {
-        // Hide tooltip on Escape
-        tooltipContent.style.visibility = 'hidden';
-        tooltipContent.style.opacity = '0';
-      }
-    });
- 
-    // Hide tooltip when clicking outside
-    document.addEventListener('click', (e) => {
-      if (!tooltipIcon.contains(e.target)) {
-        tooltipContent.style.visibility = 'hidden';
-        tooltipContent.style.opacity = '0';
-      }
-    });
- 
-    // Handle focus loss
-    tooltipIcon.addEventListener('blur', (e) => {
-      // Small delay to allow for focus to move to tooltip content if needed
-      setTimeout(() => {
-        if (!tooltipIcon.matches(':focus-within')) {
-          tooltipContent.style.visibility = 'hidden';
-          tooltipContent.style.opacity = '0';
-        }
-      }, 100);
-    });
-  }
-});
- 
-// Listen for messages from content script
-chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
-  if (message.action === 'selectorStopped') {
-    const startBtn = document.getElementById('start-selector');
-    const stopBtn = document.getElementById('stop-selector');
-    const status = document.getElementById('status');
- 
-    startBtn.style.display = 'block';
-    stopBtn.style.display = 'none';
- 
-    if (message.reason === 'screenshot-taken') {
-      status.textContent = 'Screenshot captured!';
-      status.className = 'status success';
-      status.style.display = 'block';
-      setTimeout(() => status.style.display = 'none', 2000);
-    }
-  }
-});
- -
-
- - - - - - - - \ No newline at end of file diff --git a/coverage/prettify.css b/coverage/prettify.css deleted file mode 100644 index b317a7c..0000000 --- a/coverage/prettify.css +++ /dev/null @@ -1 +0,0 @@ -.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} diff --git a/coverage/prettify.js b/coverage/prettify.js deleted file mode 100644 index b322523..0000000 --- a/coverage/prettify.js +++ /dev/null @@ -1,2 +0,0 @@ -/* eslint-disable */ -window.PR_SHOULD_USE_CONTINUATION=true;(function(){var h=["break,continue,do,else,for,if,return,while"];var u=[h,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"];var p=[u,"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"];var l=[p,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"];var x=[p,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"];var R=[x,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"];var r="all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes";var w=[p,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"];var s="caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END";var I=[h,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"];var f=[h,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"];var H=[h,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"];var A=[l,R,w,s+I,f,H];var e=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/;var C="str";var z="kwd";var j="com";var O="typ";var G="lit";var L="pun";var F="pln";var m="tag";var E="dec";var J="src";var P="atn";var n="atv";var N="nocode";var M="(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*";function k(Z){var ad=0;var S=false;var ac=false;for(var V=0,U=Z.length;V122)){if(!(al<65||ag>90)){af.push([Math.max(65,ag)|32,Math.min(al,90)|32])}if(!(al<97||ag>122)){af.push([Math.max(97,ag)&~32,Math.min(al,122)&~32])}}}}af.sort(function(av,au){return(av[0]-au[0])||(au[1]-av[1])});var ai=[];var ap=[NaN,NaN];for(var ar=0;arat[0]){if(at[1]+1>at[0]){an.push("-")}an.push(T(at[1]))}}an.push("]");return an.join("")}function W(al){var aj=al.source.match(new RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g"));var ah=aj.length;var an=[];for(var ak=0,am=0;ak=2&&ai==="["){aj[ak]=X(ag)}else{if(ai!=="\\"){aj[ak]=ag.replace(/[a-zA-Z]/g,function(ao){var ap=ao.charCodeAt(0);return"["+String.fromCharCode(ap&~32,ap|32)+"]"})}}}}return aj.join("")}var aa=[];for(var V=0,U=Z.length;V=0;){S[ac.charAt(ae)]=Y}}var af=Y[1];var aa=""+af;if(!ag.hasOwnProperty(aa)){ah.push(af);ag[aa]=null}}ah.push(/[\0-\uffff]/);V=k(ah)})();var X=T.length;var W=function(ah){var Z=ah.sourceCode,Y=ah.basePos;var ad=[Y,F];var af=0;var an=Z.match(V)||[];var aj={};for(var ae=0,aq=an.length;ae=5&&"lang-"===ap.substring(0,5);if(am&&!(ai&&typeof ai[1]==="string")){am=false;ap=J}if(!am){aj[ag]=ap}}var ab=af;af+=ag.length;if(!am){ad.push(Y+ab,ap)}else{var al=ai[1];var ak=ag.indexOf(al);var ac=ak+al.length;if(ai[2]){ac=ag.length-ai[2].length;ak=ac-al.length}var ar=ap.substring(5);B(Y+ab,ag.substring(0,ak),W,ad);B(Y+ab+ak,al,q(ar,al),ad);B(Y+ab+ac,ag.substring(ac),W,ad)}}ah.decorations=ad};return W}function i(T){var W=[],S=[];if(T.tripleQuotedStrings){W.push([C,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,null,"'\""])}else{if(T.multiLineStrings){W.push([C,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"])}else{W.push([C,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"])}}if(T.verbatimStrings){S.push([C,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null])}var Y=T.hashComments;if(Y){if(T.cStyleComments){if(Y>1){W.push([j,/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,null,"#"])}else{W.push([j,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"])}S.push([C,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,null])}else{W.push([j,/^#[^\r\n]*/,null,"#"])}}if(T.cStyleComments){S.push([j,/^\/\/[^\r\n]*/,null]);S.push([j,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}if(T.regexLiterals){var X=("/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/");S.push(["lang-regex",new RegExp("^"+M+"("+X+")")])}var V=T.types;if(V){S.push([O,V])}var U=(""+T.keywords).replace(/^ | $/g,"");if(U.length){S.push([z,new RegExp("^(?:"+U.replace(/[\s,]+/g,"|")+")\\b"),null])}W.push([F,/^\s+/,null," \r\n\t\xA0"]);S.push([G,/^@[a-z_$][a-z_$@0-9]*/i,null],[O,/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],[F,/^[a-z_$][a-z_$@0-9]*/i,null],[G,new RegExp("^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*","i"),null,"0123456789"],[F,/^\\[\s\S]?/,null],[L,/^.[^\s\w\.$@\'\"\`\/\#\\]*/,null]);return g(W,S)}var K=i({keywords:A,hashComments:true,cStyleComments:true,multiLineStrings:true,regexLiterals:true});function Q(V,ag){var U=/(?:^|\s)nocode(?:\s|$)/;var ab=/\r\n?|\n/;var ac=V.ownerDocument;var S;if(V.currentStyle){S=V.currentStyle.whiteSpace}else{if(window.getComputedStyle){S=ac.defaultView.getComputedStyle(V,null).getPropertyValue("white-space")}}var Z=S&&"pre"===S.substring(0,3);var af=ac.createElement("LI");while(V.firstChild){af.appendChild(V.firstChild)}var W=[af];function ae(al){switch(al.nodeType){case 1:if(U.test(al.className)){break}if("BR"===al.nodeName){ad(al);if(al.parentNode){al.parentNode.removeChild(al)}}else{for(var an=al.firstChild;an;an=an.nextSibling){ae(an)}}break;case 3:case 4:if(Z){var am=al.nodeValue;var aj=am.match(ab);if(aj){var ai=am.substring(0,aj.index);al.nodeValue=ai;var ah=am.substring(aj.index+aj[0].length);if(ah){var ak=al.parentNode;ak.insertBefore(ac.createTextNode(ah),al.nextSibling)}ad(al);if(!ai){al.parentNode.removeChild(al)}}}break}}function ad(ak){while(!ak.nextSibling){ak=ak.parentNode;if(!ak){return}}function ai(al,ar){var aq=ar?al.cloneNode(false):al;var ao=al.parentNode;if(ao){var ap=ai(ao,1);var an=al.nextSibling;ap.appendChild(aq);for(var am=an;am;am=an){an=am.nextSibling;ap.appendChild(am)}}return aq}var ah=ai(ak.nextSibling,0);for(var aj;(aj=ah.parentNode)&&aj.nodeType===1;){ah=aj}W.push(ah)}for(var Y=0;Y=S){ah+=2}if(V>=ap){Z+=2}}}var t={};function c(U,V){for(var S=V.length;--S>=0;){var T=V[S];if(!t.hasOwnProperty(T)){t[T]=U}else{if(window.console){console.warn("cannot override language handler %s",T)}}}}function q(T,S){if(!(T&&t.hasOwnProperty(T))){T=/^\s*]*(?:>|$)/],[j,/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],[L,/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);c(g([[F,/^[\s]+/,null," \t\r\n"],[n,/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[[m,/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],[P,/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],[L,/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);c(g([],[[n,/^[\s\S]+/]]),["uq.val"]);c(i({keywords:l,hashComments:true,cStyleComments:true,types:e}),["c","cc","cpp","cxx","cyc","m"]);c(i({keywords:"null,true,false"}),["json"]);c(i({keywords:R,hashComments:true,cStyleComments:true,verbatimStrings:true,types:e}),["cs"]);c(i({keywords:x,cStyleComments:true}),["java"]);c(i({keywords:H,hashComments:true,multiLineStrings:true}),["bsh","csh","sh"]);c(i({keywords:I,hashComments:true,multiLineStrings:true,tripleQuotedStrings:true}),["cv","py"]);c(i({keywords:s,hashComments:true,multiLineStrings:true,regexLiterals:true}),["perl","pl","pm"]);c(i({keywords:f,hashComments:true,multiLineStrings:true,regexLiterals:true}),["rb"]);c(i({keywords:w,cStyleComments:true,regexLiterals:true}),["js"]);c(i({keywords:r,hashComments:3,cStyleComments:true,multilineStrings:true,tripleQuotedStrings:true,regexLiterals:true}),["coffee"]);c(g([],[[C,/^[\s\S]+/]]),["regex"]);function d(V){var U=V.langExtension;try{var S=a(V.sourceNode);var T=S.sourceCode;V.sourceCode=T;V.spans=S.spans;V.basePos=0;q(U,T)(V);D(V)}catch(W){if("console" in window){console.log(W&&W.stack?W.stack:W)}}}function y(W,V,U){var S=document.createElement("PRE");S.innerHTML=W;if(U){Q(S,U)}var T={langExtension:V,numberLines:U,sourceNode:S};d(T);return S.innerHTML}function b(ad){function Y(af){return document.getElementsByTagName(af)}var ac=[Y("pre"),Y("code"),Y("xmp")];var T=[];for(var aa=0;aa=0){var ah=ai.match(ab);var am;if(!ah&&(am=o(aj))&&"CODE"===am.tagName){ah=am.className.match(ab)}if(ah){ah=ah[1]}var al=false;for(var ak=aj.parentNode;ak;ak=ak.parentNode){if((ak.tagName==="pre"||ak.tagName==="code"||ak.tagName==="xmp")&&ak.className&&ak.className.indexOf("prettyprint")>=0){al=true;break}}if(!al){var af=aj.className.match(/\blinenums\b(?::(\d+))?/);af=af?af[1]&&af[1].length?+af[1]:true:false;if(af){Q(aj,af)}S={langExtension:ah,sourceNode:aj,numberLines:af};d(S)}}}if(X]*(?:>|$)/],[PR.PR_COMMENT,/^<\!--[\s\S]*?(?:-\->|$)/],[PR.PR_PUNCTUATION,/^(?:<[%?]|[%?]>)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-handlebars",/^]*type\s*=\s*['"]?text\/x-handlebars-template['"]?\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i],[PR.PR_DECLARATION,/^{{[#^>/]?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{&?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{{>?\s*[\w.][^}]*}}}/],[PR.PR_COMMENT,/^{{![^}]*}}/]]),["handlebars","hbs"]);PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[ \t\r\n\f]+/,null," \t\r\n\f"]],[[PR.PR_STRING,/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],[PR.PR_STRING,/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']*)\)/i],[PR.PR_KEYWORD,/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],[PR.PR_COMMENT,/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//],[PR.PR_COMMENT,/^(?:)/],[PR.PR_LITERAL,/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],[PR.PR_LITERAL,/^#(?:[0-9a-f]{3}){1,2}/i],[PR.PR_PLAIN,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i],[PR.PR_PUNCTUATION,/^[^\s\w\'\"]+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_KEYWORD,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_STRING,/^[^\)\"\']+/]]),["css-str"]); diff --git a/coverage/sort-arrow-sprite.png b/coverage/sort-arrow-sprite.png deleted file mode 100644 index 6ed68316eb3f65dec9063332d2f69bf3093bbfab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 138 zcmeAS@N?(olHy`uVBq!ia0vp^>_9Bd!3HEZxJ@+%Qh}Z>jv*C{$p!i!8j}?a+@3A= zIAGwzjijN=FBi!|L1t?LM;Q;gkwn>2cAy-KV{dn nf0J1DIvEHQu*n~6U}x}qyky7vi4|9XhBJ7&`njxgN@xNA8m%nc diff --git a/coverage/sorter.js b/coverage/sorter.js deleted file mode 100644 index 2bb296a..0000000 --- a/coverage/sorter.js +++ /dev/null @@ -1,196 +0,0 @@ -/* eslint-disable */ -var addSorting = (function() { - 'use strict'; - var cols, - currentSort = { - index: 0, - desc: false - }; - - // returns the summary table element - function getTable() { - return document.querySelector('.coverage-summary'); - } - // returns the thead element of the summary table - function getTableHeader() { - return getTable().querySelector('thead tr'); - } - // returns the tbody element of the summary table - function getTableBody() { - return getTable().querySelector('tbody'); - } - // returns the th element for nth column - function getNthColumn(n) { - return getTableHeader().querySelectorAll('th')[n]; - } - - function onFilterInput() { - const searchValue = document.getElementById('fileSearch').value; - const rows = document.getElementsByTagName('tbody')[0].children; - for (let i = 0; i < rows.length; i++) { - const row = rows[i]; - if ( - row.textContent - .toLowerCase() - .includes(searchValue.toLowerCase()) - ) { - row.style.display = ''; - } else { - row.style.display = 'none'; - } - } - } - - // loads the search box - function addSearchBox() { - var template = document.getElementById('filterTemplate'); - var templateClone = template.content.cloneNode(true); - templateClone.getElementById('fileSearch').oninput = onFilterInput; - template.parentElement.appendChild(templateClone); - } - - // loads all columns - function loadColumns() { - var colNodes = getTableHeader().querySelectorAll('th'), - colNode, - cols = [], - col, - i; - - for (i = 0; i < colNodes.length; i += 1) { - colNode = colNodes[i]; - col = { - key: colNode.getAttribute('data-col'), - sortable: !colNode.getAttribute('data-nosort'), - type: colNode.getAttribute('data-type') || 'string' - }; - cols.push(col); - if (col.sortable) { - col.defaultDescSort = col.type === 'number'; - colNode.innerHTML = - colNode.innerHTML + ''; - } - } - return cols; - } - // attaches a data attribute to every tr element with an object - // of data values keyed by column name - function loadRowData(tableRow) { - var tableCols = tableRow.querySelectorAll('td'), - colNode, - col, - data = {}, - i, - val; - for (i = 0; i < tableCols.length; i += 1) { - colNode = tableCols[i]; - col = cols[i]; - val = colNode.getAttribute('data-value'); - if (col.type === 'number') { - val = Number(val); - } - data[col.key] = val; - } - return data; - } - // loads all row data - function loadData() { - var rows = getTableBody().querySelectorAll('tr'), - i; - - for (i = 0; i < rows.length; i += 1) { - rows[i].data = loadRowData(rows[i]); - } - } - // sorts the table using the data for the ith column - function sortByIndex(index, desc) { - var key = cols[index].key, - sorter = function(a, b) { - a = a.data[key]; - b = b.data[key]; - return a < b ? -1 : a > b ? 1 : 0; - }, - finalSorter = sorter, - tableBody = document.querySelector('.coverage-summary tbody'), - rowNodes = tableBody.querySelectorAll('tr'), - rows = [], - i; - - if (desc) { - finalSorter = function(a, b) { - return -1 * sorter(a, b); - }; - } - - for (i = 0; i < rowNodes.length; i += 1) { - rows.push(rowNodes[i]); - tableBody.removeChild(rowNodes[i]); - } - - rows.sort(finalSorter); - - for (i = 0; i < rows.length; i += 1) { - tableBody.appendChild(rows[i]); - } - } - // removes sort indicators for current column being sorted - function removeSortIndicators() { - var col = getNthColumn(currentSort.index), - cls = col.className; - - cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, ''); - col.className = cls; - } - // adds sort indicators for current column being sorted - function addSortIndicators() { - getNthColumn(currentSort.index).className += currentSort.desc - ? ' sorted-desc' - : ' sorted'; - } - // adds event listeners for all sorter widgets - function enableUI() { - var i, - el, - ithSorter = function ithSorter(i) { - var col = cols[i]; - - return function() { - var desc = col.defaultDescSort; - - if (currentSort.index === i) { - desc = !currentSort.desc; - } - sortByIndex(i, desc); - removeSortIndicators(); - currentSort.index = i; - currentSort.desc = desc; - addSortIndicators(); - }; - }; - for (i = 0; i < cols.length; i += 1) { - if (cols[i].sortable) { - // add the click event handler on the th so users - // dont have to click on those tiny arrows - el = getNthColumn(i).querySelector('.sorter').parentElement; - if (el.addEventListener) { - el.addEventListener('click', ithSorter(i)); - } else { - el.attachEvent('onclick', ithSorter(i)); - } - } - } - } - // adds sorting functionality to the UI - return function() { - if (!getTable()) { - return; - } - cols = loadColumns(); - loadData(); - addSearchBox(); - addSortIndicators(); - enableUI(); - }; -})(); - -window.addEventListener('load', addSorting); -- 2.49.1 From ff0f993b08cc2505c1f8073b4822a95a42c56a0f Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Wed, 13 Aug 2025 13:28:45 -0600 Subject: [PATCH 10/10] Remove tooltips because they are unnecessary --- popup.html | 100 +++-------------------------------------------------- popup.js | 48 ++----------------------- 2 files changed, 6 insertions(+), 142 deletions(-) diff --git a/popup.html b/popup.html index 66fa1e4..361607e 100644 --- a/popup.html +++ b/popup.html @@ -84,7 +84,6 @@ display: flex; align-items: center; justify-content: space-between; - margin-bottom: 20px; } .toggle-label { @@ -151,69 +150,6 @@ line-height: 1.3; } - .tooltip { - position: relative; - display: inline-block; - margin-left: 6px; - cursor: help; - } - - .tooltip-icon { - width: 14px; - height: 14px; - border-radius: 50%; - background: #6c757d; - color: white; - font-size: 10px; - font-weight: bold; - display: inline-flex; - align-items: center; - justify-content: center; - user-select: none; - } - - .tooltip-content { - visibility: hidden; - width: 280px; - background-color: #333; - color: #fff; - text-align: left; - border-radius: 6px; - padding: 12px; - position: absolute; - z-index: 1000; - bottom: 125%; - left: 50%; - margin-left: -140px; - opacity: 0; - transition: opacity 0.3s, visibility 0.3s; - font-size: 12px; - line-height: 1.4; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); - } - - .tooltip-content::after { - content: ""; - position: absolute; - top: 100%; - left: 50%; - margin-left: -5px; - border-width: 5px; - border-style: solid; - border-color: #333 transparent transparent transparent; - } - - .tooltip:hover .tooltip-content, - .tooltip:focus-within .tooltip-content { - visibility: visible; - opacity: 1; - } - - .tooltip-icon:focus { - outline: 2px solid #007bff; - outline-offset: 2px; - } - .shortcut-info { font-size: 11px; color: #666; @@ -277,21 +213,7 @@
- +
- Automatically download screenshots as PNG files to your computer. Hover over the "?" for more details. + Automatically download screenshots as PNG files to your default downloads folder.
- +
- Automatically copy screenshots to clipboard for quick pasting. Hover over the "?" for more details. + Automatically copy screenshots to your clipboard for quick pasting.
diff --git a/popup.js b/popup.js index 5c6a712..529b7e0 100644 --- a/popup.js +++ b/popup.js @@ -145,8 +145,7 @@ document.addEventListener('DOMContentLoaded', async () => { // Check current state checkSelectorState(); - // Handle tooltip keyboard navigation - setupTooltipAccessibility(); + // Tooltips removed function toggleButtons(isActive) { if (isActive) { @@ -191,50 +190,7 @@ document.addEventListener('DOMContentLoaded', async () => { } } - function setupTooltipAccessibility() { - const tooltips = document.querySelectorAll('.tooltip'); - - tooltips.forEach(tooltip => { - const tooltipIcon = tooltip.querySelector('.tooltip-icon'); - const tooltipContent = tooltip.querySelector('.tooltip-content'); - - if (!tooltipIcon || !tooltipContent) return; - - // Handle keyboard events for tooltip - tooltipIcon.addEventListener('keydown', (e) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - // Toggle tooltip visibility on Enter/Space - const isVisible = tooltipContent.style.visibility === 'visible'; - tooltipContent.style.visibility = isVisible ? 'hidden' : 'visible'; - tooltipContent.style.opacity = isVisible ? '0' : '1'; - } else if (e.key === 'Escape') { - // Hide tooltip on Escape - tooltipContent.style.visibility = 'hidden'; - tooltipContent.style.opacity = '0'; - } - }); - - // Hide tooltip when clicking outside - document.addEventListener('click', (e) => { - if (!tooltip.contains(e.target)) { - tooltipContent.style.visibility = 'hidden'; - tooltipContent.style.opacity = '0'; - } - }); - - // Handle focus loss - tooltipIcon.addEventListener('blur', (e) => { - // Small delay to allow for focus to move to tooltip content if needed - setTimeout(() => { - if (!tooltipIcon.matches(':focus-within')) { - tooltipContent.style.visibility = 'hidden'; - tooltipContent.style.opacity = '0'; - } - }, 100); - }); - }); - } + // Tooltips removed: no setup needed }); // Listen for messages from content script -- 2.49.1