Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion lwc-shadowpath/README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
Chrome Extension to generate CssSelector paths for elements encapsulated by our LWC components' Synthetic Shadow DOM.
Attempting to reproduce the functionality of Copy > Copy JS Path that Chrome provides for native Shadow DOM

**Updated to Manifest V3**: This extension has been updated to comply with Chrome Extension Manifest V3 requirements, including removal of unsafe `eval()` usage and proper Content Security Policy.

### Load as Chrome extension by:
1. Clone or download this repository from github.com
2. Go to chrome://extensions/ and toggle the "Developer mode" button.
3. Click "Load Unpacked" and navigate to directory /lwc-shadowpath of this repository.
4. Inspect any element to open Element Inspector, and then open up the LWC ShadowRoot panel by clicking '>>' on the far right.
4. Inspect any element to open Element Inspector, and then open up the LWC ShadowPath panel by clicking '>>' on the far right.
5. Select the element that the obsolete Selenium selector used to select.
6. Copy and paste from the panel's shadowpath field into the console to confirm that it is selecting the desired element (remove the extra surrounding double quotes after pasting).
7. Copy and paste from the panel's java field into existing java test code to replace the obsolete Selenium selector.
Expand Down Expand Up @@ -61,6 +63,18 @@ You do not need to build this repository to use @FindByJS. Instead add this piec
Please replace the version with the currently latest one by checking out
https://repository.sonatype.org/#nexus-search;quick~test-drop-in

### Changelog

#### Version 1.3.0 - Manifest V3 Compliance Update
- **Updated to Manifest V3**: Migrated from deprecated Manifest V2 to Manifest V3
- **Security Improvements**: Removed unsafe `eval()` usage and replaced with safe DOM traversal methods
- **Content Security Policy**: Added proper CSP headers to devtools.html
- **Permissions**: Added required permissions for Manifest V3 compliance (`scripting`, `<all_urls>`)
- **Code Safety**: Replaced dynamic JavaScript evaluation with deterministic DOM selection logic
- **HTML Structure**: Updated devtools.html with proper DOCTYPE and meta tags

**Breaking Changes**: None for end users - the extension functionality remains the same while being compliant with current Chrome extension policies.

### Developing extension
1. When you update code, to see changes you will need to:
1. Click reload on the chrome://extensions page
Expand Down
6 changes: 6 additions & 0 deletions lwc-shadowpath/devtools.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Security-Policy" content="script-src 'self'; object-src 'none';">
<title>LWC JS Path Generator DevTools</title>
</head>
<body>
<script src="devtools.js"></script>
</body>
Expand Down
66 changes: 47 additions & 19 deletions lwc-shadowpath/devtools.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,36 @@ let generateShadowJSPath = function() {
return "document" + expanded.join(".shadowRoot");
}

function executeJSPath(jsPath) {
let currentElement = document;

for (let i = 0; i < jsPath.length; i++) {
const [selector, index] = jsPath[i];

if (index === 0) {
currentElement = currentElement.querySelector(selector);
} else if (index === '*') {
// Return all elements for this selector - used for finding possibilities
const elements = currentElement.querySelectorAll(selector);
return Array.from(elements);
} else {
const elements = currentElement.querySelectorAll(selector);
currentElement = elements[index];
}

if (!currentElement) {
return null;
}

// Move to shadow root if not the last element
if (i < jsPath.length - 1 && currentElement.shadowRoot) {
currentElement = currentElement.shadowRoot;
}
}

return currentElement;
}

function jsPathElementsToShadowPath(jsPath) {
let selectors = [];
jsPath.forEach(function(elem) {
Expand All @@ -83,32 +113,34 @@ let generateShadowJSPath = function() {

/**
* Turn potential path into JavaScript selector.
* Use eval to select between multiple possible top-down paths.
* Use safe DOM traversal instead of eval.
*/
function jsPathFromPath(path) {
if (!path || path.length === 0) return;
let jsPath = [];
// let jsPath = "document";

for (let i = path.length - 1; i >= 0; i--) {
const node = path[i];
const selector = node.selector;
const query = jsPathElementsToQuerySelector(jsPath.concat([[selector, '*']]));
console.log(query);
// const test = query + ".querySelectorAll('" + selector + "')";
const possibilities = eval(query);
if (possibilities.length === 0) {
console.log("Error: Lost my way. No valid paths from " + query);
throw "Lost my way. No valid paths from " + query;
} else if (possibilities.length === 1) { // Selector gives an unique element
// jsPath += ".querySelector('" + selector + "')";
// jspathElements.push(".querySelector('" + selector + "')");

// Execute the path built so far to get current context
const testPath = jsPath.concat([[selector, '*']]);
const possibilities = executeJSPath(testPath);

if (!possibilities || (Array.isArray(possibilities) && possibilities.length === 0)) {
console.log("Error: Lost my way. No valid paths for selector: " + selector);
throw "Lost my way. No valid paths for selector: " + selector;
} else if (!Array.isArray(possibilities)) {
// Single element returned by querySelector
jsPath.push([selector, 0]);
} else if (possibilities.length === 1) {
// Single element in array from querySelectorAll
jsPath.push([selector, 0]);
} else {
// Multiple elements, find the correct index
let found = false;
for (let p = 0; p < possibilities.length; p++) {
if (possibilities[p] === node.elem) {
// jsPath += ".querySelectorAll('" + selector + "')[" + p + "]";
//jspathElements.push(".querySelectorAll('" + selector + "')[" + p + "]");
jsPath.push([selector, p]);
found = true;
break;
Expand All @@ -119,10 +151,6 @@ let generateShadowJSPath = function() {
throw "Could not find way to " + selector;
}
}
// We have not reached the element yet, so add shadowRoot.
// if (i !== 0) {
// jsPath = jsPath + '.shadowRoot';
// }
}
return jsPath;
}
Expand All @@ -144,7 +172,7 @@ let generateShadowJSPath = function() {
* Inspired by: https://chromium.googlesource.com/chromium/src/+/master/chrome/common/extensions/docs/examples/api/devtools/panels/chrome-query
*/
chrome.devtools.panels.elements.createSidebarPane(
"LWC JS Path",
"LWC ShadowPath",
function(sidebar) {
function updateElementProperties() {
sidebar.setExpression("(" + generateShadowJSPath.toString() + ")()");
Expand Down
10 changes: 8 additions & 2 deletions lwc-shadowpath/manifest.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
{
"name": "LWC JS Path Generator",
"version": "1.2",
"version": "1.3.0",
"description": "Generates JS selector path for LWC Synthetic ShadowDOMs.",
"manifest_version": 3,
"devtools_page": "devtools.html",
"icons": {
"16": "images/shadow16.png",
"32": "images/shadow32.png",
"48": "images/shadow48.png",
"128": "images/shadow128.png"
},
"manifest_version": 2
"permissions": [
"scripting"
],
"host_permissions": [
"<all_urls>"
]
}