diff --git a/app/components/component-tree-arg.js b/app/components/component-tree-arg.js
index 00c924fe22..df78725e0e 100644
--- a/app/components/component-tree-arg.js
+++ b/app/components/component-tree-arg.js
@@ -12,6 +12,15 @@ export default class ComponentTreeArg extends Component {
get displayValue() {
if (this.isObject) {
+ if (this.args.value.inspect) {
+ if (this.args.value.type === 'function') {
+ return this.args.value.inspect
+ .replace(/"/g, '\\"')
+ .replace('bound ', '')
+ .replace('{ ... }', '');
+ }
+ return this.args.value.inspect.replace(/"/g, '\\"');
+ }
return '...';
} else if (typeof this.args.value === 'string') {
// Escape any interior quotes – we will add the surrounding quotes in the template
diff --git a/app/components/component-tree-item.hbs b/app/components/component-tree-item.hbs
index 450b7ce0b4..b00ca0bd2e 100644
--- a/app/components/component-tree-item.hbs
+++ b/app/components/component-tree-item.hbs
@@ -11,6 +11,7 @@
{{on "mouseenter" @item.showPreview}}
{{on "mouseleave" @item.hidePreview}}
>
+ {{resource @item.id create=@item.load update=@item.update teardown=@item.unload}}
{{#if @item.hasChildren}}
{{!-- template-lint-disable no-unbalanced-curlies --}}
- {{#if @item.isComponent}}
+ {{#if (or @item.isComponent @item.isModifier)}}
{{#if @item.isCurlyInvocation}}
{{@item.name}}
@@ -83,6 +84,20 @@
\{{mount "{{@item.name}}"}}
{{else if @item.isRouteTemplate}}
{{@item.name}} route
+ {{else if @item.isHtmlTag}}
+
+ {{@item.name}}
+
+ {{#each-in @item.args.named as |name value|}}
+
+
+ {{name}}
+
+ ={{if (is-string value) "\""}}
+
+ {{if (is-string value) "\""}}
+
+ {{/each-in}}
{{/if}}
diff --git a/app/components/object-inspector/property.ts b/app/components/object-inspector/property.ts
index f621253784..c258a27701 100644
--- a/app/components/object-inspector/property.ts
+++ b/app/components/object-inspector/property.ts
@@ -119,8 +119,8 @@ export default class ObjectInspectorProperty extends Component 0;
+ }
+
+ @action
+ load() {
+ next(() => {
+ this.renderCounter += 1;
+ if (this.renderNode.args) {
+ return;
+ }
+ console.log('load', this.id);
+ this.send('view:getTreeItem', { id: this.id });
+ });
+ }
+
+ @action
+ async update(prevId) {
+ next(() => {
+ this.controller._store[prevId].renderCounter -= 1;
+ this.renderCounter += 1;
+ if (this.renderNode.args) {
+ return;
+ }
+ console.log('load', this.id);
+ this.send('view:getTreeItem', { id: this.id });
+ });
+ }
+
+ @action
+ async unload() {
+ next(() => {
+ this.renderCounter -= 1;
+ });
+ }
+
get id() {
return this.renderNode.id;
}
@@ -304,6 +353,14 @@ class RenderItem {
return this.renderNode.type === 'component';
}
+ get isModifier() {
+ return this.renderNode.type === 'modifier';
+ }
+
+ get isHtmlTag() {
+ return this.renderNode.type === 'html-element';
+ }
+
get name() {
return this.renderNode.name;
}
@@ -313,6 +370,9 @@ class RenderItem {
}
get isCurlyInvocation() {
+ if (this.isModifier) {
+ return true;
+ }
return this.renderNode.args && this.renderNode.args.positional;
}
diff --git a/app/helpers/resource.js b/app/helpers/resource.js
new file mode 100644
index 0000000000..5b6b7b4b7e
--- /dev/null
+++ b/app/helpers/resource.js
@@ -0,0 +1,25 @@
+import Helper from '@ember/component/helper';
+import { registerDestructor, unregisterDestructor } from '@ember/destroyable';
+
+export default class ResourceHelper extends Helper {
+ compute(positional, named) {
+ const firstTime = !this.updateCallback;
+ this.updateCallback = named.update;
+ if (named.teardown) {
+ if (this.teardownCallback) {
+ unregisterDestructor(this, this.teardownCallback);
+ }
+ this.teardownCallback = named.teardown;
+ registerDestructor(this, this.teardownCallback);
+ }
+ if (this.updateCallback && !firstTime) {
+ this.updateCallback(this.prevState, positional);
+ }
+ if (firstTime && named.create) {
+ named.create();
+ }
+ //access all positional params
+ positional.forEach(() => null);
+ this.prevState = [...positional];
+ }
+}
diff --git a/app/routes/component-tree.js b/app/routes/component-tree.js
index 8a3031b431..ff4dae9634 100644
--- a/app/routes/component-tree.js
+++ b/app/routes/component-tree.js
@@ -28,6 +28,7 @@ export default class ComponentTreeRoute extends TabRoute {
super.activate(...arguments);
this.port.on('view:renderTree', this, this.setRenderTree);
+ this.port.on('view:renderTreeItem', this, this.setRenderTreeItem);
this.port.on('view:cancelSelection', this, this.cancelSelection);
this.port.on('view:startInspecting', this, this.startInspecting);
this.port.on('view:stopInspecting', this, this.stopInspecting);
@@ -38,6 +39,7 @@ export default class ComponentTreeRoute extends TabRoute {
super.deactivate(...arguments);
this.port.off('view:renderTree', this, this.setRenderTree);
+ this.port.off('view:renderTreeItem', this, this.setRenderTreeItem);
this.port.off('view:cancelSelection', this, this.cancelSelection);
this.port.off('view:startInspecting', this, this.startInspecting);
this.port.off('view:stopInspecting', this, this.stopInspecting);
@@ -48,6 +50,10 @@ export default class ComponentTreeRoute extends TabRoute {
this.controller.renderTree = tree;
}
+ setRenderTreeItem({ treeItem }) {
+ this.controller.setRenderTreeItem(treeItem);
+ }
+
cancelSelection({ id, pin }) {
this.controller.cancelSelection(id, pin);
}
diff --git a/ember-cli-build.js b/ember-cli-build.js
index f383accfb7..0b92b165eb 100644
--- a/ember-cli-build.js
+++ b/ember-cli-build.js
@@ -381,19 +381,18 @@ module.exports = function (defaults) {
if (env === 'test') {
// `ember test` expects the index.html file to be in the
// output directory.
- output = mergeTrees([dists.basic, dists.chrome]);
- } else {
// Change base tag for running tests in development env.
dists.basic = replace(dists.basic, {
files: ['tests/index.html'],
patterns: [
{
match: //,
- replacement: '',
+ replacement: '',
},
],
});
-
+ output = mergeTrees([dists.basic, dists.chrome]);
+ } else {
dists.testing = mergeTrees([dists.basic, dists.chrome]);
output = mergeTrees([
diff --git a/ember_debug/libs/render-tree.js b/ember_debug/libs/render-tree.js
index fab369ff55..392bc7b7dd 100644
--- a/ember_debug/libs/render-tree.js
+++ b/ember_debug/libs/render-tree.js
@@ -1,6 +1,9 @@
import captureRenderTree from './capture-render-tree';
import { guidFor } from 'ember-debug/utils/ember/object/internals';
import { EmberLoader } from 'ember-debug/utils/ember/loader';
+import { inspect } from 'ember-debug/utils/type-check';
+import { isInVersionSpecifier } from 'ember-debug/utils/version';
+import { VERSION } from 'ember-debug/utils/ember';
class InElementSupportProvider {
constructor(owner) {
@@ -31,9 +34,45 @@ class InElementSupportProvider {
const self = this;
const NewElementBuilder = this.NewElementBuilder;
- const remoteStack = [];
const componentStack = [];
+ if (isInVersionSpecifier('~3.16.0', VERSION)) {
+ const glimmer = this.require('@ember/-internals/glimmer');
+ const insertKlass = this.require(
+ '@ember/render-modifiers/modifiers/did-insert'
+ ).default;
+ const updateKlass = this.require(
+ '@ember/render-modifiers/modifiers/did-update'
+ ).default;
+ const destroyKlass = this.require(
+ '@ember/render-modifiers/modifiers/will-destroy'
+ ).default;
+ const insert = glimmer.getModifierManager(insertKlass)();
+ const update = glimmer.getModifierManager(updateKlass)();
+ const destroy = glimmer.getModifierManager(destroyKlass)();
+ glimmer.setModifierManager(
+ () => ({
+ ...insert,
+ name: 'did-insert',
+ }),
+ insertKlass
+ );
+ glimmer.setModifierManager(
+ () => ({
+ ...update,
+ name: 'did-update',
+ }),
+ updateKlass
+ );
+ glimmer.setModifierManager(
+ () => ({
+ ...destroy,
+ name: 'will-remove',
+ }),
+ destroyKlass
+ );
+ }
+
function createRef(value) {
if (self.reference.createUnboundRef) {
return self.reference.createUnboundRef(value);
@@ -42,6 +81,16 @@ class InElementSupportProvider {
}
}
+ function createArgs(args) {
+ if (self.reference.createUnboundRef) {
+ return args;
+ } else {
+ return {
+ value: () => args,
+ };
+ }
+ }
+
const appendChild = this.debugRenderTree.appendChild;
this.debugRenderTree.appendChild = function (node, state) {
if (node.type === 'component') {
@@ -56,7 +105,7 @@ class InElementSupportProvider {
if (node?.type === 'component') {
componentStack.pop();
}
- exit.call(this, state);
+ return exit.call(this, state);
};
const didAppendNode = NewElementBuilder.prototype.didAppendNode;
@@ -77,7 +126,6 @@ class InElementSupportProvider {
guid,
insertBefore
) {
- remoteStack.push({ element });
const ref = createRef(element);
const capturedArgs = {
positional: [ref],
@@ -86,18 +134,13 @@ class InElementSupportProvider {
if (insertBefore) {
capturedArgs.named.insertBefore = insertBefore;
}
- const inElementArgs = self.reference.createUnboundRef
- ? capturedArgs
- : {
- value() {
- return capturedArgs;
- },
- };
const debugRenderTree = self.debugRenderTree;
- debugRenderTree?.create(remoteStack.at(-1), {
+
+ const r = pushRemoteElement.call(this, element, guid, insertBefore);
+ debugRenderTree?.create(this.blockStack.current, {
type: 'keyword',
name: 'in-element',
- args: inElementArgs,
+ args: createArgs(capturedArgs),
instance: {
args: {
named: {
@@ -110,21 +153,78 @@ class InElementSupportProvider {
},
},
});
- return pushRemoteElement.call(this, element, guid, insertBefore);
+ return r;
+ };
+
+ const pushModifiers = NewElementBuilder.prototype.pushModifiers;
+ NewElementBuilder.prototype.pushModifiers = function (modifiers) {
+ const debugRenderTree = self.debugRenderTree;
+ if (debugRenderTree) {
+ modifiers = modifiers || [];
+ const modifier = modifiers[0];
+ let element = null;
+ if (modifiers.length) {
+ element = modifier[1]?.element || modifier.state.element;
+ }
+ for (const modifier of modifiers) {
+ const state = {};
+ const modifierState =
+ modifier.state.instance || modifier.state || modifier[1];
+ const instance = modifierState?.instance || modifierState?.delegate;
+ const name =
+ modifier.definition?.resolvedName ||
+ modifier.manager?.getDebugName?.() ||
+ modifierState?.debugName ||
+ instance?.name ||
+ 'unknown-modifier';
+ const args = {
+ positional: [],
+ named: {},
+ };
+ const positional =
+ modifierState?.args?.positional?.references ||
+ modifierState?.args?.positional ||
+ [];
+ for (const value of positional) {
+ if (value && value[self.reference.REFERENCE]) {
+ args.positional.push(value);
+ } else {
+ args.positional.push(createRef(value));
+ }
+ }
+ const named = modifierState?.args?.named?.constructor
+ ? modifierState?.args?.named?.map
+ : modifierState?.args?.named;
+ for (const [key, value] of Object.entries(named || {})) {
+ args.named[key] = createRef(value);
+ }
+ debugRenderTree?.create(state, {
+ type: 'modifier',
+ name,
+ args: createArgs(args),
+ instance: instance,
+ });
+ debugRenderTree?.didRender(state, {
+ parentElement: () => element.parentElement,
+ firstNode: () => element,
+ lastNode: () => element,
+ });
+ }
+ }
+ return pushModifiers.call(this, modifiers);
};
const popRemoteElement = NewElementBuilder.prototype.popRemoteElement;
NewElementBuilder.prototype.popRemoteElement = function (...args) {
- const element = this.element;
+ const block = this.blockStack.current;
popRemoteElement.call(this, ...args);
const parentElement = this.element;
const debugRenderTree = self.debugRenderTree;
- debugRenderTree?.didRender(remoteStack.at(-1), {
+ debugRenderTree?.didRender(block, {
parentElement: () => parentElement,
- firstNode: () => element,
- lastNode: () => element,
+ firstNode: () => block.firstNode(),
+ lastNode: () => block.lastNode(),
});
- remoteStack.pop();
};
this.debugRenderTreeFunctions = {
@@ -136,6 +236,7 @@ class InElementSupportProvider {
pushRemoteElement,
popRemoteElement,
didAppendNode,
+ pushModifiers,
};
}
@@ -213,11 +314,7 @@ export default class RenderTree {
this._reset();
this.tree = captureRenderTree(this.owner);
- let serialized = this._serializeRenderNodes(this.tree);
-
- this._releaseStaleObjects();
-
- return serialized;
+ return this._createSimpleNodes(this.tree);
}
/**
@@ -408,28 +505,109 @@ export default class RenderTree {
this.retainedObjects = new Map();
}
- _createTemplateOnlyComponent(args) {
+ _createSimpleInstance(name, args) {
const obj = Object.create(null);
obj.args = args;
obj.constructor = {
- name: 'TemplateOnlyComponent',
+ name: name,
comment: 'fake constructor',
};
return obj;
}
+ _insertHtmlElementNode(node, parentNode) {
+ const element = node.bounds.firstNode;
+ const htmlNode = {
+ id: node.id + 'html-element',
+ type: 'html-element',
+ name: element.tagName.toLowerCase(),
+ instance: element,
+ template: null,
+ bounds: {
+ firstNode: element,
+ lastNode: element,
+ parentElement: element.parentElement,
+ },
+ args: {
+ named: {},
+ positional: [],
+ },
+ children: [],
+ };
+ const idx = parentNode.children.indexOf(node);
+ parentNode.children.splice(idx, 0, htmlNode);
+ return this._createSimpleNode(htmlNode, parentNode);
+ }
+
_serializeRenderNodes(nodes, parentNode = null) {
- return nodes.map((node) => this._serializeRenderNode(node, parentNode));
+ const mapped = [];
+ // nodes can be mutated during serialize, which is why we use indexing instead of .map
+ for (let i = 0; i < nodes.length; i++) {
+ mapped.push(this._serializeRenderNode(nodes[i], parentNode));
+ }
+ return mapped;
}
- _serializeRenderNode(node, parentNode = null) {
+ _createSimpleNode(node, parentNode) {
if (!node.id.startsWith(this.renderNodeIdPrefix)) {
node.id = `${this.renderNodeIdPrefix}-${node.id}`;
}
+
+ this.nodes[node.id] = node;
+ this.parentNodes[node.id] = parentNode;
+
+ if (node.type === 'modifier') {
+ if (parentNode.instance !== node.bounds.firstNode) {
+ return this._insertHtmlElementNode(node, parentNode);
+ }
+ }
+
+ if (node.type === 'html-element') {
+ // show set attributes in inspector
+ Array.from(node.instance.attributes).forEach((attr) => {
+ node.args.named[attr.nodeName] = attr.nodeValue;
+ });
+ // move modifiers and components into the element children
+ parentNode.children.forEach((child) => {
+ if (
+ child.bounds.parentElement === node.instance ||
+ (child.type === 'modifier' &&
+ child.bounds.firstNode === node.instance)
+ ) {
+ node.children.push(child);
+ }
+ });
+ node.children.forEach((child) => {
+ const idx = parentNode.children.indexOf(child);
+ if (idx >= 0) {
+ parentNode.children.splice(idx, 1);
+ }
+ });
+ }
+
+ return {
+ id: node.id,
+ type: node.type,
+ name: node.name,
+ children: this._createSimpleNodes(node.children, node),
+ };
+ }
+
+ _createSimpleNodes(nodes, parentNode = null) {
+ const mapped = [];
+ // nodes can be mutated during serialize, which is why we use indexing instead of .map
+ for (let i = 0; i < nodes.length; i++) {
+ mapped.push(this._createSimpleNode(nodes[i], parentNode));
+ }
+ return mapped;
+ }
+
+ _serializeRenderNode(nodeId) {
+ const node = this.nodes[nodeId];
+ if (!node) return null;
let serialized = this.serialized[node.id];
if (serialized === undefined) {
- this.nodes[node.id] = node;
if (node.type === 'keyword') {
node.type = 'component';
this.inElementSupport?.nodeMap.set(node, node.id);
@@ -456,22 +634,26 @@ export default class RenderTree {
});
}
- if (parentNode) {
- this.parentNodes[node.id] = parentNode;
+ if (node.type === 'component' && !node.instance) {
+ node.instance = this._createSimpleInstance(
+ 'TemplateOnlyComponent',
+ node.args.named
+ );
+ }
+
+ if (node.type === 'modifier') {
+ node.instance =
+ node.instance || this._createSimpleInstance(node.name, node.args);
+ node.instance.toString = () => node.name;
}
this.serialized[node.id] = serialized = {
...node,
args: this._serializeArgs(node.args),
- instance: this._serializeItem(
- node.instance ||
- (node.type === 'component'
- ? this._createTemplateOnlyComponent(node.args.named)
- : undefined)
- ),
+ instance: this._serializeItem(node.instance),
bounds: this._serializeBounds(node.bounds),
- children: this._serializeRenderNodes(node.children, node),
};
+ delete serialized.children;
}
return serialized;
@@ -527,15 +709,10 @@ export default class RenderTree {
}
_serializeObject(object) {
- let id = this.previouslyRetainedObjects.get(object);
-
- if (id === undefined) {
- id = this.retainObject(object);
- }
-
+ let id = this.retainObject(object);
this.retainedObjects.set(object, id);
- return { id };
+ return { id, type: typeof object, inspect: inspect(object) };
}
_releaseStaleObjects() {
@@ -578,8 +755,15 @@ export default class RenderTree {
while (candidates.length > 0) {
let candidate = candidates.shift();
let range = this.getRange(candidate.id);
+ const isAllowed =
+ candidate.type !== 'modifier' && candidate.type !== 'html-element';
+
+ if (!isAllowed) {
+ candidates.push(...candidate.children);
+ continue;
+ }
- if (range && range.isPointInRange(dom, 0)) {
+ if (isAllowed && range && range.isPointInRange(dom, 0)) {
// We may be able to find a more exact match in one of the children.
return (
this._matchRenderNodes(candidate.children, dom, false) || candidate
diff --git a/ember_debug/object-inspector.js b/ember_debug/object-inspector.js
index fec9f0ef51..6c0c6171dc 100644
--- a/ember_debug/object-inspector.js
+++ b/ember_debug/object-inspector.js
@@ -6,9 +6,9 @@ import {
isDescriptor,
getDescriptorFor,
typeOf,
+ inspect,
} from 'ember-debug/utils/type-check';
import { compareVersion } from 'ember-debug/utils/version';
-import { inspect as emberInspect } from 'ember-debug/utils/ember/debug';
import Ember, { EmberObject } from 'ember-debug/utils/ember';
import { cacheFor, guidFor } from 'ember-debug/utils/ember/object/internals';
import { _backburner, join } from 'ember-debug/utils/ember/runloop';
@@ -107,6 +107,12 @@ function inspectValue(object, key, computedValue) {
// TODO: this is not very clean. We should refactor calculateCP, etc, rather than passing computedValue
if (computedValue !== undefined) {
+ if (value instanceof HTMLElement) {
+ return {
+ type: 'type-object',
+ inspect: `<${value.tagName.toLowerCase()}>`,
+ };
+ }
return { type: `type-${typeOf(value)}`, inspect: inspect(value) };
}
@@ -117,83 +123,13 @@ function inspectValue(object, key, computedValue) {
return { type: 'type-descriptor', inspect: string };
} else if (isDescriptor(value)) {
return { type: 'type-descriptor', inspect: value.toString() };
+ } else if (value instanceof HTMLElement) {
+ return { type: 'type-object', inspect: value.tagName.toLowerCase() };
} else {
return { type: `type-${typeOf(value)}`, inspect: inspect(value) };
}
}
-function inspect(value) {
- if (typeof value === 'function') {
- return 'function() { ... }';
- } else if (value instanceof EmberObject) {
- return value.toString();
- } else if (typeOf(value) === 'array') {
- if (value.length === 0) {
- return '[]';
- } else if (value.length === 1) {
- return `[ ${inspect(value[0])} ]`;
- } else {
- return `[ ${inspect(value[0])}, ... ]`;
- }
- } else if (value instanceof Error) {
- return `Error: ${value.message}`;
- } else if (value === null) {
- return 'null';
- } else if (typeOf(value) === 'date') {
- return value.toString();
- } else if (typeof value === 'object') {
- // `Ember.inspect` is able to handle this use case,
- // but it is very slow as it loops over all props,
- // so summarize to just first 2 props
- // if it defines a toString, we use that instead
- if (
- typeof value.toString === 'function' &&
- value.toString !== Object.prototype.toString &&
- value.toString !== Function.prototype.toString
- ) {
- try {
- return ``;
- } catch (e) {
- //
- }
- }
- let ret = [];
- let v;
- let count = 0;
- let broken = false;
-
- for (let key in value) {
- if (!('hasOwnProperty' in value) || value.hasOwnProperty(key)) {
- if (count++ > 1) {
- broken = true;
- break;
- }
- v = value[key];
- if (v === 'toString') {
- continue;
- } // ignore useless items
- if (typeOf(v).includes('function')) {
- v = 'function() { ... }';
- }
- if (typeOf(v) === 'array') {
- v = `[Array : ${v.length}]`;
- }
- if (typeOf(v) === 'object') {
- v = '[Object]';
- }
- ret.push(`${key}: ${v}`);
- }
- }
- let suffix = ' }';
- if (broken) {
- suffix = ' ...}';
- }
- return `{ ${ret.join(', ')}${suffix}`;
- } else {
- return emberInspect(value);
- }
-}
-
function isMandatorySetter(descriptor) {
if (descriptor.set && descriptor.set === Ember.MANDATORY_SETTER_FUNCTION) {
return true;
diff --git a/ember_debug/utils/type-check.js b/ember_debug/utils/type-check.js
index 0dcb7f4add..43a9fd0f2c 100644
--- a/ember_debug/utils/type-check.js
+++ b/ember_debug/utils/type-check.js
@@ -1,5 +1,9 @@
-import Debug from 'ember-debug/utils/ember/debug';
-import { ComputedProperty, meta as emberMeta } from 'ember-debug/utils/ember';
+import Debug, { inspect as emberInspect } from 'ember-debug/utils/ember/debug';
+import {
+ ComputedProperty,
+ EmberObject,
+ meta as emberMeta,
+} from 'ember-debug/utils/ember';
import { emberSafeRequire } from 'ember-debug/utils/ember/loader';
/**
@@ -61,3 +65,77 @@ export function typeOf(obj) {
.match(/\s([a-zA-Z]+)/)[1]
.toLowerCase();
}
+
+export function inspect(value) {
+ if (typeof value === 'function') {
+ return `${value.name || 'function'}() { ... }`;
+ } else if (value instanceof EmberObject) {
+ return value.toString();
+ } else if (value instanceof HTMLElement) {
+ return `<${value.tagName.toLowerCase()}>`;
+ } else if (typeOf(value) === 'array') {
+ if (value.length === 0) {
+ return '[]';
+ } else if (value.length === 1) {
+ return `[ ${inspect(value[0])} ]`;
+ } else {
+ return `[ ${inspect(value[0])}, ... ]`;
+ }
+ } else if (value instanceof Error) {
+ return `Error: ${value.message}`;
+ } else if (value === null) {
+ return 'null';
+ } else if (typeOf(value) === 'date') {
+ return value.toString();
+ } else if (typeof value === 'object') {
+ // `Ember.inspect` is able to handle this use case,
+ // but it is very slow as it loops over all props,
+ // so summarize to just first 2 props
+ // if it defines a toString, we use that instead
+ if (
+ typeof value.toString === 'function' &&
+ value.toString !== Object.prototype.toString &&
+ value.toString !== Function.prototype.toString
+ ) {
+ try {
+ return ``;
+ } catch (e) {
+ //
+ }
+ }
+ let ret = [];
+ let v;
+ let count = 0;
+ let broken = false;
+
+ for (let key in value) {
+ if (!('hasOwnProperty' in value) || value.hasOwnProperty(key)) {
+ if (count++ > 1) {
+ broken = true;
+ break;
+ }
+ v = value[key];
+ if (v === 'toString') {
+ continue;
+ } // ignore useless items
+ if (typeOf(v).includes('function')) {
+ v = `function ${v.name}() { ... }`;
+ }
+ if (typeOf(v) === 'array') {
+ v = `[Array : ${v.length}]`;
+ }
+ if (typeOf(v) === 'object') {
+ v = '[Object]';
+ }
+ ret.push(`${key}: ${v}`);
+ }
+ }
+ let suffix = ' }';
+ if (broken) {
+ suffix = ' ...}';
+ }
+ return `{ ${ret.join(', ')}${suffix}`;
+ } else {
+ return emberInspect(value);
+ }
+}
diff --git a/ember_debug/utils/version.js b/ember_debug/utils/version.js
index 45eb3020db..5a42812d17 100644
--- a/ember_debug/utils/version.js
+++ b/ember_debug/utils/version.js
@@ -23,6 +23,35 @@ export function compareVersion(version1, version2) {
return 0;
}
+/**
+ *
+ * @param specifier e.g. ^5.12.0
+ * @param version 5.13
+ * @return {boolean}
+ */
+export function isInVersionSpecifier(specifier, version) {
+ let compared, i, version2;
+ let operator = specifier[0];
+ if (Number.isNaN(operator)) {
+ specifier = specifier.slice(1);
+ }
+ specifier = cleanupVersion(specifier).split('.');
+ version2 = cleanupVersion(version).split('.');
+ if (operator === '~' && specifier[1] !== version2[1]) {
+ return false;
+ }
+ if (operator === '^' && specifier[0] !== version2[0]) {
+ return false;
+ }
+ for (i = 0; i < 3; i++) {
+ compared = compare(+specifier[i], +version2[i]);
+ if (compared > 0) {
+ return false;
+ }
+ }
+ return true;
+}
+
/**
* Remove -alpha, -beta, etc from versions
*
diff --git a/ember_debug/view-debug.js b/ember_debug/view-debug.js
index d56e7f308e..3d1a259f3a 100644
--- a/ember_debug/view-debug.js
+++ b/ember_debug/view-debug.js
@@ -20,6 +20,10 @@ export default class extends DebugPort {
this.sendTree(immediate);
},
+ getTreeItem({ id }) {
+ this.sendTreeItem(id);
+ },
+
showInspection({ id, pin }) {
this.viewInspection.show(id, pin);
},
@@ -151,6 +155,12 @@ export default class extends DebugPort {
}, 250);
}
+ sendTreeItem(id) {
+ this.sendMessage('renderTreeItem', {
+ treeItem: this.renderTree._serializeRenderNode(id),
+ });
+ }
+
send() {
if (this.isDestroying || this.isDestroyed) {
return;
diff --git a/tests/ember_debug/view-debug-test.js b/tests/ember_debug/view-debug-test.js
index 53796f7807..a67f687e43 100644
--- a/tests/ember_debug/view-debug-test.js
+++ b/tests/ember_debug/view-debug-test.js
@@ -12,6 +12,7 @@ import EmberComponent from '@ember/component';
import EmberRoute from '@ember/routing/route';
import EmberObject from '@ember/object';
import Controller from '@ember/controller';
+import didInsert from '@ember/render-modifiers/modifiers/did-insert';
import QUnit, { module, test } from 'qunit';
import { hbs } from 'ember-cli-htmlbars';
import EmberDebug from 'ember-debug/main';
@@ -86,6 +87,23 @@ async function getRenderTree() {
EmberDebug.port.trigger('view:getTree', {});
});
+ const all = [];
+ const stack = [...message.tree];
+ while (stack.length) {
+ const item = stack.pop();
+ all.push(item);
+ stack.push(...item.children);
+ }
+
+ const fetchAll = all.map(async (item) => {
+ const message = await captureMessage('view:renderTreeItem', async () => {
+ EmberDebug.port.trigger('view:getTreeItem', { id: item.id });
+ });
+ Object.assign(item, message.treeItem);
+ });
+
+ await Promise.all(fetchAll);
+
if (message) {
return message.tree;
}
@@ -265,6 +283,47 @@ function Component(
);
}
+function Modifier(
+ {
+ name,
+ instance = Serialized(),
+ template = null,
+ bounds = 'single',
+ ...options
+ },
+ ...children
+) {
+ return RenderNode(
+ { name, instance, template, bounds, ...options, type: 'modifier' },
+ ...children
+ );
+}
+
+function HtmlElement(
+ {
+ name,
+ instance = Serialized(),
+ args = Args(),
+ template = null,
+ bounds = 'single',
+ ...options
+ },
+ ...children
+) {
+ return RenderNode(
+ {
+ name,
+ instance,
+ args,
+ template,
+ bounds,
+ ...options,
+ type: 'html-element',
+ },
+ ...children
+ );
+}
+
function Route(
{
name,
@@ -430,6 +489,7 @@ module('Ember Debug - View', function (hooks) {
this.owner.register(
'controller:simple',
Controller.extend({
+ foo() {},
get elementTarget() {
return document.querySelector('#target');
},
@@ -501,7 +561,11 @@ module('Ember Debug - View', function (hooks) {
this.owner.register(
'template:simple',
hbs(
- 'Simple {{test-foo}} {{test-bar value=(hash x=123 [x.y]=456)}} {{#in-element this.elementTarget}}{{/in-element}}',
+ `
+
+ Simple {{test-foo}} {{test-bar value=(hash x=123 [x.y]=456)}} {{#in-element this.elementTarget}}{{/in-element}}
+
+ `,
{
moduleName: 'my-app/templates/simple.hbs',
}
@@ -587,6 +651,8 @@ module('Ember Debug - View', function (hooks) {
{{/in-element}}
`)
);
+
+ this.owner.register('modifier:did-insert', didInsert);
});
test('Simple Inputs Tree', async function () {
@@ -596,6 +662,43 @@ module('Ember Debug - View', function (hooks) {
const inputChildren = [];
// https://github.com/emberjs/ember.js/commit/e6cf1766f8e02ddb24bf67833c148e7d7c93182f
+ const modifiers = [
+ Modifier({
+ name: 'on',
+ args: Args({ positionals: 2 }),
+ }),
+ Modifier({
+ name: 'on',
+ args: Args({ positionals: 2 }),
+ }),
+ Modifier({
+ name: 'on',
+ args: Args({ positionals: 2 }),
+ }),
+ Modifier({
+ name: 'on',
+ args: Args({ positionals: 2 }),
+ }),
+ Modifier({
+ name: 'on',
+ args: Args({ positionals: 2 }),
+ }),
+ ];
+ if (hasEmberVersion(3, 28) && !hasEmberVersion(4, 0)) {
+ modifiers.push(
+ Modifier({
+ name: 'unknown-modifier',
+ args: Args({ positionals: 1 }),
+ })
+ );
+ }
+ const htmlElement = HtmlElement(
+ {
+ name: 'input',
+ args: Args({ names: ['id', 'class', 'type'] }),
+ },
+ ...modifiers
+ );
if (!hasEmberVersion(3, 26)) {
inputChildren.push(
Component({
@@ -604,6 +707,8 @@ module('Ember Debug - View', function (hooks) {
args: Args({ names: ['target', 'value'], positionals: 0 }),
})
);
+ } else {
+ inputChildren.push(htmlElement);
}
matchTree(tree, [
@@ -640,44 +745,53 @@ module('Ember Debug - View', function (hooks) {
{ name: 'application' },
Route(
{ name: 'simple' },
- Component({ name: 'test-foo', bounds: 'single' }),
- Component({
- name: 'test-bar',
- bounds: 'range',
- args: Args({ names: ['value'], positionals: 0 }),
- instance: (actual) => {
- async function testArgsValue() {
- const value = await digDeeper(actual.id, 'args');
- QUnit.assert.equal(
- value.details[0].properties[0].value.inspect,
- '{ x: 123, x.y: 456 }',
- 'value inspect should be correct'
- );
- }
- argsTestPromise = testArgsValue();
- },
- }),
- Component(
+ HtmlElement(
{
- name: 'in-element',
- args: (actual) => {
- QUnit.assert.ok(actual.positional[0]);
+ name: 'div',
+ },
+ Modifier({
+ name: 'did-insert',
+ args: Args({ positionals: 1 }),
+ }),
+ Component({ name: 'test-foo', bounds: 'single' }),
+ Component({
+ name: 'test-bar',
+ bounds: 'range',
+ args: Args({ names: ['value'], positionals: 0 }),
+ instance: (actual) => {
async function testArgsValue() {
- const value = await inspectById(actual.positional[0].id);
+ const value = await digDeeper(actual.id, 'args');
QUnit.assert.equal(
- value.details[1].name,
- 'HTMLDivElement',
- 'in-element args value inspect should be correct'
+ value.details[0].properties[0].value.inspect,
+ '{ x: 123, x.y: 456 }',
+ 'test-bar args value inspect should be correct'
);
}
argsTestPromise = testArgsValue();
},
- template: null,
- },
- Component({
- name: 'test-component-in-in-element',
- template: () => null,
- })
+ }),
+ Component(
+ {
+ name: 'in-element',
+ args: (actual) => {
+ QUnit.assert.ok(actual.positional[0]);
+ async function testArgsValue() {
+ const value = await inspectById(actual.positional[0].id);
+ QUnit.assert.equal(
+ value.details[1].name,
+ 'HTMLDivElement',
+ 'in-element args value inspect should be correct'
+ );
+ }
+ argsTestPromise = testArgsValue();
+ },
+ template: null,
+ },
+ Component({
+ name: 'test-component-in-in-element',
+ template: () => null,
+ })
+ )
)
)
)
@@ -722,22 +836,31 @@ module('Ember Debug - View', function (hooks) {
{ name: 'application' },
Route(
{ name: 'simple' },
- Component({ name: 'test-foo', bounds: 'single' }),
- Component({
- name: 'test-bar',
- bounds: 'range',
- args: Args({ names: ['value'], positionals: 0 }),
- }),
- Component(
+ HtmlElement(
{
- name: 'in-element',
- args: Args({ names: [], positionals: 1 }),
- template: null,
+ name: 'div',
},
+ Modifier({
+ name: 'did-insert',
+ args: Args({ positionals: 1 }),
+ }),
+ Component({ name: 'test-foo', bounds: 'single' }),
Component({
- name: 'test-component-in-in-element',
- template: () => null,
- })
+ name: 'test-bar',
+ bounds: 'range',
+ args: Args({ names: ['value'], positionals: 0 }),
+ }),
+ Component(
+ {
+ name: 'in-element',
+ args: Args({ names: [], positionals: 1 }),
+ template: null,
+ },
+ Component({
+ name: 'test-component-in-in-element',
+ template: () => null,
+ })
+ )
)
)
)
diff --git a/tests/index.html b/tests/index.html
index 83f808c012..daf58d259a 100644
--- a/tests/index.html
+++ b/tests/index.html
@@ -5,6 +5,7 @@
EmberInspector Tests
+