From 4dffd7cb0d606177a9847b3a37c23a64f94ad010 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Fri, 25 Sep 2026 10:37:22 -0400 Subject: [PATCH 1/3] [BUGFIX lts] Add failing tests for captureRenderTree with renderComponent Components rendered with `renderComponent` are missing from `captureRenderTree(owner)` when the owner's `renderer:-dom` is not the renderer that `renderComponent` uses. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../application/debug-render-tree-test.ts | 53 ++++++++++++++++++- .../components/render-component-test.ts | 26 +++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) 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..69d2890af10 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,53 @@ 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'() { 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..86cf5687fda 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,6 +122,29 @@ 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; + + let names: string[] = []; + let collect = (nodes: CapturedRenderNode[]) => { + for (let node of nodes) { + names.push(node.name); + collect(node.children); + } + }; + collect(captureRenderTree(this.owner)); + + assert.deepEqual(names, ['{ROOT}', 'HelloWorld']); + } } ); From 1f6808bb07809aa1535895ff784e8605d8b04a97 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Fri, 25 Sep 2026 10:37:22 -0400 Subject: [PATCH 2/3] [BUGFIX lts] Include renderComponent roots in captureRenderTree `renderComponent` renders with its own renderer for each owner. `captureRenderTree` only read the owner's `renderer:-dom`, so those roots never showed up. On `main`, an application shares its renderer with `renderComponent` once `renderRootComponent` runs, so apps are fine there. Other owners, and every owner before 7.5, are not. `@ember/test-helpers` 5.5.0 renders through `renderComponent` on Ember 6.8 and newer, so `captureRenderTree` in a rendering test returns nothing on those versions. This broke the nightly smoke tests on `beta` and `release`. The renderer now registers itself with `@ember/debug`, and `captureRenderTree` includes its roots. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../-internals/glimmer/lib/base-renderer.ts | 2 ++ .../@ember/debug/lib/capture-render-tree.ts | 30 ++++++++++++++++--- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/packages/@ember/-internals/glimmer/lib/base-renderer.ts b/packages/@ember/-internals/glimmer/lib/base-renderer.ts index d604056fb05..ae939931ef6 100644 --- a/packages/@ember/-internals/glimmer/lib/base-renderer.ts +++ b/packages/@ember/-internals/glimmer/lib/base-renderer.ts @@ -1,6 +1,7 @@ import { ENV } from '@ember/-internals/environment/lib/env'; import type { InternalOwner } from '@ember/-internals/owner'; import { assert } from '@ember/debug'; +import { registerComponentRenderer } from '@ember/debug/lib/capture-render-tree'; import { _backburner, _getCurrentRunLoop } from '@ember/runloop'; import { associateDestroyableChild, @@ -525,6 +526,7 @@ export function renderComponent( hasDOM: env && 'hasDOM' in env ? Boolean(env?.['hasDOM']) : true, }); RENDERER_CACHE.set(owner, renderer); + registerComponentRenderer(owner, renderer); } /** diff --git a/packages/@ember/debug/lib/capture-render-tree.ts b/packages/@ember/debug/lib/capture-render-tree.ts index 03c5cd4b3fa..13285172bc3 100644 --- a/packages/@ember/debug/lib/capture-render-tree.ts +++ b/packages/@ember/debug/lib/capture-render-tree.ts @@ -1,7 +1,21 @@ import type { Renderer } from '@ember/-internals/glimmer/lib/renderer'; +import type { BaseRenderer } from '@ember/-internals/glimmer/lib/base-renderer'; import type Owner from '@ember/owner'; import type { CapturedRenderNode } from '@glimmer/interfaces'; +/** + * `renderComponent` renders with its own renderer per owner, + * separate from the owner's `renderer:-dom`. + * + * The renderer registers itself here, + * because importing it would be a circular import. + */ +const COMPONENT_RENDERERS = new WeakMap(); + +export function registerComponentRenderer(owner: object, renderer: BaseRenderer): void { + COMPONENT_RENDERERS.set(owner, renderer); +} + /** @module @ember/debug */ @@ -19,14 +33,22 @@ import type { CapturedRenderNode } from '@glimmer/interfaces'; @since 3.14.0 */ export default function captureRenderTree(app: Owner): CapturedRenderNode[] { - let domRenderer = app.lookup('renderer:-dom') as Renderer; + let domRenderer = app.lookup('renderer:-dom') as Renderer | undefined; + let componentRenderer = COMPONENT_RENDERERS.get(app); - if (!domRenderer) { + if (!domRenderer && !componentRenderer) { throw new Error(`BUG: owner is missing renderer`); } // 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(); + let nodes: CapturedRenderNode[] = domRenderer ? domRenderer.debugRenderTree.capture() : []; + + if (componentRenderer) { + for (let node of componentRenderer.debugRenderTree.capture()) { + nodes.push(node); + } + } + + return nodes; } From d0d855d7bf02954783fd4eea05070bb7c4df23dc Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Fri, 25 Sep 2026 10:42:21 -0400 Subject: [PATCH 3/3] Test captureRenderTree with an ownerless renderComponent Without an `owner`, `renderComponent` renders with a new owner that no caller can reach. `captureRenderTree` for another owner leaves those components out. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../components/render-component-test.ts | 58 ++++++++++++++++--- 1 file changed, 49 insertions(+), 9 deletions(-) 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 86cf5687fda..243b7c78449 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 @@ -134,20 +134,60 @@ moduleFor( if (!ENV._DEBUG_RENDER_TREE) return; - let names: string[] = []; - let collect = (nodes: CapturedRenderNode[]) => { - for (let node of nodes) { - names.push(node.name); - collect(node.children); - } - }; - collect(captureRenderTree(this.owner)); + assert.deepEqual(renderTreeNames(this.owner), ['{ROOT}', 'HelloWorld']); + } + + '@test captureRenderTree leaves out components rendered without an 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 }), + ]); - assert.deepEqual(names, ['{ROOT}', 'HelloWorld']); + assertHTML('
owned
ownerless
'); + + if (ENV._DEBUG_RENDER_TREE) { + assert.deepEqual(renderTreeNames(this.owner), ['{ROOT}', 'Owned']); + } + + run(() => { + for (let result of results) { + result.destroy(); + } + 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 {