Compare commits

..

7 Commits

Author SHA1 Message Date
Samuel Degueldre b15327309c [IMP] devtools: introduce basic testing infrastructure 2023-07-05 11:11:47 +02:00
Samuel Degueldre 432ff444a1 [FIX] devtools: fix incorrect path resolution for subscriptions
Since we now omit some observed objects when listing subscriptions, the
index of the subscription is no longer the index of the raw
subscription. This causes issue with the resolution of object paths when
they are inside a subscription. This commit fixes that by adding the
index to the raw subscription.

We also need to adapt the code that traverses the old tree, since the
index of the subscription in the component is no longer the same as the
index of the subscription "child" in the object tree.
2023-06-23 16:15:02 +02:00
Samuel Degueldre aa3c88a6c4 [IMP] devtools: present observed keys in a more compact way
Previously, the keys were displayed separately from the target, on top
of being displayed in bold inside the object when unfolded. This is
redundant, and in most cases you have to unfold the object anyway
because there are more keys observed than can be displayed.

The only "key" that cannot be displayed in bold inside the object is the
"key changes" magic key. This commit replaces it by a small +/- badge on
the right of the object's short content.

This commit also stops displaying observed object that are nested inside
other observed objects at the top level, as it can be confusing, and it
also heuristically gives a name to observed objects: if the observed
object is a property of the component or one of its props it will be
named accordingly, if not it will be named `[unknown]` (which may happen
when reobserving reactive objects inside a service or inside the env,
but is pretty rare in practice).
2023-06-23 14:32:55 +02:00
Julien Carion (juca) 601a98e649 [IMP] devtools: patch app methods only when needed
This commit changes the moment when all methods that need patching
to handle events are actually patched: the complete method of RootFiber
is now patched at devtools startup (when the tab owl tab is opened) and
every other method is patched at first activation of the event recording
functionality. This makes sure that the devtools will have no impact on
performance whatsoever when they are not directly put in use.
2023-06-23 08:51:23 +02:00
Géry Debongnie 23c7d19ef0 [FIX] blockdom: properly merge dynamic class values
Class attributes are managed in a specific way in owl: they are merged
with existing class attributes, and also, they can be defined in
multiple ways (t-att-class, t-attf-class, t-att=['class', ...] and each
of these can be combined together.

Before this commit, the `t-att` syntax was handled as a normal
attribute, and therefore, would not combine as required with existing
classes.

closes #1453
2023-06-20 08:25:36 +02:00
Julien Carion (juca) 59c49b5833 [IMP] devtools: add border for new animation frame
This commit adds a colored border between events in list view when
a new animation frame was created.
2023-06-15 10:05:08 +02:00
Julien Carion (juca) 2cca0bd819 [FIX] devtools: Fix race conditions in tree lodaing
This commit fixes some race conditions that were happening due to the
loading of the tree being sometimes triggered at the same moment as the
DOM patch. This would result in some destroyed component still being
present in the devtools' tree. This commit also ensures that the loading
of the tree is synchronous in regard to the component details loading.
2023-06-14 15:39:22 +02:00
28 changed files with 714 additions and 413 deletions
+2
View File
@@ -1,5 +1,7 @@
/node_modules
/dist
# compiled devtools files
/tools/devtools/assets/*.js
npm-debug.log
+22
View File
@@ -0,0 +1,22 @@
{
"testEnvironment": "jsdom",
"roots": [
"<rootDir>/tests"
],
"setupFiles": [
"./tests/mocks/mockEventTarget.js"
],
"transform": {
"^.+\\.ts?$": "ts-jest"
},
"verbose": false,
"testRegex": "(/tests/.*(test|spec))\\.ts?$",
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"jsx",
"json",
"node"
]
}
+40
View File
@@ -1048,6 +1048,31 @@
"@babel/types": "^7.3.0"
}
},
"@types/chrome": {
"version": "0.0.114",
"resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.114.tgz",
"integrity": "sha512-i7qRr74IrxHtbnrZSKUuP5Uvd5EOKwlwJq/yp7+yTPihOXnPhNQO4Z5bqb1XTnrjdbUKEJicaVVbhcgtRijmLA==",
"dev": true,
"requires": {
"@types/filesystem": "*",
"@types/har-format": "*"
}
},
"@types/filesystem": {
"version": "0.0.32",
"resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.32.tgz",
"integrity": "sha512-Yuf4jR5YYMR2DVgwuCiP11s0xuVRyPKmz8vo6HBY3CGdeMj8af93CFZX+T82+VD1+UqHOxTq31lO7MI7lepBtQ==",
"dev": true,
"requires": {
"@types/filewriter": "*"
}
},
"@types/filewriter": {
"version": "0.0.29",
"resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.29.tgz",
"integrity": "sha512-BsPXH/irW0ht0Ji6iw/jJaK8Lj3FJemon2gvEqHKpCdDCeemHa+rI3WBGq5z7cDMZgoLjY40oninGxqk+8NzNQ==",
"dev": true
},
"@types/fs-extra": {
"version": "8.1.2",
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.2.tgz",
@@ -1076,6 +1101,12 @@
"@types/node": "*"
}
},
"@types/har-format": {
"version": "1.2.11",
"resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.11.tgz",
"integrity": "sha512-T232/TneofqK30AD1LRrrf8KnjLvzrjWDp7eWST5KoiSzrBfRsLrWDPk4STQPW4NZG6v2MltnduBVmakbZOBIQ==",
"dev": true
},
"@types/istanbul-lib-coverage": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz",
@@ -3338,6 +3369,15 @@
}
}
},
"jest-chrome": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/jest-chrome/-/jest-chrome-0.8.0.tgz",
"integrity": "sha512-39RR1GT9nI4e4jsuH1vIf4l5ApxxkcstjGJr+GsOURL8f4Db0UlbRnsZaM+ZRniaGtokqklUH5VFKGZZ6YztUg==",
"dev": true,
"requires": {
"@types/chrome": "^0.0.114"
}
},
"jest-circus": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-27.5.1.tgz",
+9 -28
View File
@@ -21,15 +21,19 @@
"dev:devtools-firefox": "npm run build:devtools -- --config-browser=firefox",
"build:devtools-chrome": "npm run dev:devtools-chrome -- --config-env=production",
"build:devtools-firefox": "npm run dev:devtools-firefox -- --config-env=production",
"test": "jest",
"test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand --watch --testTimeout=5000000",
"test:watch": "jest --watch",
"test-owl": "jest",
"test-owl:debug": "node --inspect-brk node_modules/.bin/jest --runInBand --watch --testTimeout=5000000",
"test-owl:watch": "jest --watch",
"test-devtools": "npm run build:devtools-chrome && jest --config=tools/devtools/tests/jest.config.json",
"test-devtools:debug": "npm run build:devtools-chrome && node --inspect-brk node_modules/.bin/jest --config=tools/devtools/tests/jest.config.json --runInBand --watch --testTimeout=5000000",
"test-devtools:watch": "jest --config=tools/devtools/tests/jest.config.json --watch",
"test": "npm run test-owl && npm run test-devtools",
"playground:serve": "python3 tools/playground_server.py || python tools/playground_server.py",
"playground": "npm run build && npm run playground:serve",
"preplayground:watch": "npm run build",
"playground:watch": "npm-run-all --parallel playground:serve \"build:* -- --watch\"",
"prettier": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md,tools/devtools/**/*.js} --write",
"check-formatting": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md,tools/devtools/**/*.js} --check",
"check-formatting": "npm run prettier -- --check",
"lint": "eslint src/**/*.ts tests/**/*.ts",
"release": "node tools/release.js",
"compile_templates": "node tools/compile_xml.js"
@@ -55,13 +59,13 @@
"git-rev-sync": "^3.0.2",
"github-api": "^3.3.0",
"jest": "^27.1.0",
"jest-chrome": "^0.8.0",
"jest-diff": "^27.3.1",
"jest-environment-jsdom": "^27.1.0",
"npm-run-all": "^4.1.5",
"prettier": "2.4.1",
"rollup": "^2.56.3",
"rollup-plugin-copy": "^3.3.0",
"rollup-plugin-delete": "^2.0.0",
"rollup-plugin-dts": "^4.2.2",
"rollup-plugin-execute": "^1.1.1",
"rollup-plugin-string": "^3.0.0",
@@ -71,29 +75,6 @@
"ts-jest": "^27.0.5",
"typescript": "4.5.2"
},
"jest": {
"testEnvironment": "jsdom",
"roots": [
"<rootDir>/src",
"<rootDir>/tests"
],
"setupFiles": [
"./tests/mocks/mockEventTarget.js"
],
"transform": {
"^.+\\.ts?$": "ts-jest"
},
"verbose": false,
"testRegex": "(/tests/.*(test|spec))\\.ts?$",
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"jsx",
"json",
"node"
]
},
"prettier": {
"printWidth": 100,
"endOfLine": "auto"
+25 -5
View File
@@ -36,10 +36,18 @@ export function createAttrUpdater(attr: string): Setter<HTMLElement> {
export function attrsSetter(this: HTMLElement, attrs: any) {
if (isArray(attrs)) {
setAttribute.call(this, attrs[0], attrs[1]);
if (attrs[0] === "class") {
setClass.call(this, attrs[1]);
} else {
setAttribute.call(this, attrs[0], attrs[1]);
}
} else {
for (let k in attrs) {
setAttribute.call(this, k, attrs[k]);
if (k === "class") {
setClass.call(this, attrs[k]);
} else {
setAttribute.call(this, k, attrs[k]);
}
}
}
}
@@ -52,7 +60,11 @@ export function attrsUpdater(this: HTMLElement, attrs: any, oldAttrs: any) {
if (val === oldAttrs[1]) {
return;
}
setAttribute.call(this, name, val);
if (name === "class") {
updateClass.call(this, val, oldAttrs[1]);
} else {
setAttribute.call(this, name, val);
}
} else {
removeAttribute.call(this, oldAttrs[0]);
setAttribute.call(this, name, val);
@@ -60,13 +72,21 @@ export function attrsUpdater(this: HTMLElement, attrs: any, oldAttrs: any) {
} else {
for (let k in oldAttrs) {
if (!(k in attrs)) {
removeAttribute.call(this, k);
if (k === "class") {
updateClass.call(this, "", oldAttrs[k]);
} else {
removeAttribute.call(this, k);
}
}
}
for (let k in attrs) {
const val = attrs[k];
if (val !== oldAttrs[k]) {
setAttribute.call(this, k, val);
if (k === "class") {
updateClass.call(this, val, oldAttrs[k]);
} else {
setAttribute.call(this, k, val);
}
}
}
}
+2 -14
View File
@@ -8,12 +8,6 @@ import { OwlError } from "./error_handling";
import type { ComponentNode } from "./component_node";
const ObjectCreate = Object.create;
const ObjectGetPrototypeOf = Object.getPrototypeOf;
const ObjectGetOwnPropertyDescriptors = Object.getOwnPropertyDescriptors;
const ObjectDefineProperty = Object.defineProperty;
const ObjectEntries = Object.entries;
const hasOwnProperty = (obj: Object, prop: PropertyKey) =>
Object.prototype.hasOwnProperty.call(obj, prop);
/**
* This file contains utility functions that will be injected in each template,
* to perform various useful tasks in the compiled code.
@@ -55,14 +49,8 @@ function callSlot(
function capture(ctx: any): any {
const result = ObjectCreate(ctx);
let current = ctx;
while (current && current !== Object.prototype) {
for (const [key, descriptor] of ObjectEntries(ObjectGetOwnPropertyDescriptors(current))) {
if (!hasOwnProperty(result, key) && "value" in descriptor) {
ObjectDefineProperty(result, key, descriptor);
}
}
current = ObjectGetPrototypeOf(current);
for (let k in ctx) {
result[k] = ctx[k];
}
return result;
}
+31
View File
@@ -145,3 +145,34 @@ test("class attribute (with a preexisting value", async () => {
patch(tree, block([""]));
expect(fixture.innerHTML).toBe(`<div class="tomato"></div>`);
});
test("block-class attributes with preexisting class attribute", async () => {
const block = createBlock('<div block-attributes="0" class="owl"></div>');
const tree = block([{ class: "eagle" }]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe(`<div class="owl eagle"></div>`);
patch(tree, block([{ class: "falcon" }]));
expect(fixture.innerHTML).toBe(`<div class="owl falcon"></div>`);
patch(tree, block([{}]));
expect(fixture.innerHTML).toBe(`<div class="owl"></div>`);
});
test("block-class attributes (array syntax) with preexisting class attribute", async () => {
const block = createBlock('<div block-attributes="0" class="owl"></div>');
const tree = block([["class", "eagle"]]);
mount(tree, fixture);
expect(fixture.innerHTML).toBe(`<div class="owl eagle"></div>`);
patch(tree, block([["class", "falcon"]]));
expect(fixture.innerHTML).toBe(`<div class="owl falcon"></div>`);
patch(tree, block([["class", ""]]));
expect(fixture.innerHTML).toBe(`<div class="owl"></div>`);
patch(tree, block([["class", "buzzard"]]));
expect(fixture.innerHTML).toBe(`<div class="owl buzzard"></div>`);
});
@@ -707,6 +707,123 @@ exports[`attributes updating classes (with obj notation) 1`] = `
}"
`;
exports[`attributes various combinations of class, t-att-class, and t-att 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div block-attributes=\\"0\\" class=\\"c\\">content</div>\`);
return function template(ctx, node, key = \\"\\") {
let attr1 = {class:'a'};
return block1([attr1]);
}
}"
`;
exports[`attributes various combinations of class, t-att-class, and t-att 2`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div block-attributes=\\"0\\" block-attribute-1=\\"class\\" class=\\"c\\">content</div>\`);
return function template(ctx, node, key = \\"\\") {
let attr1 = {class:'a'};
let attr2 = {'b':true};
return block1([attr1, attr2]);
}
}"
`;
exports[`attributes various combinations of class, t-att-class, and t-att 3`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div block-attributes=\\"0\\" class=\\"c\\" block-attribute-1=\\"class\\">content</div>\`);
return function template(ctx, node, key = \\"\\") {
let attr1 = {class:'a'};
let attr2 = {'b':true};
return block1([attr1, attr2]);
}
}"
`;
exports[`attributes various combinations of class, t-att-class, and t-att 4`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div class=\\"c\\" block-attributes=\\"0\\" block-attribute-1=\\"class\\">content</div>\`);
return function template(ctx, node, key = \\"\\") {
let attr1 = {class:'a'};
let attr2 = {'b':true};
return block1([attr1, attr2]);
}
}"
`;
exports[`attributes various combinations of class, t-att-class, and t-att 5`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div class=\\"c\\" block-attribute-0=\\"class\\" block-attributes=\\"1\\">content</div>\`);
return function template(ctx, node, key = \\"\\") {
let attr1 = {'b':true};
let attr2 = {class:'a'};
return block1([attr1, attr2]);
}
}"
`;
exports[`attributes various combinations of class, t-att-class, and t-att 6`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div class=\\"c\\" block-attribute-0=\\"class\\">content</div>\`);
return function template(ctx, node, key = \\"\\") {
let attr1 = {'b':true};
return block1([attr1]);
}
}"
`;
exports[`attributes various combinations of class, t-att-class, and t-att 7`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div class=\\"c\\" block-attribute-0=\\"class\\">content</div>\`);
return function template(ctx, node, key = \\"\\") {
let attr1 = ('b');
return block1([attr1]);
}
}"
`;
exports[`attributes various combinations of class, t-att-class, and t-att 8`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div block-attributes=\\"0\\" block-attribute-1=\\"class\\">content</div>\`);
return function template(ctx, node, key = \\"\\") {
let attr1 = {class:'a'};
let attr2 = {'b':true};
return block1([attr1, attr2]);
}
}"
`;
exports[`attributes various escapes 1`] = `
"function anonymous(app, bdom, helpers
) {
+29
View File
@@ -371,4 +371,33 @@ describe("attributes", () => {
// not sure about this. maybe we want to remove the attribute?
expect(fixture.innerHTML).toBe('<div class="hoy a b"></div>');
});
test("various combinations of class, t-att-class, and t-att", () => {
const template1 = `<div t-att="{ class: 'a' }" class="c">content</div>`;
expect(renderToString(template1)).toBe('<div class="c a">content</div>');
const template2 = `<div t-att="{ class: 'a' }" t-att-class="{'b': true}" class="c">content</div>`;
expect(renderToString(template2)).toBe('<div class="c a b">content</div>');
const template3 = `<div t-att="{ class: 'a' }" class="c" t-att-class="{'b': true}">content</div>`;
expect(renderToString(template3)).toBe('<div class="c a b">content</div>');
const template4 = `<div class="c" t-att="{ class: 'a' }" t-att-class="{'b': true}">content</div>`;
expect(renderToString(template4)).toBe('<div class="c a b">content</div>');
const template5 = `<div class="c" t-att-class="{'b': true}" t-att="{ class: 'a' }">content</div>`;
expect(renderToString(template5)).toBe('<div class="c b a">content</div>');
const template6 = `<div class="c" t-att-class="{'b': true}">content</div>`;
expect(renderToString(template6)).toBe('<div class="c b">content</div>');
const template7 = `<div class="c" t-attf-class="{{'b'}}">content</div>`;
expect(renderToString(template7)).toBe('<div class="c b">content</div>');
const template8 = `<div t-att="{ class: 'a' }" class="c" t-att-class="{'b': true}">content</div>`;
expect(renderToString(template8)).toBe('<div class="c a b">content</div>');
const template9 = `<div t-att="{ class: 'a' }" t-att-class="{'b': true}">content</div>`;
expect(renderToString(template9)).toBe('<div class="a b">content</div>');
});
});
@@ -1,50 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`t-call component with an enumerable getter, t-call inside slot 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { capture, markRaw } = helpers;
const callTemplate_1 = app.getTemplate(\`sub\`);
const comp1 = app.createComponent(\`Child\`, true, true, false, []);
function slot1(ctx, node, key = \\"\\") {
return callTemplate_1.call(this, ctx, node, key + \`__1\`);
}
return function template(ctx, node, key = \\"\\") {
const ctx1 = capture(ctx);
return comp1({slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx1}})}, key + \`__2\`, node, this, null);
}
}"
`;
exports[`t-call component with an enumerable getter, t-call inside slot 2`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div><block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let txt1 = ctx['foo'];
return block1([txt1]);
}
}"
`;
exports[`t-call component with an enumerable getter, t-call inside slot 3`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { callSlot } = helpers;
return function template(ctx, node, key = \\"\\") {
return callSlot(ctx, node, key, 'default', false, {});
}
}"
`;
exports[`t-call dynamic t-call 1`] = `
"function anonymous(app, bdom, helpers
) {
-22
View File
@@ -425,26 +425,4 @@ describe("t-call", () => {
await nextTick();
expect(fixture.innerHTML).toBe("Bchild");
});
test("component with an enumerable getter, t-call inside slot", async () => {
class Child extends Component {
static template = xml`<t t-slot="default"/>`;
}
class Parent extends Component {
static components = { Child };
static template = xml`<Child><t t-call="sub"/></Child>`;
}
// simulate adding a getter with patch in odoo: getter will be enumarable
Object.defineProperty(Parent.prototype, "foo", {
get() {
return 1;
},
enumerable: true,
});
const app = new App(Parent);
app.addTemplate("sub", `<div t-esc="foo"/>`);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div>1</div>");
});
});
+9 -13
View File
@@ -1,7 +1,6 @@
import terser from "rollup-plugin-terser";
import copy from "rollup-plugin-copy";
import execute from "rollup-plugin-execute";
import del from "rollup-plugin-delete";
import { string } from "rollup-plugin-string";
const isWindows = process.platform === "win32";
@@ -37,12 +36,12 @@ export default ({ "config-browser": browser, "config-env": env }) => {
},
];
function generateRule(input, format = "esm") {
function generateRule(input, plugins = []) {
return {
input: input,
input,
output: [
{
format: format,
format: "esm",
file: input.replace("tools/devtools/src", "dist/devtools"),
},
],
@@ -51,6 +50,7 @@ export default ({ "config-browser": browser, "config-env": env }) => {
include: "**/page_scripts/owl_devtools_global_hook.js",
}),
isProduction && terser.terser(),
...plugins,
],
};
}
@@ -58,7 +58,6 @@ export default ({ "config-browser": browser, "config-env": env }) => {
commands[1] = isWindows
? "npm run compile_templates -- tools\\devtools\\src && move templates.js tools\\devtools\\assets\\templates.js"
: "npm run compile_templates -- tools/devtools/src && mv templates.js tools/devtools/assets/templates.js";
const firstRule = generateRule("tools/devtools/src/page_scripts/owl_devtools_global_hook.js");
if (isProduction) {
commands[0] = isWindows
? "npm run build && copy dist\\owl.iife.js tools\\devtools\\assets\\owl.js && npm run build:compiler"
@@ -68,19 +67,16 @@ export default ({ "config-browser": browser, "config-env": env }) => {
? "copy dist\\owl.iife.js tools\\devtools\\assets\\owl.js"
: "cp dist/owl.iife.js tools/devtools/assets/owl.js";
}
firstRule.plugins.push(execute(commands, true));
const secondRule = generateRule("tools/devtools/src/content.js");
secondRule.plugins.push(copy({ targets: filesToMove }));
const lastRule = generateRule("tools/devtools/src/background.js");
lastRule.plugins.push(del({ targets: "tools/devtools/assets/*.js" }));
return [
firstRule,
secondRule,
generateRule("tools/devtools/src/page_scripts/owl_devtools_global_hook.js", [
execute(commands, true),
]),
generateRule("tools/devtools/src/content.js", [copy({ targets: filesToMove })]),
generateRule("tools/devtools/src/devtools_app/devtools.js"),
generateRule("tools/devtools/src/utils.js"),
generateRule("tools/devtools/src/devtools_app/devtools_panel.js"),
generateRule("tools/devtools/src/popup_app/popup.js"),
lastRule,
generateRule("tools/devtools/src/background.js"),
];
};
@@ -3,8 +3,10 @@
import { DevtoolsWindow } from "./devtools_window/devtools_window";
const { mount } = owl;
import { templates } from "../../assets/templates.js";
import { createStore } from "./store/store";
for (const template in templates) {
owl.App.registerTemplate(template, templates[template]);
}
createStore();
mount(DevtoolsWindow, document.body, { dev: true });
@@ -1,11 +1,10 @@
const { Component, useRef, useEffect } = owl;
import { useStore } from "../../../store/store";
import { ObjectTreeElement } from "./object_tree_element/object_tree_element";
import { Subscriptions } from "./subscriptions/subscriptions";
export class DetailsWindow extends Component {
static template = "devtools.DetailsWindow";
static components = { ObjectTreeElement, Subscriptions };
static components = { ObjectTreeElement };
setup() {
this.store = useStore();
this.contextMenu = useRef("contextmenu");
@@ -53,7 +53,11 @@
</div>
<i title="Store observed states as global variable in the console" class="fa fa-bug utility-icon p-1" t-on-click.stop="() => this.store.logObjectInConsole([...this.store.activeComponent.path, {type: 'item', value: 'subscriptions'}])"></i>
</div>
<Subscriptions t-if="store.activeComponent.subscriptions.toggled"/>
<div t-if="store.activeComponent.subscriptions.toggled" id="subscriptionsPanel">
<t t-foreach="store.activeComponent.subscriptions.children" t-as="subscription" t-key="subscription_index">
<ObjectTreeElement object="subscription.target"/>
</t>
</div>
</div>
<div t-if="store.activeComponent.instance.children.length > 0" id="instance" class="details-panel ps-2 py-1">
<div class="d-flex mb-2">
@@ -41,23 +41,22 @@ export class ObjectTreeElement extends Component {
return JSON.stringify(this.props.object.path);
}
get objectName() {
return this.props.object.name;
get keyChanges() {
return this.props.object.keys?.includes("Symbol(Key changes)");
}
get objectLineClass() {
classFor(object) {
// Prototype items will be dyed down to appear less important
if (this.pathAsString.includes('{"type":"prototype",')) {
return { attenuate: true };
if (object.path.some((item) => item?.type === "prototype")) {
return "attenuate";
}
// Same for subscription items which are not present in the keys while the keys will be bold
if (this.props.object.objectType === "subscription" && this.props.object.depth > 0) {
if (this.props.keys.includes(this.props.object.name.toString())) {
return { "fw-bolder": true };
if (object.objectType === "subscription" && object.depth > 0) {
if (this.props.object.keys?.includes(object.name.toString())) {
return "fw-bolder";
}
return { attenuate: true };
return "attenuate";
}
return {};
}
get objectPadding() {
@@ -2,7 +2,7 @@
<templates xml:space="preserve">
<t t-name="devtools.ObjectTreeElement" owl="1">
<div class="m-0 p-0 text-nowrap w-100 object-line"
t-att-class="objectLineClass"
t-att-class="props.class"
t-on-click.stop="() => this.store.toggleObjectTreeElementsDisplay(this.props.object)"
t-on-contextmenu.prevent="openMenu"
>
@@ -11,7 +11,7 @@
t-att-class="{'fa-caret-right': !props.object.toggled, 'fa-caret-down': props.object.toggled}"
t-attf-style="visibility: {{props.object.hasChildren ? '' : 'hidden'}};"
/>
<t t-esc="objectName"/>
<t t-esc="props.object.name"/>
<t t-if="props.object.content.length > 0">: </t>
<t t-if="props.object.contentType == 'getter'">
<span class="getter-content object-content" t-att-class="objectLineClass" t-on-click.stop="() => this.store.loadGetterContent(this.props.object)">
@@ -28,6 +28,7 @@
</t>
</span>
</t>
<span t-if="keyChanges" class="key-changes ms-1 badge p-1" title="Key additions/deletions are observed">+/-</span>
</div>
</div>
<div t-if="store.contextMenu.activeMenu === contextMenuId" class="custom-menu" t-ref="contextmenu">
@@ -40,8 +41,7 @@
</div>
<t t-if="props.object.toggled" t-key="contextMenuId">
<t t-foreach="props.object.children" t-as="child" t-key="child_index">
<ObjectTreeElement t-if="props.object.objectType === 'subscription'" object="child" keys="props.keys"/>
<ObjectTreeElement t-else="" object="child"/>
<ObjectTreeElement object="child" class="this.classFor(child)"/>
</t>
</t>
</t>
@@ -1,30 +0,0 @@
const { Component } = owl;
import { useStore } from "../../../../store/store";
import { ObjectTreeElement } from "../object_tree_element/object_tree_element";
export class Subscriptions extends Component {
static template = "devtools.Subscriptions";
static components = { ObjectTreeElement };
setup() {
this.store = useStore();
}
// Used to display the keys in a compact way
keysContent(index) {
const keys = this.store.activeComponent.subscriptions.children[index].keys;
let content = JSON.stringify(keys);
const maxLength = 50;
content = content.replace(/,/g, ", ");
if (content.length > maxLength) {
content = content.slice(0, content.lastIndexOf(",", maxLength - 5)) + ", ...]";
}
return content;
}
expandKeys(event, index) {
this.store.activeComponent.subscriptions.children[index].keysExpanded =
!this.store.activeComponent.subscriptions.children[index].keysExpanded;
}
}
@@ -1,24 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="devtools.Subscriptions" owl="1">
<div id="subscriptionsPanel">
<t t-foreach="store.activeComponent.subscriptions.children" t-as="subscription" t-key="subscription_index">
<div class="my-2">
<div class="my-0 p-0 object-line" t-on-click.stop="(ev) => this.expandKeys(ev, subscription_index)">
<span class="ps-1 text-nowrap">
<i class="fa fa-caret-right ms-1" t-attf-style="cursor: pointer;{{subscription.keysExpanded ? 'transform: rotate(90deg);' : ''}}"></i>
keys: <span class="key-name"><t t-esc="this.keysContent(subscription_index)"/></span>
</span>
</div>
<div t-foreach="subscription.keys" t-as="key" t-key="key_index" class="my-0 p-0 object-line" t-attf-style="display: {{subscription.keysExpanded ? 'flex' : 'none'}}">
<div style="transform: translateX(calc(1.1rem))" class="key-content">
<i class="fa fa-caret-right mx-1" t-attf-style="cursor: pointer; visibility: hidden;"></i>
<t t-esc="key"/>
</div>
</div>
<ObjectTreeElement object="subscription.target" keys="subscription.keys"/>
</div>
</t>
</div>
</t>
</templates>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="devtools.Event" owl="1">
<div class="event-container">
<div class="event-container" t-att-class="{ 'event-last': props.event.isLast }">
<div class="my-0 p-0 object-line" t-on-click.stop="toggleDisplay">
<div class="ps-2 text-nowrap">
<i class="fa px-1 pointer-icon caret"
+16 -24
View File
@@ -102,20 +102,17 @@ export const store = reactive({
if (IS_FIREFOX) {
await evalInWindow("window.$0 = $0;", this.activeFrame);
}
const apps = await evalFunctionInWindow(
const [apps, component] = await evalFunctionInWindow(
"getComponentsTree",
fromOld && this.activeComponent ? [this.activeComponent.path, this.apps] : [],
fromOld && this.activeComponent
? [this.activeComponent.path, this.apps, this.activeComponent]
: [],
this.activeFrame
);
this.apps = apps ? apps : [];
if (!fromOld && this.settings.expandByDefault) {
this.apps.forEach((tree) => expandNodes(tree, true));
}
const component = await evalFunctionInWindow(
"getComponentDetails",
fromOld && this.activeComponent ? [this.activeComponent.path, this.activeComponent] : [],
this.activeFrame
);
this.activeComponent = component;
},
@@ -413,12 +410,7 @@ export const store = reactive({
if (!scriptsLoaded) {
await loadScripts(frame);
}
evalInWindow(
`__OWL__DEVTOOLS_GLOBAL_HOOK__.devtoolsId = ${
store.devtoolsId
}; __OWL__DEVTOOLS_GLOBAL_HOOK__.frame = ${JSON.stringify(frame)};`,
frame
);
evalFunctionInWindow("initDevtools", [frame], frame);
if (!this.frameUrls.includes(frame)) {
this.frameUrls = [...this.frameUrls, frame];
}
@@ -633,12 +625,10 @@ export function useStore() {
return useState(store);
}
init();
async function init() {
export async function createStore() {
store.devtoolsId = await getTabURL();
evalInWindow("__OWL__DEVTOOLS_GLOBAL_HOOK__.devtoolsId = " + store.devtoolsId + ";");
evalFunctionInWindow("initDevtools", []);
await loadSettings();
@@ -671,7 +661,7 @@ async function init() {
}, 500);
}
let flushRendersTimeout = false;
let rootRendersTimeout = false;
// Connect to the port to communicate to the background script
browserInstance.runtime.onConnect.addListener((port) => {
if (port.name === "OwlDevtoolsPort_" + store.devtoolsId) {
@@ -680,7 +670,7 @@ browserInstance.runtime.onConnect.addListener((port) => {
if (msg.type === "Reload") {
store.owlStatus = await evalInWindow("window.__OWL__DEVTOOLS_GLOBAL_HOOK__ !== undefined;");
if (store.owlStatus) {
evalInWindow("__OWL__DEVTOOLS_GLOBAL_HOOK__.devtoolsId = " + store.devtoolsId + ";");
evalFunctionInWindow("initDevtools", []);
await store.resetData();
}
}
@@ -694,9 +684,9 @@ browserInstance.runtime.onConnect.addListener((port) => {
if (msg.type === "RefreshApps") {
store.loadComponentsTree(true);
}
// When message of type Flush is received, overwrite the component tree with the new one from page
// A flush message is sent everytime a component is rendered on the page
if (msg.type === "Flush") {
// When message of type Complete is received, overwrite the component tree with the new one from page
// A Complete message is sent everytime a root render is triggered on the page
if (msg.type === "Complete") {
if (msg.origin.frame !== store.activeFrame) {
return;
}
@@ -705,8 +695,8 @@ browserInstance.runtime.onConnect.addListener((port) => {
}
// This determines which components will have a short highlight effect in the tree to indicate they have been rendered
store.renderPaths.add(JSON.stringify(msg.data));
clearTimeout(flushRendersTimeout);
flushRendersTimeout = setTimeout(() => {
clearTimeout(rootRendersTimeout);
rootRendersTimeout = setTimeout(() => {
store.renderPaths.clear();
}, 100);
store.loadComponentsTree(true);
@@ -787,6 +777,7 @@ function loadEvents(events) {
}
event.origin = null;
event.toggled = false;
event.isLast = false;
// Logic to retrace the origin of the event if it is not a root render event
if (!event.type.includes("render")) {
for (let i = store.events.length - 1; i >= 0; i--) {
@@ -836,6 +827,7 @@ function loadEvents(events) {
// Make sure we add the event while keeping the whole list ordered by id
addEventSorted(event);
}
store.events[store.events.length - 1].isLast = true;
}
// Deselect component and remove highlight on all children
+8
View File
@@ -126,6 +126,10 @@
color: var(--prototype-color);
}
.key-changes {
background-color: var(--version-bg);
}
.event-container {
border-bottom: 1px solid rgb(240, 238, 238);
padding-top: 2px!important;
@@ -133,6 +137,10 @@
font-size: 11px;
}
.event-last {
border-bottom: 3px solid rgb(225, 154, 0);
}
.getter-content:hover {
text-decoration: underline;
}
@@ -15,8 +15,6 @@
// in __OWL_DEVTOOLS__
this.toRaw = window.__OWL_DEVTOOLS__.toRaw ?? window.owl?.toRaw;
this.reactive = window.__OWL_DEVTOOLS__.reactive ?? window.owl?.reactive;
// Set to keep track of the fibers that are in the flush queue
this.queuedFibers = new WeakSet();
// Set to keep track of the HTML elements we added to the page
this.addedElements = [];
// To keep track of the succession order of the render events
@@ -24,7 +22,6 @@
// Set to keep track of the frame on which this script is loaded
this.frame = "top";
// Allows to launch a message each time an iframe html element is added to the page
const self = this;
const iFrameObserver = new MutationObserver(function (mutationsList) {
mutationsList.forEach(function (mutation) {
mutation.addedNodes.forEach(function (addedNode) {
@@ -36,23 +33,21 @@
* This process may seem long and indirect but is necessary. This applies to all window.top.postMessage methods in this file.
* More information in the docs: https://developer.chrome.com/docs/extensions/mv3/devtools/#evaluated-scripts-to-devtools
*/
window.top.postMessage({
source: "owl-devtools",
type: "NewIFrame",
data: addedNode.contentDocument.location.href,
});
window.top.postMessage(
{
source: "owl-devtools",
type: "NewIFrame",
data: addedNode.contentDocument.location.href,
},
"/"
);
}
}
});
});
});
iFrameObserver.observe(document.body, { subtree: true, childList: true });
this.appsPatched = false;
this.destroyPatched = false;
this.patchAppsSetMethods();
if (this.apps.size > 0) {
this.patchAppMethods();
}
this.recordEvents = false;
this.traceRenderings = false;
this.traceSubscriptions = false;
@@ -172,114 +167,84 @@
};
}
initDevtools(frame = "top") {
if (!this.devtoolsInit) {
this.frame = frame;
const self = this;
// Flush the events batcher when a root render is completed
const original_Complete = self.RootFiber.prototype.complete;
self.RootFiber.prototype.complete = function () {
original_Complete.call(this, ...arguments);
const path = self.getComponentPath(this.node);
//Add a functionnality to the complete function which sends a message to the window every time it is triggered.
window.top.postMessage(
{
source: "owl-devtools",
type: "Complete",
data: path,
origin: { frame: self.frame },
},
"/"
);
if (self.recordEvents) {
window.top.postMessage(
{
source: "owl-devtools",
type: "Event",
data: self.eventsBatch,
},
"/"
);
self.eventsBatch = [];
}
};
this.devtoolsInit = true;
}
}
// Modify the methods of the apps set in order to send a message each time it is modified.
patchAppsSetMethods() {
const originalAdd = this.apps.add;
const originalDelete = this.apps.delete;
const self = this;
this.apps.add = function () {
originalAdd.call(this, ...arguments);
if (!self.destroyPatched) {
const newApp = arguments[0];
// It is not a given that apps have a root node at creation so we need to wait
if (newApp.root) {
self.patchDestroyMethod(newApp.root);
} else {
let root = null;
Object.defineProperty(newApp, "root", {
get() {
return root;
},
set(value) {
root = value;
if (!self.destroyPatched) {
self.patchDestroyMethod(root);
}
},
});
}
}
self.patchAppMethods();
window.top.postMessage({
source: "owl-devtools",
type: "RefreshApps",
});
window.top.postMessage(
{
source: "owl-devtools",
type: "RefreshApps",
},
"/"
);
};
this.apps.delete = function () {
originalDelete.call(this, ...arguments);
window.top.postMessage({
source: "owl-devtools",
type: "RefreshApps",
});
window.top.postMessage(
{
source: "owl-devtools",
type: "RefreshApps",
},
"/"
);
};
}
patchDestroyMethod(root) {
if (!this.destroyPatched) {
// Signals when a component is destroyed
const originalDestroy = root.constructor.prototype._destroy;
const self = this;
root.constructor.prototype._destroy = function () {
if (self.recordEvents) {
const path = self.getComponentPath(this);
const event = {
type: "destroy",
component: this.name,
key: this.parentKey,
path: path,
time: 0,
id: self.eventId++,
};
self.eventsBatch.push(event);
const before = performance.now();
originalDestroy.call(this, ...arguments);
event.time = performance.now() - before;
} else {
originalDestroy.call(this, ...arguments);
}
};
this.destroyPatched = true;
}
}
// Modify methods of each app so that it triggers messages on each flush and component render
patchAppMethods() {
if (this.appsPatched) {
let app;
for (const appItem of this.apps) {
if (appItem.root) {
app = appItem;
}
}
if (!app.root) {
return;
}
let app = this.apps.values().next().value;
const self = this;
if (app.root) {
this.patchDestroyMethod(app.root);
} else {
const originalMount = app.constructor.prototype.mount;
app.constructor.prototype.mount = async function (...args) {
const result = await originalMount.call(this, ...args);
const root = this.root;
self.patchDestroyMethod(root);
app.constructor.prototype.mount = originalMount;
return result;
};
}
const originalFlush = app.scheduler.constructor.prototype.flush;
let inFlush = false;
let _render = false;
app.scheduler.constructor.prototype.flush = function () {
// Used to know when a render is triggered inside the flush method or not
inFlush = true;
[...this.tasks].map((fiber) => {
if (fiber.counter === 0 && !self.queuedFibers.has(fiber)) {
self.queuedFibers.add(fiber);
const path = self.getComponentPath(fiber.node);
//Add a functionnality to the flush function which sends a message to the window every time it is triggered.
window.top.postMessage({
source: "owl-devtools",
type: "Flush",
data: path,
origin: { frame: self.frame },
});
}
});
originalFlush.call(this, ...arguments);
inFlush = false;
};
@@ -376,20 +341,27 @@
_render = true;
original_Render.call(this, ...arguments);
};
// Flush the events batcher when a root render is completed
const original_Complete = self.RootFiber.prototype.complete;
self.RootFiber.prototype.complete = function () {
original_Complete.call(this, ...arguments);
// Signals when a component is destroyed
const originalDestroy = app.root.constructor.prototype._destroy;
app.root.constructor.prototype._destroy = function () {
if (self.recordEvents) {
window.top.postMessage({
source: "owl-devtools",
type: "Event",
data: self.eventsBatch,
});
self.eventsBatch = [];
const path = self.getComponentPath(this);
const event = {
type: "destroy",
component: this.name,
key: this.parentKey,
path: path,
time: 0,
id: self.eventId++,
};
self.eventsBatch.push(event);
const before = performance.now();
originalDestroy.call(this, ...arguments);
event.time = performance.now() - before;
} else {
originalDestroy.call(this, ...arguments);
}
};
this.appsPatched = true;
}
// patch reactivity system to activate subscription tracing
@@ -453,9 +425,14 @@
}
toggleTracing(value) {
if (value) {
this.patchAppMethods();
this.patchAppMethods = () => {}; // to only patch once
}
this.traceRenderings = value;
return this.traceRenderings;
}
toggleSubscriptionTracing(value) {
if (value) {
this.patchReactivity();
@@ -466,6 +443,10 @@
}
// Enables/disables the recording of the render/destroy events based on value
toggleEventsRecording(value, index) {
if (value) {
this.patchAppMethods();
this.patchAppMethods = () => {}; // to only patch once
}
this.recordEvents = value;
this.eventId = index;
return this.recordEvents;
@@ -636,11 +617,14 @@
const path = this.getElementPath(target);
this.highlightComponent(path);
this.currentSelectedElement = target;
window.top.postMessage({
source: "owl-devtools",
type: "SelectElement",
data: path,
});
window.top.postMessage(
{
source: "owl-devtools",
type: "SelectElement",
data: path,
},
"/"
);
}
};
@@ -664,10 +648,13 @@
document.removeEventListener("mouseover", this.HTMLSelector, { capture: true });
document.removeEventListener("click", this.disableHTMLSelector, { capture: true });
document.removeEventListener("mouseout", this.removeHighlights, { capture: true });
window.top.postMessage({
source: "owl-devtools",
type: "StopSelector",
});
window.top.postMessage(
{
source: "owl-devtools",
type: "StopSelector",
},
"/"
);
};
// Returns the object specified by the path starting from the topParent object
@@ -819,57 +806,47 @@
}
break;
}
if (child.contentType) {
if (child.toggled) {
child.children = this.loadObjectChildren(
child.path,
child.depth,
child.contentType,
child.objectType,
oldTree
);
}
return child;
}
if (obj === null) {
child.content = "null";
child.contentType = "object";
child.hasChildren = false;
} else if (obj === undefined) {
child.content = "undefined";
child.contentType = "undefined";
child.hasChildren = false;
} else {
obj = this.toRaw(obj);
switch (true) {
case obj instanceof Map:
child.contentType = "map";
child.hasChildren = true;
break;
case obj instanceof Set:
child.contentType = "set";
child.hasChildren = true;
break;
case obj instanceof Array:
child.contentType = "array";
child.hasChildren = obj.length > 0;
break;
case typeof obj === "function":
child.contentType = "function";
child.hasChildren = true;
break;
case obj instanceof Object:
child.contentType = "object";
child.hasChildren = Object.keys(obj).length > 0;
break;
default:
child.contentType = typeof obj;
child.hasChildren = false;
}
if (key.type === "set entry") {
child.content = this.serializer.serializeItem(obj, true);
if (!child.contentType) {
if (obj === null) {
child.content = "null";
child.contentType = "object";
child.hasChildren = false;
} else if (obj === undefined) {
child.content = "undefined";
child.contentType = "undefined";
child.hasChildren = false;
} else {
child.content = this.serializer.serializeContent(obj, child.contentType);
obj = this.toRaw(obj);
switch (true) {
case obj instanceof Map:
child.contentType = "map";
child.hasChildren = true;
break;
case obj instanceof Set:
child.contentType = "set";
child.hasChildren = true;
break;
case obj instanceof Array:
child.contentType = "array";
child.hasChildren = obj.length > 0;
break;
case typeof obj === "function":
child.contentType = "function";
child.hasChildren = true;
break;
case obj instanceof Object:
child.contentType = "object";
child.hasChildren = Object.keys(obj).length > 0;
break;
default:
child.contentType = typeof obj;
child.hasChildren = false;
}
if (key.type === "set entry") {
child.content = this.serializer.serializeItem(obj, true);
} else {
child.content = this.serializer.serializeContent(obj, child.contentType);
}
}
}
if (child.toggled) {
@@ -881,6 +858,7 @@
oldTree
);
}
this.addHighlightedKeys(child);
return child;
}
@@ -890,7 +868,10 @@
let path = completePath.slice(objPathIndex);
let obj;
if (objType === "subscription") {
obj = oldTree.subscriptions.children[path[1].value].target;
const subscriptionPath = completePath.slice(0, objPathIndex + 3);
obj = oldTree.subscriptions.children.find(
(child) => JSON.stringify(child.target.path) === JSON.stringify(subscriptionPath)
).target;
path = path.slice(3);
} else {
// Everything here is in component if it is not an app so remove this key of the path in the former case
@@ -1303,16 +1284,15 @@
children: [],
};
} else {
const rawSubscriptions = node.subscriptions;
const rawSubscriptions = this.topLevelSubscriptions(node);
component.subscriptions = {
toggled: oldTree ? oldTree.subscriptions.toggled : true,
children: [],
};
rawSubscriptions.forEach((rawSubscription, index) => {
rawSubscriptions.forEach((rawSubscription) => {
let subscription = {
keys: [],
target: {
name: "target",
name: this.targetName(rawSubscription.target, node),
contentType:
typeof rawSubscription.target === "object"
? Array.isArray(rawSubscription.target)
@@ -1323,28 +1303,20 @@
path: [
...path,
{ type: "item", value: "subscriptions" },
{ type: "item", value: index },
{ type: "item", value: rawSubscription.index },
{ type: "item", value: "target" },
],
toggled: false,
objectType: "subscription",
},
keysExpanded: false,
};
if (
oldTree &&
oldTree.subscriptions.children[index] &&
oldTree.subscriptions.children[index].target.toggled
oldTree.subscriptions.children[rawSubscription.index] &&
oldTree.subscriptions.children[rawSubscription.index].target.toggled
) {
subscription.target.toggled = true;
}
rawSubscription.keys.forEach((key) => {
if (typeof key === "symbol") {
subscription.keys.push(key.toString());
} else {
subscription.keys.push(key);
}
});
if (rawSubscription.target == null) {
if (subscription.target.contentType === "undefined") {
subscription.target.content = "undefined";
@@ -1374,6 +1346,7 @@
oldTree
);
}
this.addHighlightedKeys(subscription.target);
component.subscriptions.children.push(subscription);
});
}
@@ -1556,7 +1529,7 @@
}
// Returns the tree of components of the inspected page in a parsed format
// Use inspectedPath to specify the path of the selected component
getComponentsTree(inspectedPath = null, oldTrees = null) {
getComponentsTree(inspectedPath = null, oldTrees = null, oldDetails = null) {
const appsArray = [...this.apps];
const trees = appsArray.map((app, index) => {
let oldTree;
@@ -1609,7 +1582,8 @@
}
return appNode;
});
return trees ? trees : [];
const component = this.getComponentDetails(inspectedPath, oldDetails);
return trees ? [trees, component] : [];
}
// Recursively fills the components tree as a parsed version
fillTree(appNode, treeNode, inspectedPathString, oldBranch) {
@@ -1705,6 +1679,54 @@
inspect(obj);
}
}
targetName(target, node) {
// check on component
const { component } = node;
for (const [key, value] of Object.entries(component)) {
if (target === this.toRaw(value)) {
return key;
}
}
// check on props
for (const [key, value] of Object.entries(component.props)) {
if (target === this.toRaw(value)) {
return `props.${key}`;
}
}
return "[unknown]";
}
/**
* Removes subscriptions that are a direct child of another subscription:
* they will be reachable from the top level by expanding observed keys.
*
* @param {ComponentNode} node
* @returns {{ keys: PropertyKey[], target: unknown}[]} the top level
* subscriptions of the node
*/
topLevelSubscriptions(node) {
const subscriptions = node.subscriptions.map((s, index) => ({ ...s, index }));
const topLevelValues = new Set(Object.values(node.component).map((o) => this.toRaw(o)));
const toOmit = new Set(
subscriptions
.flatMap(({ keys, target }) => keys.map((k) => this.toRaw(target[k])))
.filter((obj) => !topLevelValues.has(obj))
);
return subscriptions.filter(({ target }) => !toOmit.has(target));
}
addHighlightedKeys(child) {
const { path } = child;
const subscriptionIndex = path.findIndex((item) => typeof item !== "string");
if (path[subscriptionIndex]?.value === "subscriptions") {
const node = this.getComponentNode(path.slice(0, subscriptionIndex));
// Add observed keys
const targetToKeys = new Map(node.subscriptions.map(({ keys, target }) => [target, keys]));
const target = this.getObjectProperty(child.path);
child.keys = targetToKeys.get(target)?.map((k) => String(k));
}
}
}
function checkOwlStatus() {
@@ -1717,7 +1739,7 @@
owlStatus = -1;
}
}
window.postMessage({ source: "owl-devtools", type: "owlStatus", data: owlStatus });
window.postMessage({ source: "owl-devtools", type: "owlStatus", data: owlStatus }, "/");
}
if (!window.__OWL_DEVTOOLS__) {
@@ -1739,19 +1761,19 @@
if (value?.Fiber !== undefined) {
window.__OWL__DEVTOOLS_GLOBAL_HOOK__ = new OwlDevtoolsGlobalHook();
}
window.top.postMessage({ source: "owl-devtools", type: "FrameReady" });
window.top.postMessage({ source: "owl-devtools", type: "FrameReady" }, "/");
checkOwlStatus();
},
});
// Do note that the reload message is not sent on the top window so that it is not intercepted when originating
// from an iframe
window.postMessage({ source: "owl-devtools", type: "Reload" });
window.postMessage({ source: "owl-devtools", type: "Reload" }, "/");
} else if (
window.__OWL_DEVTOOLS__?.Fiber !== undefined &&
!window.__OWL__DEVTOOLS_GLOBAL_HOOK__
) {
window.__OWL__DEVTOOLS_GLOBAL_HOOK__ = new OwlDevtoolsGlobalHook();
window.postMessage({ source: "owl-devtools", type: "Reload" });
window.postMessage({ source: "owl-devtools", type: "Reload" }, "/");
}
// Listener that checks whether owl is available on the page and if it has the right version
window.addEventListener(
+9
View File
@@ -0,0 +1,9 @@
import type * as owl from "../../../src/runtime";
import type { JestChrome } from "jest-chrome/types/jest-chrome";
declare global {
interface Window {
owl: typeof owl;
browser: any;
chrome: JestChrome;
}
}
+15
View File
@@ -0,0 +1,15 @@
import { mount } from "../../../src";
import { makeTestFixture } from "../../../tests/helpers";
import { DevtoolsWindow } from "../src/devtools_app/devtools_window/devtools_window";
let fixture = makeTestFixture();
beforeEach(() => {
fixture = makeTestFixture();
});
describe("devtools", () => {
test("mounting the devtools", async () => {
await mount(DevtoolsWindow as any, fixture);
expect(fixture.querySelector(".status-message")!.textContent).toContain("There are no apps currently running.")
});
});
+28
View File
@@ -0,0 +1,28 @@
{
"testEnvironment": "jsdom",
"roots": [
"<rootDir>"
],
"setupFiles": [
"<rootDir>/../../../tests/mocks/mockEventTarget.js",
"<rootDir>/setup.ts"
],
"globals": {
"ts-jest": {
"tsconfig": "<rootDir>/tsconfig.json"
}
},
"transform": {
"^.+\\.[jt]s?$": "ts-jest"
},
"verbose": false,
"testRegex": "(/tests/.*(test|spec))\\.ts?$",
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"jsx",
"json",
"node"
]
}
+9
View File
@@ -0,0 +1,9 @@
import * as owl from "../../../src/runtime";
window.owl = owl;
import { chrome } from "jest-chrome";
window.chrome = chrome;
import { templates } from "../assets/templates";
for (const template in templates) {
owl.App.registerTemplate(template, templates[template as keyof typeof templates]);
}
+109
View File
@@ -0,0 +1,109 @@
{
/**
** Commented-out options have their default values.
**/
"include": [
"**/*.ts",
],
                                                              // "exclude": [],
// "files": [],                   // A list of relative or absolute file paths to include.
// "extends": "",                   // A string containing a path to another configuration file to inherit from.
// "references": [],                   // An array of objects `{"path": "./to/dirOrConfig"}` that specifies projects to reference.
// "compileOnSave": false,                   // Signals to the IDE to generate all files for a given tsconfig.json upon saving.
"compilerOptions": {
                                                            // Main options
"target": "es2019",                                         // Specify ECMAScript target version: 'es3' (default), 'es5', 'es2015', 'es2016', 'es2017','es2018' or 'esnext'.
"module": "es6",                                         // Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'.
// "lib": ["esnext", "dom"],                 // Specify library files to be included in the compilation.
"allowJs": true,                  // Allow javascript files to be compiled.
// "checkJs": false,                 // Report errors in .js files.
// "outFile": "./",                 // Concatenate and emit output to single file.
"outDir": "dist",                                           // Redirect output structure to the directory.
// "rootDir": "./",                 // Specify the root directory of input files. Use to control the output directory structure with `--outDir`.
// "project": "",                 // Compile a project given a valid configuration file.
                                                            // Compilation options
// "composite": true,                 // Enable project compilation
// "diagnostics": false,                 // Show diagnostic information.
// "incremental": true,                 // Enable incremental compilation by reading/writing information from prior compilations to a file on disk.
// "isolatedModules": false,                 // Transpile each file as a separate module (similar to 'ts.transpileModule').
// "listEmittedFiles": false,                 // Print names of generated files part of the compilation.
// "listFiles": true,                 // Print names of files part of the compilation.
// "noErrorTruncation": false,                 // Do not truncate error messages.
// "preserveWatchOutput": false,                 // Keep outdated console output in watch mode instead of clearing the screen.
// "traceResolution": false,                 // Enable tracing of the name resolution process.
// "tsBuildInfoFile": ".tsbuildinfo",                 // Specify file to store incremental compilation information.
                                                            // Strict typechecking options
"strict": true,                                          // Enable all strict type-checking options.
// "noImplicitAny": true,                 // Raise error on expressions and declarations with an implied 'any' type.
// "noImplicitThis": true,                 // Raise error on 'this' expressions with an implied 'any' type.
// "strictBindCallApply": true,                 // Enable stricter checking of of the `bind`, `call`, and `apply` methods on functions.
// "strictFunctionTypes": true,                 // Disable bivariant parameter checking for function types.
// "strictNullChecks": true,                 // In strict null checking mode, the null and undefined values are not in the domain of every type and are only assignable to themselves and any.
// "strictPropertyInitialization": true,                 // Ensure non-undefined class properties are initialized in the constructor. This option requires `--strictNullChecks` be enabled in order to take effect.
// "alwaysStrict": true,                 // Parse in strict mode and emit "use strict" for each source file.
                                                            // Additional checks
// "allowUnreachableCode": false,                 // Do not report errors on unreachable code.
// "allowUnusedLabels": false,                 // Do not report errors on unused labels.
"forceConsistentCasingInFileNames": true,                   // Disallow inconsistently-cased references to the same file.
// "noStrictGenericChecks": false,                 // Disable strict checking of generic signatures in function types.
"noUnusedLocals": true,                                     // Report errors on unused locals.
"noUnusedParameters": false,                                // Report errors on unused parameters.
"noImplicitReturns": true,                                  // Report error when not all code paths in function return a value.
"noFallthroughCasesInSwitch": true,                         // Report errors for fallthrough cases in switch statement.
// "skipLibCheck": false,                 // Skip type checking of all declaration files (*.d.ts).
// "suppressExcessPropertyErrors": false,                 // Suppress excess property checks for object literals.
// "suppressImplicitAnyIndexErrors": false,                 // Suppress noImplicitAny errors for indexing objects lacking index signatures.
                                                            // Module resolution options
"moduleResolution": "node",                                 // Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6).
// "baseUrl": "./",                 // Base directory to resolve non-absolute module names.
// "paths": {},                 // A series of entries which re-map imports to lookup locations relative to the 'baseUrl'.
// "rootDirs": [],                 // List of root folders whose combined content represents the structure of the project at runtime.
// "typeRoots": [
// "./node_modules/@types",
// "./tools/devtools/tests"
// ],                 // List of folders to include type definitions from.
"types": [
"jest",
"node"
],                                                // Type declaration files to be included in compilation.
// "allowSyntheticDefaultImports": false                    // Allow default imports from modules with no default export. This does not affect code emit, just typechecking.
"esModuleInterop": true,                  // Emit '__importStar' and '__importDefault' helpers for runtime babel ecosystem compatibility and enable '--allowSyntheticDefaultImports' for typesystem compatibility.
// "maxNodeModuleJsDepth": 0,                 // The maximum dependency depth to search under node_modules and load JavaScript files. Only applicable with --allowJs.
// "preserveSymlinks": false,                 // Do not resolve the real path of symlinks.
"resolveJsonModule": true,                                  // Include modules imported with '.json' extension.
                                                            // Emit options
"declaration": true,                                        // Generates corresponding '.d.ts' file.
"declarationDir": "dist/types",                             // Output directory for generated declaration files.
// "declarationMap": false,                 // Generates a sourcemap for each corresponding '.d.ts' file.
// "emitBOM": false,                 // Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files.
// "emitDeclarationOnly": false,                 // Only emit ‘.d.ts’ declaration files.
// "importHelpers": false,                 // Import emit helpers from 'tslib'.
// "newLine": "LF",                 // Use the specified end of line sequence to be used when emitting files: "crlf" (windows) or "lf" (unix).”
// "noEmit": true,                 // Do not emit outputs.
// "noEmitHelpers": false,                 // Do not generate custom helper functions like __extends in compiled output.
// "noEmitOnError": false,                 // Do not emit outputs if any errors were reported.
// "noImplicitUseStrict": false,                 // Do not emit "use strict" directives in module output.
// "noResolve": false,                 // Do not add triple-slash references or module import targets to the list of compiled files.
"preserveConstEnums": false,                                 // Do not erase const enum declarations in generated code.
// "removeComments": false,                 // Remove all comments except copy-right header comments beginning with
// "experimentalDecorators": true,                 // Enables experimental support for ES7 decorators.
// "emitDecoratorMetadata": true,                 // Enables experimental support for emitting type metadata for decorators.
                                                            // Source map options
// "sourceMap": false,                 // Generates corresponding '.map' file.
// "sourceRoot": "",                 // Specify the location where debugger should locate TypeScript files instead of source locations.
// "mapRoot": "",                 // Specify the location where debugger should locate map files instead of generated locations.
// "inlineSourceMap": true,                 // Emit a single file with source maps instead of having a separate file.
// "inlineSources": true,                 // Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set.
                                                            // JSX options
// "jsx": "preserve",                 // Specify JSX code generation: 'preserve', 'react-native', or 'react'.
// "jsxFactory": "React.createElement",                 // Specify the JSX factory function to use when targeting react JSX emit, e.g. 'React.createElement' or 'h'.
                                                            // Other options
// "allowUmdGlobalAccess": true,                 // Allow accessing UMD globals from modules.
// "charset": "utf8",                 // The character set of the input files.
// "downlevelIteration": false,                 // Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'.
// "disableSizeLimit": false,                 // Disable size limitation on JavaScript project.
// "keyofStringsOnly": false,                 // Resolve 'keyof' to string valued property names only (no numbers or symbols).
// "noLib": false,                 // Do not include the default library file (lib.d.ts).
// "pretty": true,                 // Stylize errors and messages using color and context.
}
}