From 7078d64049bbf0f5969ab86f97d0b3f9528ce219 Mon Sep 17 00:00:00 2001 From: "Julien Carion (juca)" Date: Wed, 30 Oct 2024 14:37:21 +0100 Subject: [PATCH] [IMP] devtools: observe variables This commit allows the user to observe variables so that their content will be refreshed every 200ms and will be displayed as long as they are found in the tree. --- .../details_window/details_window.xml | 11 ++ .../object_tree_element.js | 20 ++- .../devtools/src/devtools_app/store/store.js | 40 ++++++ .../page_scripts/owl_devtools_global_hook.js | 115 ++++++++++++++---- 4 files changed, 160 insertions(+), 26 deletions(-) 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);