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 +