From cfff7182188e6459436786b8b5cf69d9d6de654f Mon Sep 17 00:00:00 2001 From: Patrick Pircher Date: Mon, 18 Mar 2024 17:06:05 +0100 Subject: [PATCH] add modifiers to render tree (#2548) --- app/components/component-tree-arg.js | 9 + app/components/component-tree-item.hbs | 20 +- app/components/object-inspector/property.ts | 4 +- app/controllers/component-tree.js | 51 +++- app/styles/component_tree.scss | 13 + ember-cli-build.js | 7 +- ember_debug/adapters/web-extension.js | 6 +- ember_debug/libs/render-tree.js | 241 ++++++++++++++--- ember_debug/object-inspector.js | 82 +----- ember_debug/utils/get-object-name.js | 4 + ember_debug/utils/type-check.js | 82 +++++- ember_debug/utils/version.js | 37 +++ tests/ember_debug/view-debug-test.js | 280 +++++++++++++++----- tests/index.html | 1 + 14 files changed, 650 insertions(+), 187 deletions(-) diff --git a/app/components/component-tree-arg.js b/app/components/component-tree-arg.js index 00c924fe22..9b7501e61a 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 truncate(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..aa7ceab593 100644 --- a/app/components/component-tree-item.hbs +++ b/app/components/component-tree-item.hbs @@ -23,8 +23,10 @@ {{!-- template-lint-disable no-unbalanced-curlies --}} - {{#if @item.isComponent}} + {{#if (or @item.isComponent @item.isModifier)}} {{#if @item.isCurlyInvocation}} {{@item.name}} @@ -83,6 +85,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 27701979ff..82181b625c 100644 --- a/app/components/object-inspector/property.ts +++ b/app/components/object-inspector/property.ts @@ -121,8 +121,8 @@ export default class ObjectInspectorProperty extends Component c.type === 'modifier') + ) { + const idx = renderNode.children.findLastIndex( + (c) => c.type === 'modifier' + ); + renderNode.children.splice(idx + 1, 0, { + type: 'placeholder-closing-tag', + id: renderNode.id + '-closing-tag', + name: '', + children: [], + }); + } + renderNode.children.forEach((node) => flatten(item, node)); } }; @@ -301,7 +316,32 @@ class RenderItem { } get isComponent() { - return this.renderNode.type === 'component'; + return ( + this.renderNode.type === 'component' || + this.renderNode.type === 'remote-element' + ); + } + + get isModifier() { + return this.renderNode.type === 'modifier'; + } + + get hasModifiers() { + return this.childItems.some((item) => item.isModifier); + } + + get isLastModifier() { + return ( + this.parentItem.childItems.findLast((item) => item.isModifier) === this + ); + } + + get isHtmlTag() { + return this.renderNode.type === 'html-element'; + } + + get isClosingTag() { + return this.renderNode.type === 'placeholder-closing-tag'; } get name() { @@ -313,12 +353,15 @@ class RenderItem { } get isCurlyInvocation() { + if (this.isModifier) { + return true; + } return this.renderNode.args && this.renderNode.args.positional; } get hasInstance() { let { instance } = this.renderNode; - return typeof instance === 'object' && instance !== null; + return typeof instance === 'object' && instance; } get instance() { @@ -330,7 +373,7 @@ class RenderItem { } get hasBounds() { - return this.renderNode.bounds !== null; + return this.renderNode.bounds; } get isRoot() { @@ -400,6 +443,7 @@ class RenderItem { } @action showPreview() { + if (this.isClosingTag) return; this.controller.previewing = this.id; } @@ -410,6 +454,7 @@ class RenderItem { } @action toggleInspection() { + if (this.isClosingTag) return; if (this.isPinned) { this.controller.pinned = undefined; } else { diff --git a/app/styles/component_tree.scss b/app/styles/component_tree.scss index bb399c8e1f..2b92b88c5d 100644 --- a/app/styles/component_tree.scss +++ b/app/styles/component_tree.scss @@ -141,6 +141,19 @@ color: var(--base09); } +.component-tree-item__has_modifier:after { + content: '' !important; +} + +.component-tree-item__closing:before { + content: '>'; + margin-left: -13px; +} + +.component-tree-item__self-closing:before { + content: '/>'; +} + .component-tree-item__bracket:before { content: '<'; } 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/adapters/web-extension.js b/ember_debug/adapters/web-extension.js index d1749b632d..fb98e2a882 100644 --- a/ember_debug/adapters/web-extension.js +++ b/ember_debug/adapters/web-extension.js @@ -28,7 +28,11 @@ export default class extends BasicAdapter { // "clone" them through postMessage unless they are converted to a // native array. options = deepClone(options); - this._chromePort.postMessage(options); + try { + this._chromePort.postMessage(options); + } catch (e) { + console.log('failed to send message', e); + } } /** diff --git a/ember_debug/libs/render-tree.js b/ember_debug/libs/render-tree.js index fab369ff55..64fed9239b 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 { EmberLoader, emberSafeRequire } 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) { @@ -14,6 +17,12 @@ class InElementSupportProvider { // nope } + this.DESTROY = emberSafeRequire('@glimmer/util')?.DESTROY; + this.registerDestructor = + emberSafeRequire('@glimmer/destroyable')?.registerDestructor || + emberSafeRequire('@ember/destroyable')?.registerDestructor || + emberSafeRequire('@ember/runtime')?.registerDestructor; + this.debugRenderTree = owner.lookup('renderer:-dom')?.debugRenderTree || owner.lookup('service:-glimmer-environment')._debugRenderTree; @@ -31,9 +40,10 @@ class InElementSupportProvider { const self = this; const NewElementBuilder = this.NewElementBuilder; - const remoteStack = []; const componentStack = []; + const enableModifierSupport = isInVersionSpecifier('>3.28.0', VERSION); + function createRef(value) { if (self.reference.createUnboundRef) { return self.reference.createUnboundRef(value); @@ -42,6 +52,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 +76,7 @@ class InElementSupportProvider { if (node?.type === 'component') { componentStack.pop(); } - exit.call(this, state); + return exit.call(this, state); }; const didAppendNode = NewElementBuilder.prototype.didAppendNode; @@ -71,13 +91,91 @@ class InElementSupportProvider { args[0].__emberInspectorParentNode = componentStack.at(-1); }; + const pushModifiers = NewElementBuilder.prototype.pushModifiers; + if (enableModifierSupport) { + 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; + let name = + modifier.definition?.resolvedName || + modifierState?.debugName || + instance?.name; + if (!name) { + try { + name = modifier.manager?.getDebugName?.(); + } catch (e) { + // failed + } + name = 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)); + } + } + let named = modifierState?.args?.named; + if (!self.reference.createUnboundRef) { + try { + named = modifierState?.args?.named?.constructor; + } catch (e) { + // + } + try { + named = named || modifierState?.args?.named?.map; + } catch (e) { + // + } + } + 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, + }); + self.registerDestructor(modifier.state, () => { + debugRenderTree?.willDestroy(state); + }); + } + } + return pushModifiers.call(this, modifiers); + }; + } + const pushRemoteElement = NewElementBuilder.prototype.pushRemoteElement; NewElementBuilder.prototype.pushRemoteElement = function ( element, guid, insertBefore ) { - remoteStack.push({ element }); const ref = createRef(element); const capturedArgs = { positional: [ref], @@ -86,18 +184,27 @@ 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); + const block = this.blockStack.current; + + if (this.DESTROY) { + const destructor = block[this.DESTROY]; + block[this.DESTROY] = function () { + self.debugRenderTree?.willDestroy(block); + destructor.call(this); + }; + } else { + self.registerDestructor?.(block, () => { + self.debugRenderTree?.willDestroy(block); + }); + } + + debugRenderTree?.create(block, { type: 'keyword', name: 'in-element', - args: inElementArgs, + args: createArgs(capturedArgs), instance: { args: { named: { @@ -110,21 +217,20 @@ class InElementSupportProvider { }, }, }); - return pushRemoteElement.call(this, element, guid, insertBefore); + return r; }; 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 +242,7 @@ class InElementSupportProvider { pushRemoteElement, popRemoteElement, didAppendNode, + pushModifiers, }; } @@ -148,6 +255,7 @@ class InElementSupportProvider { this.NewElementBuilder.prototype, this.NewElementBuilderFunctions ); + this.NewElementBuilderFunctions = null; } require(req) { @@ -175,7 +283,8 @@ export default class RenderTree { try { this.inElementSupport = new InElementSupportProvider(owner); } catch (e) { - console.error('failed to setup in element support', e); + console.error('failed to setup in element support'); + console.error(e); // not supported } @@ -408,18 +517,47 @@ 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._serializeRenderNode(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) { @@ -460,15 +598,49 @@ export default class RenderTree { this.parentNodes[node.id] = 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); + } + }); + } + + 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; + if (parentNode.instance !== node.bounds.firstNode) { + return this._insertHtmlElementNode(node, parentNode); + } + } + 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), }; @@ -535,7 +707,7 @@ export default class RenderTree { this.retainedObjects.set(object, id); - return { id }; + return { id, type: typeof object, inspect: inspect(object) }; } _releaseStaleObjects() { @@ -578,8 +750,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 a266c4f1fa..fda102f50c 100644 --- a/ember_debug/object-inspector.js +++ b/ember_debug/object-inspector.js @@ -5,9 +5,9 @@ import { isComputed, 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 { EmberObject, meta as emberMeta, @@ -113,6 +113,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) }; } @@ -123,83 +129,13 @@ function inspectValue(object, key, computedValue) { return { type: 'type-descriptor', inspect: string }; } else if (value?.isDescriptor) { 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 && diff --git a/ember_debug/utils/get-object-name.js b/ember_debug/utils/get-object-name.js index c70504c2fa..a01173daf3 100644 --- a/ember_debug/utils/get-object-name.js +++ b/ember_debug/utils/get-object-name.js @@ -7,6 +7,10 @@ export default function getObjectName(object) { (emberNames.get(object.constructor) || object.constructor.name)) || ''; + if (object instanceof Function) { + return 'Function ' + object.name; + } + // check if object is a primitive value if (object !== Object(object)) { return typeof object; diff --git a/ember_debug/utils/type-check.js b/ember_debug/utils/type-check.js index de0f129028..9c71aa48bc 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 { meta as emberMeta, ComputedProperty } 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'; /** @@ -53,3 +57,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..d07b125267 100644 --- a/ember_debug/utils/version.js +++ b/ember_debug/utils/version.js @@ -23,6 +23,43 @@ 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; + } + + if (operator === '>' && specifier[0] > version2[0]) { + return false; + } + + for (i = 0; i < 3; i++) { + compared = compare(+specifier[i], +version2[i]); + if (compared < 0) { + return true; + } + if (compared > 0) { + return false; + } + } + return true; +} + /** * Remove -alpha, -beta, etc from versions * diff --git a/tests/ember_debug/view-debug-test.js b/tests/ember_debug/view-debug-test.js index 53796f7807..3a58d7c860 100644 --- a/tests/ember_debug/view-debug-test.js +++ b/tests/ember_debug/view-debug-test.js @@ -12,10 +12,13 @@ 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'; import setupEmberDebugTest from '../helpers/setup-ember-debug-test'; +import { isInVersionSpecifier } from 'ember-debug/utils/version'; +import { VERSION } from 'ember-debug/utils/ember'; let templateOnlyComponent = null; try { @@ -265,6 +268,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 +474,7 @@ module('Ember Debug - View', function (hooks) { this.owner.register( 'controller:simple', Controller.extend({ + foo() {}, get elementTarget() { return document.querySelector('#target'); }, @@ -501,7 +546,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 +636,8 @@ module('Ember Debug - View', function (hooks) { {{/in-element}} `) ); + + this.owner.register('modifier:did-insert', didInsert); }); test('Simple Inputs Tree', async function () { @@ -596,6 +647,41 @@ 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: 'deprecated-event-handlers', + args: Args({ positionals: 1 }), + }) + ); + } + const enableModifierSupport = isInVersionSpecifier('>3.28.0', VERSION); + if (!enableModifierSupport) { + modifiers.length = 0; + } + if (!hasEmberVersion(3, 26)) { inputChildren.push( Component({ @@ -606,6 +692,19 @@ module('Ember Debug - View', function (hooks) { ); } + if (enableModifierSupport) { + const htmlElement = HtmlElement( + { + name: 'input', + args: Args({ names: ['id', 'class', 'type'] }), + }, + ...modifiers + ); + if (hasEmberVersion(3, 26)) { + inputChildren.push(htmlElement); + } + } + matchTree(tree, [ TopLevel( Route( @@ -634,55 +733,77 @@ module('Ember Debug - View', function (hooks) { let argsTestPromise; + const enableModifierSupport = isInVersionSpecifier('>3.28.0', VERSION); + + const children = [ + 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 }', + 'test-bar args value inspect should be correct' + ); + } + argsTestPromise = testArgsValue(); + }, + }), + 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, + }) + ), + ]; + + const root = []; + + if (enableModifierSupport) { + root.push( + ...[ + HtmlElement( + { + name: 'div', + }, + Modifier({ + name: 'did-insert', + args: Args({ positionals: 1 }), + }), + ...children + ), + ] + ); + } else { + root.push(...children); + } + matchTree(tree, [ TopLevel( - Route( - { 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( - { - 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, - }) - ) - ) - ) + Route({ name: 'application' }, Route({ name: 'simple' }, ...root)) ), ]); + QUnit.assert.ok( argsTestPromise instanceof Promise, 'args should be tested' @@ -716,31 +837,52 @@ module('Ember Debug - View', function (hooks) { let tree = await getRenderTree(); + const root = []; + + const children = [ + Component({ name: 'test-foo', bounds: 'single' }), + Component({ + 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, + }) + ), + ]; + + const enableModifierSupport = isInVersionSpecifier('>3.28.0', VERSION); + + if (enableModifierSupport) { + root.push( + ...[ + HtmlElement( + { + name: 'div', + }, + Modifier({ + name: 'did-insert', + args: Args({ positionals: 1 }), + }), + ...children + ), + ] + ); + } else { + root.push(...children); + } + matchTree(tree, [ TopLevel( - Route( - { name: 'application' }, - Route( - { name: 'simple' }, - Component({ name: 'test-foo', bounds: 'single' }), - Component({ - 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, - }) - ) - ) - ) + Route({ name: 'application' }, Route({ name: 'simple' }, ...root)) ), ]); }); 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 +