Skip to content
Open
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
3 changes: 1 addition & 2 deletions packages/@ember/-internals/glimmer/lib/base-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -134,8 +135,6 @@ export class ComponentRootState implements RendererRoot {
}
}

const renderers: BaseRenderer[] = [];

export function _resetRenderers() {
renderers.length = 0;
}
Expand Down
12 changes: 12 additions & 0 deletions packages/@ember/-internals/glimmer/lib/renderers.ts
Original file line number Diff line number Diff line change
@@ -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[] = [];
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,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('<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 components rendered with renderComponent and a different owner'() {
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 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 <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,75 @@ 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 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('<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', '{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 {
Expand Down
22 changes: 12 additions & 10 deletions packages/@ember/debug/lib/capture-render-tree.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -8,25 +8,27 @@ 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.

@private
@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;
}
Loading