Skip to content

Commit d7f6f27

Browse files
committed
wip: iterating on tests
1 parent 4aa52b2 commit d7f6f27

File tree

6 files changed

+456
-75
lines changed

6 files changed

+456
-75
lines changed

packages/core/src/render3/node_manipulation.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ export function detachViewFromDOM(tView: TView, lView: LView) {
250250
* - Destroy only called on movement to sibling or movement to parent (laterally or up)
251251
*
252252
* @param rootView The view to destroy
253+
* TODO: Relevant?
253254
*/
254255
export function destroyViewTree(rootView: LView): void {
255256
// If the view has no children, we can clean it up and return early.

packages/core/src/render3/util/view_traversal_utils.ts

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,20 @@ import {assertDefined} from '../../util/assert';
1313
import {assertLView} from '../assert';
1414
import {readPatchedLView} from '../context_discovery';
1515
import {CONTAINER_HEADER_OFFSET, LContainer} from '../interfaces/container';
16-
import {isLContainer, isLView, isRootView} from '../interfaces/type_checks';
17-
import {CHILD_HEAD, CONTEXT, HOST, INJECTOR, LView, NEXT, RENDERER} from '../interfaces/view';
16+
import {TElementNode, TNode} from '../interfaces/node';
17+
import {isComponentHost, isLContainer, isLView, isRootView} from '../interfaces/type_checks';
18+
import {
19+
CHILD_HEAD,
20+
DECLARATION_COMPONENT_VIEW,
21+
CONTEXT,
22+
HOST,
23+
INJECTOR,
24+
LView,
25+
NEXT,
26+
RENDERER,
27+
T_HOST,
28+
PARENT,
29+
} from '../interfaces/view';
1830

1931
import {getLViewParent} from './view_utils';
2032

@@ -69,13 +81,23 @@ function getNearestLContainer(viewOrContainer: LContainer | LView | null) {
6981
return viewOrContainer as LContainer | null;
7082
}
7183

72-
/** Generates all the {@link LView} and {@link LContainer} descendants of the given input. */
84+
/**
85+
* Generates all the {@link LView} and {@link LContainer} descendants of the given input. Also generates {@link LView}
86+
* and {@link LContainer} instances which are projected into a descendant.
87+
*
88+
* There are no strict guarantees on the order of traversal.
89+
* TODO: Duplicating results.
90+
*/
7391
export function* walkDescendants(
7492
parent: LView | LContainer,
7593
): Generator<LView | LContainer, void, void> {
7694
for (const child of walkChildren(parent)) {
7795
yield child;
78-
yield* walkDescendants(child);
96+
// console.log(child); // DEBUG
97+
for (const descendant of walkDescendants(child)) {
98+
yield descendant;
99+
// console.log(descendant); // DEBUG
100+
}
79101
}
80102
}
81103

@@ -85,6 +107,43 @@ function* walkChildren(parent: LView | LContainer): Generator<LView | LContainer
85107
yield child;
86108
child = child[NEXT];
87109
}
110+
111+
if (isLView(parent)) {
112+
const host = parent[T_HOST];
113+
if (host && isComponentHost(host)) {
114+
// `parent[T_HOST]` is the `TElementNode` in the parents's parent view, which
115+
// owns the host element of `parent`. So we need to look up the grandparent
116+
// to access it.
117+
const grandparent = isLContainer(parent[PARENT]) ? parent[PARENT][PARENT]! : parent[PARENT]!;
118+
yield* walkProjectedChildren(grandparent, host as TElementNode);
119+
}
120+
}
121+
}
122+
123+
function* walkProjectedChildren(
124+
lView: LView,
125+
componentHost: TElementNode,
126+
): Generator<LView | LContainer, void, void> {
127+
if (!componentHost.projection) return;
128+
129+
for (const projectedNodes of componentHost.projection) {
130+
if (Array.isArray(projectedNodes)) {
131+
// Projected `RNode` objects are just raw elements and don't contain any `LView` objects.
132+
continue;
133+
}
134+
135+
for (const projectedNode of walkProjectedSiblings(projectedNodes)) {
136+
const projected = lView[projectedNode.index];
137+
if (isLView(projected) || isLContainer(projected)) yield projected;
138+
}
139+
}
140+
}
141+
142+
function* walkProjectedSiblings(node: TNode | null): Generator<TNode, void, void> {
143+
while (node) {
144+
yield node;
145+
node = node.projectionNext;
146+
}
88147
}
89148

90149
/** Combine multiple iterables into a single stream with the same ordering. */

packages/core/test/acceptance/authoring/signal_inputs_spec.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ import {By} from '@angular/platform-browser';
3333
import {tickAnimationFrames} from '../../animation_utils/tick_animation_frames';
3434
import {isNode} from '@angular/private/testing';
3535
import {Subscription} from 'rxjs';
36+
import {walkDescendants} from '../../../src/render3/util/view_traversal_utils';
37+
import {readPatchedLView} from '../../../src/render3/context_discovery';
3638

3739
describe('signal inputs', () => {
3840
beforeEach(() =>
@@ -373,7 +375,8 @@ describe('signal inputs', () => {
373375
expect(childCmp.nativeElement.className).not.toContain('fade-in');
374376
}));
375377

376-
it('should support content projection', fakeAsync(() => {
378+
// TODO: Walk projected content.
379+
fit('should support content projection', fakeAsync(() => {
377380
const animateStyles = `
378381
.fade-in {
379382
animation: fade 1ms forwards;
@@ -453,6 +456,8 @@ describe('signal inputs', () => {
453456
const fixture = TestBed.createComponent(TestComponent);
454457
const button = fixture.nativeElement.querySelector('button');
455458

459+
Array.from(walkDescendants(readPatchedLView(fixture.componentInstance)!));
460+
456461
fixture.detectChanges();
457462
expect(fixture.nativeElement.querySelector('app-content')).toBeNull();
458463
expect(button).not.toBeNull();
@@ -601,6 +606,7 @@ describe('signal inputs', () => {
601606
public ngOnDestroy(): void {
602607
this.closedSubscription?.unsubscribe();
603608
this.closedSubscription = null;
609+
// TODO: Valid?
604610
this.componentRef?.destroy(); // Explicitly destroy the dynamically created component
605611
}
606612

packages/core/test/acceptance/hmr_spec.ts

Lines changed: 118 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
provideZoneChangeDetection,
2424
QueryList,
2525
SimpleChanges,
26+
StyleRoot,
2627
Type,
2728
ViewChild,
2829
ViewChildren,
@@ -39,14 +40,18 @@ import {clearTranslations, loadTranslations} from '@angular/localize';
3940
import {computeMsgId} from '@angular/compiler';
4041
import {EVENT_MANAGER_PLUGINS} from '@angular/platform-browser';
4142
import {ComponentType} from '../../src/render3';
43+
import {ɵSharedStylesHost as SharedStylesHost} from '@angular/platform-browser';
4244
import {isNode} from '@angular/private/testing';
4345

4446
describe('hot module replacement', () => {
4547
beforeEach(() => {
4648
TestBed.configureTestingModule({
4749
providers: [provideZoneChangeDetection()],
4850
});
51+
52+
for (const child of document.body.childNodes) child.remove();
4953
});
54+
5055
it('should recreate a single usage of a basic component', () => {
5156
let instance!: ChildCmp;
5257
const initialMetadata: Component = {
@@ -285,11 +290,11 @@ describe('hot module replacement', () => {
285290
const getShadowRoot = () => fixture.nativeElement.querySelector('child-cmp').shadowRoot;
286291

287292
markNodesAsCreatedInitially(getShadowRoot());
288-
expectHTML(getShadowRoot(), `<style>strong {color: red;}</style>Hello <strong>0</strong>`);
293+
expectHTML(getShadowRoot(), `Hello <strong>0</strong><style>strong {color: red;}</style>`);
289294

290295
instance.state = 1;
291296
fixture.detectChanges();
292-
expectHTML(getShadowRoot(), `<style>strong {color: red;}</style>Hello <strong>1</strong>`);
297+
expectHTML(getShadowRoot(), `Hello <strong>1</strong><style>strong {color: red;}</style>`);
293298

294299
replaceMetadata(ChildCmp, {
295300
...initialMetadata,
@@ -307,6 +312,103 @@ describe('hot module replacement', () => {
307312
getShadowRoot(),
308313
`<style>strong {background: pink;}</style>Changed <strong>1</strong>!`,
309314
);
315+
316+
fixture.destroy();
317+
assertNoLeakedStyles(TestBed.inject(SharedStylesHost));
318+
});
319+
320+
// TODO: `NoneEncapsulationDomRenderer.prototype.removeStyles` skips when animations are running, breaks in full test suite in headless mode.
321+
// Don't *think* it's related to rAF throttling.
322+
it("should replace a component child's styles within shadow DOM encapsulation", async () => {
323+
// Domino doesn't support shadow DOM.
324+
if (isNode) {
325+
return;
326+
}
327+
328+
const initialMetadata: Component = {
329+
selector: 'child-cmp',
330+
template: 'Hello <strong>World</strong>!',
331+
styles: `strong {color: red;}`,
332+
encapsulation: ViewEncapsulation.None,
333+
};
334+
335+
@Component(initialMetadata)
336+
class ChildCmp {}
337+
338+
@Component({
339+
template: '<child-cmp/>',
340+
encapsulation: ViewEncapsulation.ShadowDom,
341+
imports: [ChildCmp],
342+
})
343+
class RootCmp {}
344+
345+
const fixture = TestBed.createComponent(RootCmp);
346+
fixture.detectChanges();
347+
const getShadowRoot = () => fixture.nativeElement.shadowRoot;
348+
349+
expect(getShadowRoot().innerHTML).toContain(`<style>strong {color: red;}</style>`);
350+
351+
replaceMetadata(ChildCmp, {
352+
...initialMetadata,
353+
styles: `strong {background: pink;}`,
354+
});
355+
fixture.detectChanges();
356+
357+
expect(getShadowRoot().innerHTML).toContain('<style>strong {background: pink;}</style>');
358+
expect(getShadowRoot().innerHTML).not.toContain(`<style>strong {color: red;}</style>`);
359+
360+
fixture.destroy();
361+
assertNoLeakedStyles(TestBed.inject(SharedStylesHost));
362+
});
363+
364+
it('should support components within nested shadow DOM', () => {
365+
// Domino doesn't support shadow DOM.
366+
if (isNode) {
367+
return;
368+
}
369+
370+
@Component({
371+
selector: 'child-cmp',
372+
template: 'Hello <strong>{{state}}</strong>',
373+
styles: `
374+
strong {
375+
color: red;
376+
}
377+
`,
378+
encapsulation: ViewEncapsulation.ShadowDom,
379+
})
380+
class ChildCmp {}
381+
382+
const initialMetadata: Component = {
383+
template: '<child-cmp />',
384+
styles: `:host {color: red;}`,
385+
encapsulation: ViewEncapsulation.ShadowDom,
386+
imports: [ChildCmp],
387+
};
388+
389+
@Component(initialMetadata)
390+
class RootCmp {}
391+
392+
const fixture = TestBed.createComponent(RootCmp);
393+
fixture.detectChanges();
394+
395+
// Can't use `fixture.nativeElement` because the host element is recreated
396+
// during HMR and `fixture` is not updated.
397+
const getShadowRoot = () => document.querySelector('[ng-version]')!.shadowRoot!;
398+
399+
expect(getShadowRoot().innerHTML).toContain(`<style>:host {color: red;}</style>`);
400+
401+
replaceMetadata(RootCmp, {
402+
...initialMetadata,
403+
styles: `:host {background: pink;}`,
404+
});
405+
fixture.detectChanges();
406+
407+
expect(getShadowRoot().innerHTML).toContain('<style>:host {background: pink;}</style>');
408+
expect(getShadowRoot().innerHTML).not.toContain(`<style>:host {color: red;}</style>`);
409+
410+
fixture.destroy();
411+
assertNoLeakedStyles(TestBed.inject(SharedStylesHost));
310412
});
311413

312414
it('should continue binding inputs to a component that is replaced', () => {
@@ -2173,4 +2275,18 @@ describe('hot module replacement', () => {
21732275
}
21742276
}
21752277
}
2278+
2279+
function assertNoLeakedStyles(sharedStylesHost: SharedStylesHost): void {
2280+
const ssh = sharedStylesHost as unknown as Omit<SharedStylesHost, 'inline' | 'external'> & {
2281+
inline: Map<StyleRoot, unknown>;
2282+
external: Map<StyleRoot, unknown>;
2283+
};
2284+
2285+
const totalStyles = ssh.inline.size + ssh.external.size;
2286+
if (totalStyles > 0) {
2287+
throw new Error(
2288+
`Expected \`SharedStylesHost\` to have no leaked styles, found: ${totalStyles}.`,
2289+
);
2290+
}
2291+
}
21762292
});

0 commit comments

Comments
 (0)