Skip to content

Commit 1082312

Browse files
committed
wip: iterating on tests
1 parent 1ced740 commit 1082312

File tree

6 files changed

+474
-74
lines changed

6 files changed

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

90145
/** 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: 142 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,20 @@ 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';
45+
import {allLeavingAnimations} from '../../src/animation/longest_animation';
46+
import {getLViewById} from '../../src/render3/interfaces/lview_tracking';
4347

4448
describe('hot module replacement', () => {
4549
beforeEach(() => {
4650
TestBed.configureTestingModule({
4751
providers: [provideZoneChangeDetection()],
4852
});
53+
54+
for (const child of document.body.childNodes) child.remove();
4955
});
56+
5057
it('should recreate a single usage of a basic component', () => {
5158
let instance!: ChildCmp;
5259
const initialMetadata: Component = {
@@ -285,11 +292,11 @@ describe('hot module replacement', () => {
285292
const getShadowRoot = () => fixture.nativeElement.querySelector('child-cmp').shadowRoot;
286293

287294
markNodesAsCreatedInitially(getShadowRoot());
288-
expectHTML(getShadowRoot(), `<style>strong {color: red;}</style>Hello <strong>0</strong>`);
295+
expectHTML(getShadowRoot(), `Hello <strong>0</strong><style>strong {color: red;}</style>`);
289296

290297
instance.state = 1;
291298
fixture.detectChanges();
292-
expectHTML(getShadowRoot(), `<style>strong {color: red;}</style>Hello <strong>1</strong>`);
299+
expectHTML(getShadowRoot(), `Hello <strong>1</strong><style>strong {color: red;}</style>`);
293300

294301
replaceMetadata(ChildCmp, {
295302
...initialMetadata,
@@ -307,6 +314,110 @@ describe('hot module replacement', () => {
307314
getShadowRoot(),
308315
`<style>strong {background: pink;}</style>Changed <strong>1</strong>!`,
309316
);
317+
318+
fixture.destroy();
319+
assertNoLeakedStyles(TestBed.inject(SharedStylesHost));
320+
});
321+
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+
await waitForAnimations();
329+
if (allLeavingAnimations.size > 0) {
330+
console.log('animations');
331+
for (const id of allLeavingAnimations) {
332+
const lView = getLViewById(id);
333+
console.log(lView);
334+
}
335+
}
336+
337+
const initialMetadata: Component = {
338+
selector: 'child-cmp',
339+
template: 'Hello <strong>World</strong>!',
340+
styles: `strong {color: red;}`,
341+
encapsulation: ViewEncapsulation.None,
342+
};
343+
344+
@Component(initialMetadata)
345+
class ChildCmp {}
346+
347+
@Component({
348+
template: '<child-cmp/>',
349+
encapsulation: ViewEncapsulation.ShadowDom,
350+
imports: [ChildCmp],
351+
})
352+
class RootCmp {}
353+
354+
const fixture = TestBed.createComponent(RootCmp);
355+
fixture.detectChanges();
356+
const getShadowRoot = () => fixture.nativeElement.shadowRoot;
357+
358+
expect(getShadowRoot().innerHTML).toContain(`<style>strong {color: red;}</style>`);
359+
360+
replaceMetadata(ChildCmp, {
361+
...initialMetadata,
362+
styles: `strong {background: pink;}`,
363+
});
364+
fixture.detectChanges();
365+
366+
expect(getShadowRoot().innerHTML).toContain('<style>strong {background: pink;}</style>');
367+
expect(getShadowRoot().innerHTML).not.toContain(`<style>strong {color: red;}</style>`);
368+
369+
fixture.destroy();
370+
assertNoLeakedStyles(TestBed.inject(SharedStylesHost));
371+
});
372+
373+
it('should support components within nested shadow DOM', () => {
374+
// Domino doesn't support shadow DOM.
375+
if (isNode) {
376+
return;
377+
}
378+
379+
@Component({
380+
selector: 'child-cmp',
381+
template: 'Hello <strong>{{state}}</strong>',
382+
styles: `
383+
strong {
384+
color: red;
385+
}
386+
`,
387+
encapsulation: ViewEncapsulation.ShadowDom,
388+
})
389+
class ChildCmp {}
390+
391+
const initialMetadata: Component = {
392+
template: '<child-cmp />',
393+
styles: `:host {color: red;}`,
394+
encapsulation: ViewEncapsulation.ShadowDom,
395+
imports: [ChildCmp],
396+
};
397+
398+
@Component(initialMetadata)
399+
class RootCmp {}
400+
401+
const fixture = TestBed.createComponent(RootCmp);
402+
fixture.detectChanges();
403+
404+
// Can't use `fixture.nativeElement` because the host element is recreated
405+
// during HMR and `fixture` is not updated.
406+
const getShadowRoot = () => document.querySelector('[ng-version]')!.shadowRoot!;
407+
408+
expect(getShadowRoot().innerHTML).toContain(`<style>:host {color: red;}</style>`);
409+
410+
replaceMetadata(RootCmp, {
411+
...initialMetadata,
412+
styles: `:host {background: pink;}`,
413+
});
414+
fixture.detectChanges();
415+
416+
expect(getShadowRoot().innerHTML).toContain('<style>:host {background: pink;}</style>');
417+
expect(getShadowRoot().innerHTML).not.toContain(`<style>:host {color: red;}</style>`);
418+
419+
fixture.destroy();
420+
assertNoLeakedStyles(TestBed.inject(SharedStylesHost));
310421
});
311422

312423
it('should continue binding inputs to a component that is replaced', () => {
@@ -2173,4 +2284,33 @@ describe('hot module replacement', () => {
21732284
}
21742285
}
21752286
}
2287+
2288+
function assertNoLeakedStyles(sharedStylesHost: SharedStylesHost): void {
2289+
const ssh = sharedStylesHost as unknown as Omit<SharedStylesHost, 'inline' | 'external'> & {
2290+
inline: Map<StyleRoot, unknown>;
2291+
external: Map<StyleRoot, unknown>;
2292+
};
2293+
2294+
const totalStyles = ssh.inline.size + ssh.external.size;
2295+
if (totalStyles > 0) {
2296+
throw new Error(
2297+
`Expected \`SharedStylesHost\` to have no leaked styles, found: ${totalStyles}.`,
2298+
);
2299+
}
2300+
}
2301+
async function waitForAnimations() {
2302+
const timer = timeout(() => allLeavingAnimations.size > 0, 1_000);
2303+
while (timer()) {
2304+
await new Promise((resolve) => setTimeout(resolve, 10));
2305+
}
2306+
}
2307+
2308+
function timeout(predicate: () => boolean, timeout: number): () => boolean {
2309+
const start = Date.now();
2310+
return () => {
2311+
if (!predicate()) return false;
2312+
if (Date.now() - start > timeout) return false;
2313+
return true;
2314+
};
2315+
}
21762316
});

0 commit comments

Comments
 (0)