From 12744061ef80ff7b1f338b96a5fb4c5f3f1250e8 Mon Sep 17 00:00:00 2001 From: Teddy Zeenny Date: Sat, 16 May 2015 15:38:54 +0300 Subject: [PATCH] Adds `Ember.Debugging.RenderDebug` as an api for render debugging. Currently exposes `Ember.Debugging.RenderDebug.getTopLevelNode` which returns the top level inspected node. `InspectedNode` is a wrapper class for the htmlbars render node. It provides public debugging methods so that external tools like the Ember Inspector can use stable and tested apis. --- packages/ember-extension-support/lib/main.js | 3 + .../lib/models/inspected_node.js | 234 ++++++++++++++++++ .../lib/render_debug.js | 18 ++ .../tests/render_debug_test.js | 113 +++++++++ 4 files changed, 368 insertions(+) create mode 100644 packages/ember-extension-support/lib/models/inspected_node.js create mode 100644 packages/ember-extension-support/lib/render_debug.js create mode 100644 packages/ember-extension-support/tests/render_debug_test.js diff --git a/packages/ember-extension-support/lib/main.js b/packages/ember-extension-support/lib/main.js index 41ab1ff18b4..ffc91946753 100644 --- a/packages/ember-extension-support/lib/main.js +++ b/packages/ember-extension-support/lib/main.js @@ -9,6 +9,9 @@ Ember Extension Support import Ember from "ember-metal/core"; import DataAdapter from "ember-extension-support/data_adapter"; import ContainerDebugAdapter from "ember-extension-support/container_debug_adapter"; +import { getTopLevelNode } from "ember-extension-support/render_debug"; Ember.DataAdapter = DataAdapter; Ember.ContainerDebugAdapter = ContainerDebugAdapter; + +Ember.Debugging = { RenderDebug: { getTopLevelNode } }; diff --git a/packages/ember-extension-support/lib/models/inspected_node.js b/packages/ember-extension-support/lib/models/inspected_node.js new file mode 100644 index 00000000000..ff3be6ad2cb --- /dev/null +++ b/packages/ember-extension-support/lib/models/inspected_node.js @@ -0,0 +1,234 @@ +import Component from "ember-views/views/component"; +import { A as emberA } from "ember-runtime/system/native_array"; +import { get } from "ember-metal/property_get"; + +/** + * Wraps a render node to provide debugging API for external services + * such as the Ember Inspector. + * + * Ideally public methods on this class should completely hide + * the internal implementations of render nodes. + * + * @class InspectedNode + * @param {Object} renderNode The Htmlbars render node + * @param {Object} parent The parent InspectedNode instance + */ +function InspectedNode(renderNode, parent) { + this.renderNode = renderNode; + this.parent = parent; +} + +InspectedNode.prototype = { + /** + * Useful for subclassing. + * + * @property constructor + * @type {Function} + */ + constructor: InspectedNode, + + /** + * The render node we are representing and adding + * debugging functionality to. + * + * This property is set upon creation of the instance. + * + * @property renderNode + * @type {Object} + * @private + */ + renderNode: null, + + /** + * The parent inspected node. This property is set + * when building the a node's children. This allows + * us to walk upward in the render hierarchy. + * + * @property parent + * @type {InspectedNode} + */ + parent: null, + + /** + * Fetches the render node's children and returns + * an array of InspectedNode instances (an instance is created + * for each child). + * + * Consecutive calls to this method does *not* return the same + * instances (each call creates new instances). This is why + * the method name is `buildChildren` as opposed to `getChildren`. + * + * @method buildChildren + * @return {Array} array of inspected nodes wrapping the render node's children. + */ + buildChildren() { + var childNodes = emberA(this.renderNode.childNodes); + return childNodes.map(renderNode => new this.constructor(renderNode, this)); + }, + + /** + * Not all nodes are actually views/components. + * Nodes can be attributes for example. This method allows + * us to identify component nodes. + * + * Not to be confused with `isEmberComponent`. `Ember.View` instances + * also return `true`. + * + * @method isComponentNode + * @return {Boolean} + */ + isComponentNode() { + return !!this.renderNode.state.manager; + }, + + /** + * Check if a node has its own controller (as opposed to sharing + * its parent's controller). + * + * Useful to identify route views from other views. Views that have + * their own controller are shown by default in the Ember Inspector + * view tree. + * + * @method hasOwnController + * @return {Boolean} + */ + hasOwnController() { + return !this.parent || (this.getController() !== this.parent.getController()); + }, + + /** + * Check if the node has a view or component instance. + * Virtual nodes don't have a view/component instance. + * + * @method hasComponentInstance + * @return {Boolean} + */ + hasComponentInstance() { + return !!this.getComponentInstance(); + }, + + /** + * Return a node's view/component instance. + * + * @method getComponentInstance + * @return {Ember.View|Ember.Component} + */ + getComponentInstance() { + return this.renderNode.emberView; + }, + + /** + * Returns the node's controller. + * + * @method getController + * @return {Ember.Controller} + */ + getController() { + return this.renderNode.lastResult.scope.locals.controller.value(); + }, + + /** + * Get the node's template name. Relies on an htmlbars + * feature that adds the module name as a meta property + * to compiled templates. + * + * When compiling a template, pass the template's name + * as a the `moduleName` property on the options argument. + * This should be done automatically by tools that pre-compile + * templates (ex: ember-cli does this in version >= 0.2.4) + * + * @method getTemplateName + * @return {String} The template name + */ + getTemplateName() { + var template = this.renderNode.lastResult.template; + if (template && template.meta && template.meta.moduleName) { + return template.meta.moduleName.replace(/\.hbs$/, ''); + } + }, + + /** + * Returns whether the node is an Ember.Component or not. + * *Not* to be confused with `isComponent` which returns true + * for both views and components. + * + * @return {Boolean} + */ + isEmberComponent() { + var componentInstance = this.getComponentInstance(this.renderNode); + return !!(componentInstance && (componentInstance instanceof Component)); + }, + + /** + * The node's model. If the view has a controller, + * it will be the controller's `model` property. + * + * @return {Object} the model + */ + getModel() { + var controller = this.getController(); + if (controller) { + return get(controller, 'model'); + } + }, + + /** + * The name of the component/view instance. + * + * @return {String} + */ + getComponentInstanceName() { + var name; + var componentInstance = this.getComponentInstance(); + if (componentInstance) { + // Has a component instance - take the component's name + name = get(componentInstance, '_debugContainerKey'); + if (name) { + name = name.replace(/.*(view|component):/, '').replace(/:$/, ''); + } + return name; + } + }, + + /** + * The node's name. Should be anything that the user + * can use to identity what node we are talking about. + * + * Usually either the view instance name, or the template name. + * + * @return {String} The node's name + */ + getName() { + var name; + var componentInstanceName = this.getComponentInstanceName(); + if (componentInstanceName) { + name = componentInstanceName; + // If application view was not defined, it uses a `toplevel` view + if (name === 'toplevel') { + name = 'application'; + } + } else { + // Virtual - no component/view instance + var templateName = this.getTemplateName(); + if (templateName) { + name = templateName.replace(/^.*templates\//, '').replace(/\//g, '.'); + } + } + return name; + }, + + /** + * Get the node's bounding client rect. + * Can be used to get the node's position. + * + * @return {DOMRect} + */ + getBoundingClientRect() { + var range = document.createRange(); + range.setStartBefore(this.renderNode.firstNode); + range.setEndAfter(this.renderNode.lastNode); + return range.getBoundingClientRect(); + } +}; + +export default InspectedNode; diff --git a/packages/ember-extension-support/lib/render_debug.js b/packages/ember-extension-support/lib/render_debug.js new file mode 100644 index 00000000000..aedc1ed7bdd --- /dev/null +++ b/packages/ember-extension-support/lib/render_debug.js @@ -0,0 +1,18 @@ +import InspectedNode from 'ember-extension-support/models/inspected_node'; +import $ from "ember-views/system/jquery"; + +/** + * Returns the top level node in an application. + * The returned object will be an `InspectedNode` instance + * which will wrap the htmlbars `renderNode`. + * + * @param {Ember.Application} application + * @return {InspectedNode} The top level node + */ +export function getTopLevelNode(application) { + var topViewId = $(application.rootElement).find('> .ember-view').attr('id'); + var rootView = application.__container__.lookup('-view-registry:main')[topViewId]; + if (rootView) { + return new InspectedNode(rootView._renderNode); + } +} diff --git a/packages/ember-extension-support/tests/render_debug_test.js b/packages/ember-extension-support/tests/render_debug_test.js new file mode 100644 index 00000000000..fa804ce88da --- /dev/null +++ b/packages/ember-extension-support/tests/render_debug_test.js @@ -0,0 +1,113 @@ +import EmberApplication from "ember-application/system/application"; +import run from "ember-metal/run_loop"; +import { getTopLevelNode } from "ember-extension-support/render_debug"; +import compile from "ember-template-compiler/system/compile"; +import Route from "ember-routing/system/route"; +import Component from "ember-views/views/component"; +import View from "ember-views/views/view"; + +var app; + +function lookup(name) { + return app.__container__.lookup(name); +} + +function flattenComponentNodes(node) { + var array = []; + if (node.isComponentNode()) { + array.push(node); + } + node.buildChildren().forEach(child => { + array = array.concat(flattenComponentNodes(child)); + }); + return array; +} + +QUnit.module("Debugging - Render Debug", { + setup() { + run(() => { + app = EmberApplication.create(); + app.deferReadiness(); + }); + }, + + teardown() { + run(app, 'destroy'); + app = null; + } +}); + +QUnit.test('Simple render tree', function() { + let model = {}; + app.registry.register('template:application', compile('{{outlet}}', { moduleName: 'application' })); + app.registry.register('template:index', compile('

Index Page

', { moduleName: 'index' })); + app.registry.register('route:application', Route.extend({ + model() { + return model; + } + })); + + run(app, 'advanceReadiness'); + + var nodes = flattenComponentNodes(getTopLevelNode(app)); + var node = nodes[0]; + equal(node.isComponentNode(), true, 'correctly identified as a component node'); + equal(node.hasOwnController(), true, 'top level always has its own controller'); + equal(node.hasComponentInstance(), true, 'Top level always has its own View instance'); + equal(node.getController(), lookup('controller:application'), 'returns the correct controller'); + equal(node.getTemplateName(), 'application', 'Gets the correct template name'); + equal(node.isEmberComponent(), false, 'top level is a view not a component'); + equal(node.getModel(), model, 'returns the correct model'); + equal(node.getComponentInstanceName(), 'toplevel', 'returns the correct view instance name'); + equal(node.getName(), 'application', 'returns the correct name'); + ok(node.getBoundingClientRect()); + + node = nodes[1]; + equal(node.isComponentNode(), true, 'index correctly identified as a component node'); + equal(node.hasOwnController(), true, 'route nodes always have their own controller'); + equal(node.hasComponentInstance(), false, 'index is virtual - no view instance'); + equal(node.getController(), lookup('controller:index'), 'returns the correct controller'); + equal(node.getTemplateName(), 'index', 'index gets the correct template name'); + equal(node.isEmberComponent(), false, 'index is a view not a component'); + equal(node.getModel(), model, "index has the same model as application"); + equal(node.getName(), 'index', 'returns the correct name'); + ok(node.getBoundingClientRect()); +}); + +QUnit.test('Ember component node', function() { + app.registry.register('template:application', compile('{{foo-bar}}', { moduleName: 'application' })); + app.registry.register('template:components/foo-bar', compile('Foo bar', { moduleName: 'foo-bar' })); + app.registry.register('component:foo-bar', Component); + + run(app, 'advanceReadiness'); + + var nodes = flattenComponentNodes(getTopLevelNode(app)); + var component = nodes[1]; + + equal(component.isComponentNode(), true, 'correctly identified as a component node'); + equal(component.hasComponentInstance(), true, 'Ember component instance detected'); + equal(component.getTemplateName(), 'foo-bar', 'has the correct template name'); + equal(component.isEmberComponent(), true, 'correctly identified as an Ember Component'); + equal(component.getName(), 'foo-bar', 'returns the correct name'); + +}); + +QUnit.test('Inline Ember View', function() { + app.registry.register('template:application', compile('{{view "foo"}}', { moduleName: 'application' })); + app.registry.register('template:foo', compile('Foo View', { moduleName: 'foo' })); + app.registry.register('view:foo', View.extend({ + templateName: 'foo' + })); + + run(app, 'advanceReadiness'); + + var nodes = flattenComponentNodes(getTopLevelNode(app)); + var view = nodes[1]; + + equal(view.isComponentNode(), true, 'correctly identified as a component node'); + equal(view.hasComponentInstance(), true, 'Ember view instance detected'); + equal(view.getTemplateName(), 'foo', 'has the correct template name'); + equal(view.isEmberComponent(), false, 'not an Ember Component'); + equal(view.getName(), 'foo', 'returns the correct name'); + +});