diff --git a/packages/@ember/-internals/glimmer/lib/base-renderer.ts b/packages/@ember/-internals/glimmer/lib/base-renderer.ts index d604056fb05..90addfe68e5 100644 --- a/packages/@ember/-internals/glimmer/lib/base-renderer.ts +++ b/packages/@ember/-internals/glimmer/lib/base-renderer.ts @@ -30,6 +30,7 @@ import type { SimpleDocument, SimpleElement } from '@simple-dom/interface'; import { hasDOM } from '../../browser-environment'; import { EmberEnvironmentDelegate } from './environment'; import ResolverImpl from './resolver'; +import { renderers } from './renderers'; import { EvaluationContextImpl } from '@glimmer/opcode-compiler/lib/program-context'; export type IBuilder = (env: Environment, cursor: Cursor) => TreeBuilder; @@ -134,8 +135,6 @@ export class ComponentRootState implements RendererRoot { } } -const renderers: BaseRenderer[] = []; - export function _resetRenderers() { renderers.length = 0; } diff --git a/packages/@ember/-internals/glimmer/lib/renderers.ts b/packages/@ember/-internals/glimmer/lib/renderers.ts new file mode 100644 index 00000000000..b1de3272d91 --- /dev/null +++ b/packages/@ember/-internals/glimmer/lib/renderers.ts @@ -0,0 +1,12 @@ +import type { BaseRenderer } from './base-renderer'; + +/** + * Every renderer that has at least one live root. + * + * This covers each application's `renderer:-dom` + * and every renderer that `renderComponent` created. + * + * It lives in its own module so that `@ember/debug` + * can read it without importing the renderer. + */ +export const renderers: BaseRenderer[] = []; diff --git a/packages/@ember/-internals/glimmer/tests/integration/application/debug-render-tree-test.ts b/packages/@ember/-internals/glimmer/tests/integration/application/debug-render-tree-test.ts index 8cd7e1568c1..0bbf0471c5d 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/application/debug-render-tree-test.ts +++ b/packages/@ember/-internals/glimmer/tests/integration/application/debug-render-tree-test.ts @@ -1,7 +1,11 @@ import { ApplicationTestCase, ModuleBasedTestResolver, moduleFor } from 'internal-test-helpers'; import { ENV } from '@ember/-internals/environment'; -import { Component as EmberComponent, setComponentManager } from '@ember/-internals/glimmer'; +import { + Component as EmberComponent, + renderComponent, + setComponentManager, +} from '@ember/-internals/glimmer'; import Component from '@glimmer/component'; import type { InternalOwner } from '@ember/-internals/owner'; import Route from '@ember/routing/route'; @@ -1128,6 +1132,101 @@ if (ENV._DEBUG_RENDER_TREE) { ]); } + async '@test components rendered with renderComponent'() { + await this.visit('/'); + + let HelloWorld = setComponentTemplate( + precompileTemplate('Hello {{@name}}', { strictMode: true }), + templateOnly() + ); + let Root = setComponentTemplate( + precompileTemplate('', { + strictMode: true, + scope: () => ({ HelloWorld }), + }), + templateOnly() + ); + let into = document.createElement('div'); + + let result = runTask(() => renderComponent(Root, { owner: this.owner, into })); + + let roots = captureRenderTree(this.owner).filter((node) => node.type === 'component'); + + this.assertRenderNodes( + roots, + [ + { + type: 'component', + name: '{ROOT}', + args: { positional: [], named: {} }, + instance: null, + bounds: this.elementBounds(into), + children: [ + { + type: 'component', + name: 'HelloWorld', + args: { positional: [], named: { name: 'world' } }, + instance: null, + bounds: this.elementBounds(into), + children: [], + }, + ], + }, + ], + 'root' + ); + + runTask(() => result.destroy()); + } + + async '@test components rendered with renderComponent and a different owner'() { + await this.visit('/'); + + let HelloWorld = setComponentTemplate( + precompileTemplate('Hello {{@name}}', { strictMode: true }), + templateOnly() + ); + let Root = setComponentTemplate( + precompileTemplate('', { + strictMode: true, + scope: () => ({ HelloWorld }), + }), + templateOnly() + ); + let owner = {}; + let into = document.createElement('div'); + + let result = runTask(() => renderComponent(Root, { owner, into })); + + let roots = captureRenderTree(this.owner).filter((node) => node.type === 'component'); + + this.assertRenderNodes( + roots, + [ + { + type: 'component', + name: '{ROOT}', + args: { positional: [], named: {} }, + instance: null, + bounds: this.elementBounds(into), + children: [ + { + type: 'component', + name: 'HelloWorld', + args: { positional: [], named: { name: 'world' } }, + instance: null, + bounds: this.elementBounds(into), + children: [], + }, + ], + }, + ], + 'root' + ); + + runTask(() => result.destroy()); + } + async '@test components'() { this.add( 'template:application', diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/render-component-test.ts b/packages/@ember/-internals/glimmer/tests/integration/components/render-component-test.ts index 85319efad4c..479cafe6b6e 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/components/render-component-test.ts +++ b/packages/@ember/-internals/glimmer/tests/integration/components/render-component-test.ts @@ -27,6 +27,9 @@ import { trackedObject } from '@ember/reactive/collections'; import { cached, tracked } from '@glimmer/tracking'; import Service, { service } from '@ember/service'; import type Owner from '@ember/owner'; +import { ENV } from '@ember/-internals/environment'; +import { captureRenderTree } from '@ember/debug'; +import type { CapturedRenderNode } from '@glimmer/interfaces'; class RenderComponentTestCase extends AbstractStrictTestCase { declare component: (RenderResult & { rerender: () => void }) | undefined; @@ -119,9 +122,75 @@ moduleFor( assertHTML(''); } + + '@test captureRenderTree includes the rendered components'(assert: QUnit['assert']) { + let HelloWorld = setComponentTemplate(precompileTemplate('Hello, world!'), templateOnly()); + let Root = setComponentTemplate( + precompileTemplate('', { strictMode: true, scope: () => ({ HelloWorld }) }), + templateOnly() + ); + + this.renderComponent(Root, { expect: 'Hello, world!' }); + + if (!ENV._DEBUG_RENDER_TREE) return; + + assert.deepEqual(renderTreeNames(this.owner), ['{ROOT}', 'HelloWorld']); + } + + '@test captureRenderTree includes components rendered with any owner'(assert: QUnit['assert']) { + let Owned = setComponentTemplate(precompileTemplate('owned'), templateOnly()); + let Ownerless = setComponentTemplate(precompileTemplate('ownerless'), templateOnly()); + let OwnedRoot = setComponentTemplate( + precompileTemplate('', { strictMode: true, scope: () => ({ Owned }) }), + templateOnly() + ); + let OwnerlessRoot = setComponentTemplate( + precompileTemplate('', { strictMode: true, scope: () => ({ Ownerless }) }), + templateOnly() + ); + + let ownedElement = document.createElement('div'); + let ownerlessElement = document.createElement('div'); + this.element.append(ownedElement, ownerlessElement); + + let results = run(() => [ + renderComponent(OwnedRoot, { owner: this.owner, into: ownedElement }), + renderComponent(OwnerlessRoot, { into: ownerlessElement }), + ]); + + assertHTML('
owned
ownerless
'); + + if (ENV._DEBUG_RENDER_TREE) { + assert.deepEqual(renderTreeNames(this.owner), ['{ROOT}', 'Owned', '{ROOT}', 'Ownerless']); + } + + run(() => { + for (let result of results) { + result.destroy(); + } + }); + + if (ENV._DEBUG_RENDER_TREE) { + assert.deepEqual(renderTreeNames(this.owner), [], 'destroyed renders are gone'); + } + + run(() => destroy(this)); + } } ); +function renderTreeNames(owner: Owner): string[] { + let names: string[] = []; + let collect = (nodes: CapturedRenderNode[]) => { + for (let node of nodes) { + names.push(node.name); + collect(node.children); + } + }; + collect(captureRenderTree(owner)); + return names; +} + moduleFor( 'Strict Mode - renderComponent (direct)', class extends AbstractStrictTestCase { diff --git a/packages/@ember/debug/lib/capture-render-tree.ts b/packages/@ember/debug/lib/capture-render-tree.ts index 03c5cd4b3fa..efb1caa1979 100644 --- a/packages/@ember/debug/lib/capture-render-tree.ts +++ b/packages/@ember/debug/lib/capture-render-tree.ts @@ -1,4 +1,4 @@ -import type { Renderer } from '@ember/-internals/glimmer/lib/renderer'; +import { renderers } from '@ember/-internals/glimmer/lib/renderers'; import type Owner from '@ember/owner'; import type { CapturedRenderNode } from '@glimmer/interfaces'; @@ -8,6 +8,9 @@ import type { CapturedRenderNode } from '@glimmer/interfaces'; /** Ember Inspector calls this function to capture the current render tree. + The result has the tree of every renderer with live roots: + each application, and each `renderComponent` call, whatever its owner. + In production mode, this requires turning on `ENV._DEBUG_RENDER_TREE` before loading Ember. @@ -15,18 +18,17 @@ import type { CapturedRenderNode } from '@glimmer/interfaces'; @static @method captureRenderTree @for @ember/debug - @param app {ApplicationInstance} An `ApplicationInstance`. + @param app {ApplicationInstance} Unused. Ember Inspector still passes it. @since 3.14.0 */ -export default function captureRenderTree(app: Owner): CapturedRenderNode[] { - let domRenderer = app.lookup('renderer:-dom') as Renderer; +export default function captureRenderTree(_app: Owner): CapturedRenderNode[] { + let nodes: CapturedRenderNode[] = []; - if (!domRenderer) { - throw new Error(`BUG: owner is missing renderer`); + for (let renderer of renderers) { + for (let node of renderer.debugRenderTree.capture()) { + nodes.push(node); + } } - // SAFETY: Ideally we'd assert here but that causes awkward circular requires since this is also in @ember/debug. - // This is only for debug stuff so not very risky. - let renderer = domRenderer; - return renderer.debugRenderTree.capture(); + return nodes; }