diff --git a/tools/devtools/src/devtools_app/devtools_window/components_tab/details_window/details_window.xml b/tools/devtools/src/devtools_app/devtools_window/components_tab/details_window/details_window.xml index 34deaaf9..5933efcd 100644 --- a/tools/devtools/src/devtools_app/devtools_window/components_tab/details_window/details_window.xml +++ b/tools/devtools/src/devtools_app/devtools_window/components_tab/details_window/details_window.xml @@ -20,6 +20,17 @@
+
+
+
+ observed variables +
+ +
+ + + +
diff --git a/tools/devtools/src/devtools_app/devtools_window/components_tab/details_window/object_tree_element/object_tree_element.js b/tools/devtools/src/devtools_app/devtools_window/components_tab/details_window/object_tree_element/object_tree_element.js index 9a2b7947..eb818804 100644 --- a/tools/devtools/src/devtools_app/devtools_window/components_tab/details_window/object_tree_element/object_tree_element.js +++ b/tools/devtools/src/devtools_app/devtools_window/components_tab/details_window/object_tree_element/object_tree_element.js @@ -64,6 +64,16 @@ export class ObjectTreeElement extends Component { show: this.props.object.contentType === "function", action: () => this.store.inspectFunctionSource(this.props.object.path), }, + { + title: "Observe variable", + show: this.props.object.objectType !== "observed", + action: () => this.store.observeVariable(this.props.object.path), + }, + { + title: "Unobserve variable", + show: this.props.object.objectType === "observed", + action: () => this.store.clearObservedVariable(this.props.index), + }, { title: "Inject breakpoint on component", show: this.props.object.contentType === "array" && this.props.object.objectType === "hook", @@ -106,10 +116,12 @@ export class ObjectTreeElement extends Component { } setupEditMode() { - if (!this.state.editMode) { - if (!this.props.object.hasChildren) { - this.state.editMode = true; - } + if ( + !this.state.editMode && + !this.props.object.hasChildren && + !(this.props.object.objectType === "observed") + ) { + this.state.editMode = true; } } diff --git a/tools/devtools/src/devtools_app/store/store.js b/tools/devtools/src/devtools_app/store/store.js index 80808b73..b3df8132 100644 --- a/tools/devtools/src/devtools_app/store/store.js +++ b/tools/devtools/src/devtools_app/store/store.js @@ -37,6 +37,7 @@ export const store = reactive({ version: "1.0", }, selectedElement: null, + observedVariables: [], componentSearch: { search: "", searchResults: [], @@ -580,6 +581,27 @@ export const store = reactive({ await this.loadComponentsTree(true); }, + async observeVariable(path) { + this.observedVariables.push({ path: [...path], visible: false }); + this.observedVariables = await evalFunctionInWindow( + "getObservedVariables", + [store.observedVariables], + store.activeFrame + ); + await browserInstance.storage.local.set({ + observedVariables: toRaw(this.observedVariables).map((o) => o.path), + }); + }, + + clearObservedVariable(index) { + if (index !== undefined) { + this.observedVariables.splice(index, 1); + } else { + this.observedVariables = []; + } + browserInstance.storage.local.set({ observedVariables: toRaw(this.observedVariables) }); + }, + // Trigger the highlight on the component in the page highlightComponent(path) { evalFunctionInWindow("highlightComponent", [path], this.activeFrame); @@ -670,6 +692,14 @@ async function init() { } } }, 500); + // Refresh observed variables values every 200 ms + setInterval(async () => { + store.observedVariables = await evalFunctionInWindow( + "getObservedVariables", + [[...store.observedVariables]], + store.activeFrame + ); + }, 200); } let rootRendersTimeout = false; @@ -764,6 +794,16 @@ async function loadSettings() { storage.owlDevtoolsComponentsToggleBlacklist ); } + // Observed variables + if (storage.observedVariables) { + store.observedVariables = []; + for (const path of storage.observedVariables) { + store.observedVariables.push({ + path: [...path], + visible: false, + }); + } + } } // Function to handle and store a batch of events coming from the page diff --git a/tools/devtools/src/page_scripts/owl_devtools_global_hook.js b/tools/devtools/src/page_scripts/owl_devtools_global_hook.js index 7f738a88..0fc0f9d4 100644 --- a/tools/devtools/src/page_scripts/owl_devtools_global_hook.js +++ b/tools/devtools/src/page_scripts/owl_devtools_global_hook.js @@ -143,7 +143,7 @@ result.push(element); } } - if (obj.constructor.name !== "Object") { + if (obj.constructor && obj.constructor.name !== "Object") { return obj.constructor.name + " {" + result.join(", ") + "}"; } return "{" + result.join(", ") + "}"; @@ -703,7 +703,7 @@ try { obj = Object.getOwnPropertyDescriptor(obj, key.value).get.call(obj); } catch (e) { - obj = e.toString(); + obj = "Exception: " + e.toString(); } break; case "prototype getter": @@ -721,13 +721,13 @@ try { obj = Object.getOwnPropertyDescriptor(obj, key.value).get; } catch (e) { - obj = e.toString(); + obj = "Exception: " + e.toString(); } } else { try { obj = obj[key.value]; } catch (e) { - obj = e.toString(); + obj = "Exception: " + e.toString(); } } } @@ -749,9 +749,11 @@ if (index === -1) { return this.getComponentNode(path); } - const componentNode = this.getComponentNode(path.slice(0, index)); - const obj = this.getObject(componentNode, path.slice(index)); - return obj; + const componentNode = this.getComponentNode(index === 1 ? path[0] : path.slice(0, index)); + if (componentNode) { + return this.getObject(componentNode, path.slice(index)); + } + return "Exception: component not found"; } // Returns a modified version of an object node that has compatible format with the devtools ObjectTreeElement component @@ -799,6 +801,11 @@ child.name = "value"; obj = parentObj[1]; break; + case "getter": + child.name = key.value; + obj = parentObj[key.value]; + break; + case "prototype getter": case "set entry": case "map entry": case "item": @@ -814,6 +821,8 @@ parentObj ).findIndex((sym) => sym === key.value); child.path[child.path.length - 1].value = key.value.toString(); + } else if (key.hasOwnProperty("symbolIndex")) { + obj = parentObj[Object.getOwnPropertySymbols(parentObj)[key.symbolIndex]]; } else { obj = parentObj[key.value]; } @@ -845,7 +854,7 @@ child.contentType = "set"; child.hasChildren = true; break; - case obj.constructor.name === "Array": + case obj.constructor?.name === "Array": child.contentType = "array"; child.hasChildren = obj.length > 0; break; @@ -853,11 +862,11 @@ child.contentType = "function"; child.hasChildren = true; break; - case obj instanceof Object: + case typeof obj === "object": child.contentType = "object"; child.hasChildren = - Object.keys(obj).length || - Object.getOwnPropertySymbols(obj).length || + Object.keys(obj).length > 0 || + Object.getOwnPropertySymbols(obj).length > 0 || obj.constructor.name !== "Object"; break; default: @@ -1123,19 +1132,33 @@ } // The second element in the path will always be the root of the app let node = [...this.apps][path[0]]?.root; - if (!node) { - return null; - } - for (let i = 2; i < path.length; i++) { - // From this point onwards, it is an object path inside the component node - if (typeof path[i] !== "string") { - break; - } - if (node.children.hasOwnProperty(path[i])) { - node = node.children[path[i]]; - } else { + if (path instanceof Array) { + if (!node) { return null; } + for (let i = 2; i < path.length; i++) { + // From this point onwards, it is an object path inside the component node + if (typeof path[i] !== "string") { + break; + } + if (node.children.hasOwnProperty(path[i])) { + node = node.children[path[i]]; + } else { + return null; + } + } + } else { + const simplifiedPathArray = path.split("/"); + if (node.name !== simplifiedPathArray[1]) { + return null; + } + for (let i = 2; i < simplifiedPathArray.length; i += 2) { + const key = Reflect.ownKeys(node.children)[simplifiedPathArray[i]]; + node = node.children[key]; + if (node.name !== simplifiedPathArray[i + 1]) { + return null; + } + } } return node; } @@ -1692,6 +1715,37 @@ } return children; } + getObservedVariables(current) { + const res = [...current]; + for (let i = 0; i < current.length; i++) { + const path = current[i].path; + const parent = this.getObjectProperty(path.slice(0, path.length - 1)); + if (parent && !(typeof parent === "string" && parent.startsWith("Exception: "))) { + const result = this.serializeObjectChild( + parent, + path.at(-1), + 0, + "observed", + path.slice(0, path.length - 1), + {}, + {} + ); + result.hasChildren = false; + result.visible = true; + const index = path.findIndex((key) => typeof key !== "string"); + if (index > 1) { + const componentNode = this.getComponentNode(path.slice(0, index)); + result.path = [this.getComponentSimplifiedPath(componentNode)].concat( + path.slice(index) + ); + } + res[i] = result; + } else { + res[i].visible = false; + } + } + return res; + } // Returns the path of the given component node getComponentPath(componentNode) { let path = []; @@ -1708,6 +1762,23 @@ path.unshift(index.toString()); return path; } + // Returns the simplified path of the given component node (using component names and indexes) + getComponentSimplifiedPath(componentNode) { + let path = componentNode.name; + if (componentNode.parentKey) { + while (componentNode.parent) { + const previousKey = componentNode.parentKey; + componentNode = componentNode.parent; + path = `${componentNode.name}/${Reflect.ownKeys(componentNode.children).indexOf( + previousKey + )}/${path}`; + } + } + const appsArray = [...this.apps]; + let index = appsArray.findIndex((app) => app === componentNode.app); + path = index.toString() + (path.length ? `/${path}` : ""); + return path; + } // Store the object into a temp window variable and log it to the console sendObjectToConsole(path) { const obj = this.getObjectProperty(path);