Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/@ember/-internals/glimmer/lib/base-renderer.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -525,6 +526,7 @@ export function renderComponent(
hasDOM: env && 'hasDOM' in env ? Boolean(env?.['hasDOM']) : true,
});
RENDERER_CACHE.set(owner, renderer);
registerComponentRenderer(owner, renderer);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -1128,6 +1132,53 @@ if (ENV._DEBUG_RENDER_TREE) {
]);
}

async '@test components rendered with renderComponent'() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good test

await this.visit('/');

let HelloWorld = setComponentTemplate(
precompileTemplate('Hello {{@name}}', { strictMode: true }),
templateOnly()
);
let Root = setComponentTemplate(
precompileTemplate('<HelloWorld @name="world" />', {
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 <Input> components'() {
this.add(
'template:application',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -119,9 +122,72 @@ moduleFor(

assertHTML('');
}

'@test captureRenderTree includes the rendered components'(assert: QUnit['assert']) {
let HelloWorld = setComponentTemplate(precompileTemplate('Hello, world!'), templateOnly());
let Root = setComponentTemplate(
precompileTemplate('<HelloWorld/>', { 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 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('<Owned/>', { strictMode: true, scope: () => ({ Owned }) }),
templateOnly()
);
let OwnerlessRoot = setComponentTemplate(
precompileTemplate('<Ownerless/>', { 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('<div>owned</div><div>ownerless</div>');

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 {
Expand Down
30 changes: 26 additions & 4 deletions packages/@ember/debug/lib/capture-render-tree.ts
Original file line number Diff line number Diff line change
@@ -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<object, BaseRenderer>();

export function registerComponentRenderer(owner: object, renderer: BaseRenderer): void {
COMPONENT_RENDERERS.set(owner, renderer);
}

/**
@module @ember/debug
*/
Expand All @@ -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;
}
Loading