Skip to content

Commit d9cf236

Browse files
committed
add modifiers to debug render tree
1 parent dc76897 commit d9cf236

File tree

4 files changed

+173
-4
lines changed

4 files changed

+173
-4
lines changed

packages/@glimmer-workspace/integration-tests/lib/setup-harness.ts

+13
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,19 @@ export async function setupQunit() {
6363
qunit.moduleDone(pause);
6464
}
6565

66+
// @ts-expect-error missing in types, does exist: https://api.qunitjs.com/callbacks/QUnit.on/#the-testend-event
67+
QUnit.on('testEnd', (testEnd) => {
68+
if (testEnd.status === 'failed') {
69+
testEnd.errors.forEach((assertion: any) => {
70+
console.error(assertion.stack);
71+
// message: speedometer
72+
// actual: 75
73+
// expected: 88
74+
// stack: at dmc.test.js:12
75+
});
76+
}
77+
});
78+
6679
qunit.done(({ failed }) => {
6780
if (failed > 0) {
6881
console.log('[HARNESS] fail');

packages/@glimmer-workspace/integration-tests/test/debug-render-tree-test.ts

+90-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import type {
99
SimpleNode,
1010
} from '@glimmer/interfaces';
1111
import type { TemplateOnlyComponent } from '@glimmer/runtime';
12-
import { setComponentTemplate } from '@glimmer/manager';
12+
import { modifierCapabilities, setComponentTemplate, setModifierManager } from '@glimmer/manager';
1313
import { EMPTY_ARGS, templateOnlyComponent, TemplateOnlyComponentManager } from '@glimmer/runtime';
1414
import { assign, expect } from '@glimmer/util';
1515

@@ -66,9 +66,26 @@ class DebugRenderTreeDelegate extends JitRenderDelegate {
6666

6767
this.registry.register('component', name, definition);
6868
}
69+
70+
registerCustomModifier(name: string) {
71+
const r = setModifierManager(
72+
() => ({
73+
capabilities: modifierCapabilities('3.22'),
74+
createModifier() {},
75+
installModifier() {},
76+
updateModifier() {},
77+
destroyModifier() {},
78+
}),
79+
class DidInsertModifier {}
80+
);
81+
this.registry.register('modifier', name, r);
82+
}
6983
}
7084

7185
class DebugRenderTreeTest extends RenderTest {
86+
defineModifier(name: string) {
87+
this.delegate.registerCustomModifier(name);
88+
}
7289
static suiteName = 'Application test: debug render tree';
7390

7491
declare delegate: DebugRenderTreeDelegate;
@@ -253,6 +270,77 @@ class DebugRenderTreeTest extends RenderTest {
253270
]);
254271
}
255272

273+
@test modifiers() {
274+
this.registerComponent('Glimmer', 'HelloWorld', 'Hello World');
275+
const didInsert = () => null;
276+
this.registerModifier(
277+
'did-insert',
278+
class {
279+
element?: SimpleElement;
280+
didInsertElement() {}
281+
didUpdate() {}
282+
willDestroyElement() {}
283+
}
284+
);
285+
this.defineModifier('did-update');
286+
287+
this.render(
288+
`<div {{on 'click' this.didInsert}} {{did-insert this.didInsert}} {{did-update this.didInsert}}><HelloWorld /></div>`,
289+
{
290+
didInsert: didInsert,
291+
}
292+
);
293+
294+
this.assertRenderTree([
295+
{
296+
type: 'html-element',
297+
name: 'div',
298+
args: { positional: [], named: {} },
299+
instance: (instance: HTMLDivElement) => instance.tagName === 'DIV',
300+
template: null,
301+
bounds: this.nodeBounds(this.element.firstChild),
302+
children: [
303+
{
304+
type: 'modifier',
305+
name: 'did-insert',
306+
args: { positional: [didInsert], named: {} },
307+
instance: (instance: any) => typeof instance.didInsertElement === 'function',
308+
template: null,
309+
bounds: this.nodeBounds(this.element.firstChild),
310+
children: [],
311+
},
312+
{
313+
type: 'modifier',
314+
name: 'did-update',
315+
args: { positional: [didInsert], named: {} },
316+
instance: (instance: any) => typeof instance.installModifier === 'function',
317+
template: null,
318+
bounds: this.nodeBounds(this.element.firstChild),
319+
children: [],
320+
},
321+
{
322+
type: 'modifier',
323+
name: 'on',
324+
args: { positional: ['click', didInsert], named: {} },
325+
instance: (instance: any) => instance === undefined,
326+
template: null,
327+
bounds: this.nodeBounds(this.element.firstChild),
328+
children: [],
329+
},
330+
{
331+
type: 'component',
332+
name: 'HelloWorld',
333+
args: { positional: [], named: {} },
334+
instance: (instance: any) => instance !== undefined,
335+
template: '(unknown template module)',
336+
bounds: this.nodeBounds(this.element.firstChild!.firstChild),
337+
children: [],
338+
},
339+
],
340+
},
341+
]);
342+
}
343+
256344
@test 'getDebugCustomRenderTree works'() {
257345
let bucket1 = {};
258346
let instance1 = {};
@@ -502,7 +590,7 @@ class DebugRenderTreeTest extends RenderTest {
502590
this.assertRenderNode(actualNode, expected, `${actualNode.type}:${actualNode.name}`);
503591
});
504592
} else {
505-
this.assert.deepEqual(actual, [], path);
593+
this.assert.deepEqual(actual, expectedNodes, path);
506594
}
507595
}
508596

packages/@glimmer/interfaces/lib/runtime/debug-render-tree.d.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@ import type { SimpleElement, SimpleNode } from '@simple-dom/interface';
33
import type { Bounds } from '../dom/bounds.js';
44
import type { Arguments, CapturedArguments } from './arguments.js';
55

6-
export type RenderNodeType = 'outlet' | 'engine' | 'route-template' | 'component';
6+
export type RenderNodeType =
7+
| 'outlet'
8+
| 'engine'
9+
| 'route-template'
10+
| 'component'
11+
| 'html-element'
12+
| 'modifier';
713

814
export interface RenderNode {
915
type: RenderNodeType;

packages/@glimmer/runtime/lib/vm/element-builder.ts

+63-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import type {
2020
UpdatableBlock,
2121
} from '@glimmer/interfaces';
2222
import { destroy, registerDestructor } from '@glimmer/destroyable';
23+
import { createUnboundRef, REFERENCE } from '@glimmer/reference';
2324
import { assert, expect, Stack } from '@glimmer/util';
2425

2526
import type { DynamicAttribute } from './attributes/dynamic';
@@ -83,6 +84,7 @@ export class NewElementBuilder implements ElementBuilder {
8384
[CURSOR_STACK] = new Stack<Cursor>();
8485
private modifierStack = new Stack<Nullable<ModifierInstance[]>>();
8586
private blockStack = new Stack<LiveBlock>();
87+
private htmlElementsState: { shouldAddHtmlElement?: boolean }[];
8688

8789
static forInitialRender(env: Environment, cursor: CursorImpl) {
8890
return new this(env, cursor.element, cursor.nextSibling).initialize();
@@ -104,6 +106,7 @@ export class NewElementBuilder implements ElementBuilder {
104106
this.env = env;
105107
this.dom = env.getAppendOperations();
106108
this.updateOperations = env.getDOM();
109+
this.htmlElementsState = [];
107110
}
108111

109112
protected initialize(): this {
@@ -195,8 +198,8 @@ export class NewElementBuilder implements ElementBuilder {
195198
this.constructing = null;
196199
this.operations = null;
197200

198-
this.pushModifiers(modifiers);
199201
this.pushElement(element, null);
202+
this.pushModifiers(modifiers);
200203
this.didOpenElement(element);
201204
}
202205

@@ -205,6 +208,15 @@ export class NewElementBuilder implements ElementBuilder {
205208
}
206209

207210
closeElement(): Nullable<ModifierInstance[]> {
211+
const htmlState = this.htmlElementsState.pop();
212+
if (htmlState?.shouldAddHtmlElement) {
213+
const element = this.element;
214+
this.env.debugRenderTree?.didRender(htmlState, {
215+
parentElement: () => (element as any).parentElement,
216+
firstNode: () => element,
217+
lastNode: () => element,
218+
});
219+
}
208220
this.willCloseElement();
209221
this.popElement();
210222
return this.popModifiers();
@@ -247,6 +259,56 @@ export class NewElementBuilder implements ElementBuilder {
247259

248260
private pushModifiers(modifiers: Nullable<ModifierInstance[]>): void {
249261
this.modifierStack.push(modifiers);
262+
if (this.env.debugRenderTree) {
263+
modifiers = modifiers || [];
264+
const htmlState = {
265+
shouldAddHtmlElement:
266+
modifiers.length || (globalThis as any).ENV_DEBUG_RENDER_TREE_ALL_ELEMENTS,
267+
};
268+
this.htmlElementsState.push(htmlState);
269+
if (htmlState.shouldAddHtmlElement) {
270+
this.env.debugRenderTree?.create(htmlState, {
271+
type: 'html-element',
272+
name: this.element.tagName.toLowerCase(),
273+
args: {
274+
named: {},
275+
positional: [],
276+
} as any,
277+
instance: this.element,
278+
});
279+
}
280+
for (const modifier of modifiers) {
281+
const state = {};
282+
const name = modifier.definition.resolvedName || 'unknown-modifier';
283+
const instance = (modifier.state as any).instance || (modifier.state as any).delegate;
284+
const element = this.element;
285+
const args: any = {
286+
positional: [],
287+
named: {},
288+
};
289+
for (const value of (modifier.state as any).args.positional) {
290+
if (value && value[REFERENCE]) {
291+
args.positional.push(value);
292+
} else {
293+
args.positional.push(createUnboundRef(value, false));
294+
}
295+
}
296+
for (const [key, value] of Object.entries((modifier.state as any)?.args.named)) {
297+
args.named[key] = createUnboundRef(value, false);
298+
}
299+
this.env.debugRenderTree?.create(state, {
300+
type: 'modifier',
301+
name,
302+
args: args,
303+
instance,
304+
});
305+
this.env.debugRenderTree?.didRender(state, {
306+
parentElement: () => (element as any).parentElement,
307+
firstNode: () => element,
308+
lastNode: () => element,
309+
});
310+
}
311+
}
250312
}
251313

252314
private popModifiers(): Nullable<ModifierInstance[]> {

0 commit comments

Comments
 (0)