diff --git a/common/js/injected_script.js b/common/js/injected_script.js index 39dba09b6..42cfe3b38 100644 --- a/common/js/injected_script.js +++ b/common/js/injected_script.js @@ -114,7 +114,23 @@ class CSSQueryEngine { class TextQueryEngine { queryAll(root, selector) { - return root.queryAll(selector); + // Remove the `text=` prefix if present + const text = selector.startsWith('text=') ? selector.slice(5) : selector; + + // Query all elements within the root + const elements = Array.from(root.querySelectorAll('*')); + + // Filter elements based on their text content + const e = elements.filter((element) => { + const elementText = element.textContent.trim(); + return elementText.includes(text); + }); + + if (e.length > 0) { + return [e[0]] + } + + return e } } @@ -215,7 +231,7 @@ class RoleQueryEngine { dialog: ["dialog"], img: ["img[alt]"], form: ["form"], - textbox: ["input[type='text']", "textarea"], + textbox: ["input[type='text']", "input[type='email']", "input[type='password']", "textarea"], radio: ["input[type='radio']"], // Add more implicit roles as needed }; diff --git a/common/js/selector_engine.js b/common/js/selector_engine.js index 78feec67f..8788a0186 100644 --- a/common/js/selector_engine.js +++ b/common/js/selector_engine.js @@ -1,30 +1,33 @@ (() => { // Selector Finder Function function findBestSelector(element) { - // 1. Check for `data-testid` - if (element.hasAttribute('data-testid')) { + // Prefer aria-label or aria-labelledby + if (!element.hasAttribute('aria-label')) { + // 1. Check for `data-testid` + if (element.hasAttribute('data-testid')) { return `'[data-testid="${element.getAttribute('data-testid')}"]'`; - } + } - // 2. Check for `id` - if (element.id) { + // 2. Check for `id` + if (element.id) { return `'#${element.id}'`; + } } // 3. Check for role and accessible name (explicit or implicit roles) const role = getRole(element); if (role) { - const name = getAccessibleName(element); - if (name) { - return `'role=${role}[name="${name}"]'`; - } - return `'role=${role}'`; + const name = getAccessibleName(element); + if (name) { + return `'role=${role}[name="${name}"]'`; + } + return `'role=${role}'`; } // 4. Check for visible text const text = element.textContent.trim(); if (text) { - return `'text="${text}"'`; + return `'text="${text}"'`; } // 5. Fallback to XPath @@ -33,64 +36,64 @@ // Helper function to compute the role (explicit or implicit) function getRole(element) { - // Check for explicit role - if (element.hasAttribute('role')) { - return element.getAttribute('role'); - } + // Check for explicit role + if (element.hasAttribute('role')) { + return element.getAttribute('role'); + } + + // Implicit role mapping + const implicitRoles = { + button: ['button', "input[type='button']", "input[type='submit']", "input[type='reset']"], + link: ['a[href]'], + checkbox: ["input[type='checkbox']"], + heading: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'], + dialog: ['dialog'], + img: ['img[alt]'], + textbox: ["input[type='text']", "input[type='email']", "input[type='password']", 'textarea'], + radio: ["input[type='radio']"], + // Add more implicit roles if needed + }; - // Implicit role mapping - const implicitRoles = { - button: ['button', "input[type='button']", "input[type='submit']", "input[type='reset']"], - link: ['a[href]'], - checkbox: ["input[type='checkbox']"], - heading: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'], - dialog: ['dialog'], - img: ['img[alt]'], - textbox: ["input[type='text']", 'textarea'], - radio: ["input[type='radio']"], - // Add more implicit roles if needed - }; - - for (const [role, selectors] of Object.entries(implicitRoles)) { - for (const selector of selectors) { - if (element.matches(selector)) { - return role; - } - } + for (const [role, selectors] of Object.entries(implicitRoles)) { + for (const selector of selectors) { + if (element.matches(selector)) { + return role; + } } + } - return null; + return null; } // Helper function to compute the accessible name of an element function getAccessibleName(element) { - // Prefer aria-label or aria-labelledby - if (element.hasAttribute('aria-label')) { - return element.getAttribute('aria-label'); - } - if (element.hasAttribute('aria-labelledby')) { - const labelId = element.getAttribute('aria-labelledby'); - const labelElement = element.ownerDocument.getElementById(labelId); - return labelElement ? labelElement.textContent.trim() : ''; - } - // Use text content as a fallback - return element.textContent.trim(); + // Prefer aria-label or aria-labelledby + if (element.hasAttribute('aria-label')) { + return element.getAttribute('aria-label'); + } + if (element.hasAttribute('aria-labelledby')) { + const labelId = element.getAttribute('aria-labelledby'); + const labelElement = element.ownerDocument.getElementById(labelId); + return labelElement ? labelElement.textContent.trim() : ''; + } + // Use text content as a fallback + return element.textContent.trim(); } // Helper function to generate XPath as a fallback function generateXPath(element) { - if (element.id) { - return `'//*[@id="${element.id}"]'`; - } - const siblings = Array.from(element.parentNode.children).filter( - (el) => el.nodeName === element.nodeName - ); - const index = siblings.indexOf(element) + 1; - const tagName = element.nodeName.toLowerCase(); - if (element.parentNode === document) { - return `'/${tagName}[${index}]'`; - } - return `'${generateXPath(element.parentNode)}/${tagName}[${index}]'`; + if (element.id) { + return `'//*[@id="${element.id}"]'`; + } + const siblings = Array.from(element.parentNode.children).filter( + (el) => el.nodeName === element.nodeName + ); + const index = siblings.indexOf(element) + 1; + const tagName = element.nodeName.toLowerCase(); + if (element.parentNode === document) { + return `'/${tagName}[${index}]'`; + } + return `'${generateXPath(element.parentNode)}/${tagName}[${index}]'`; } // Highlight and Selector Display @@ -108,28 +111,28 @@ // Helper to copy text to clipboard function copyToClipboard(text) { - navigator.clipboard.writeText(text).then(() => { - console.log(`Copied to clipboard: ${text}`); - showTemporaryMessage('Copied!', selectorOverlay); - }).catch((err) => { - console.error('Failed to copy text: ', err); - showTemporaryMessage('Failed to copy', selectorOverlay); - }); + navigator.clipboard.writeText(text).then(() => { + console.log(`Copied to clipboard: ${text}`); + showTemporaryMessage('Copied!', selectorOverlay); + }).catch((err) => { + console.error('Failed to copy text: ', err); + showTemporaryMessage('Failed to copy', selectorOverlay); + }); } // Show a temporary message in the overlay function showTemporaryMessage(message, overlay) { - const originalText = overlay.textContent; - overlay.textContent = message; - setTimeout(() => { - overlay.textContent = originalText; - }, 1000); // Reset after 1 second + const originalText = overlay.textContent; + overlay.textContent = message; + setTimeout(() => { + overlay.textContent = originalText; + }, 1000); // Reset after 1 second } // Highlight the element and show selector function highlightElement(event) { if (lastHighlightedElement) { - lastHighlightedElement.style.outline = ''; + lastHighlightedElement.style.outline = ''; } const element = event.target; element.style.outline = '2px solid #FF671D'; @@ -143,17 +146,17 @@ // Copy to clipboard on Command + C document.onkeydown = (e) => { - if (e.metaKey && e.key === 'c') { // Press Command + C - e.preventDefault(); - copyToClipboard(selector); - } + if (e.metaKey && e.key === 'c') { // Press Command + C + e.preventDefault(); + copyToClipboard(selector); + } }; } function removeHighlight() { if (lastHighlightedElement) { - lastHighlightedElement.style.outline = ''; - lastHighlightedElement = null; + lastHighlightedElement.style.outline = ''; + lastHighlightedElement = null; } selectorOverlay.textContent = ''; document.onkeydown = null; // Remove keydown listener