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;
}