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);