Skip to content

Commit 4fa8053

Browse files
committed
build: do not leak providers and component styles between tests
Angular's `TestBed` by default only removes a test component from the document body and destructs individual component fixtures. It never destroys the test module properly, but always re-creates it. This means that providers are not necessarily cleaned up (could leak in memory) and component styles are not cleaned up / deduped. This results in thousands of style elements in the browsers. Ultimately causing significantly increased memory consumption and CPU blocking. This potentially leads to repeated crashes within browsers (as seen in BrowserStack and Saucelabs in the past). Initial testing of this change unveiled a reduction from 30min tests to 5min in BrowserStack. This is a signficant improvement and we should consider moving this change upstream with: angular/angular#31834. Benefits are: reduced test time; increased test/browser stability; isolated tests without leaking styles and services.
1 parent 7430760 commit 4fa8053

File tree

4 files changed

+71
-21
lines changed

4 files changed

+71
-21
lines changed

src/cdk/drag-drop/directives/drag.spec.ts

+39-17
Original file line numberDiff line numberDiff line change
@@ -2169,7 +2169,7 @@ describe('CdkDrag', () => {
21692169
fixture.componentInstance.boundarySelector = '.cdk-drop-list';
21702170
fixture.detectChanges();
21712171

2172-
const container: HTMLElement = fixture.nativeElement.querySelector('.container');
2172+
const container: HTMLElement = fixture.nativeElement.querySelector('.scroll-container');
21732173
const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement;
21742174
const list = fixture.componentInstance.dropInstance.element.nativeElement;
21752175
const cleanup = makeScrollable('vertical', container);
@@ -3889,7 +3889,7 @@ describe('CdkDrag', () => {
38893889
const fixture = createComponent(DraggableInScrollableParentContainer);
38903890
fixture.detectChanges();
38913891
const item = fixture.componentInstance.dragItems.first.element.nativeElement;
3892-
const container = fixture.nativeElement.querySelector('.container');
3892+
const container = fixture.nativeElement.querySelector('.scroll-container');
38933893
const containerRect = container.getBoundingClientRect();
38943894

38953895
expect(container.scrollTop).toBe(0);
@@ -5519,7 +5519,7 @@ class StandaloneDraggableWithMultipleHandles {
55195519
const DROP_ZONE_FIXTURE_TEMPLATE = `
55205520
<div
55215521
cdkDropList
5522-
class="drop-list"
5522+
class="drop-list scroll-container"
55235523
style="width: 100px; background: pink;"
55245524
[id]="dropZoneId"
55255525
[cdkDropListData]="items"
@@ -5539,7 +5539,7 @@ const DROP_ZONE_FIXTURE_TEMPLATE = `
55395539
`;
55405540

55415541
@Component({template: DROP_ZONE_FIXTURE_TEMPLATE})
5542-
class DraggableInDropZone {
5542+
class DraggableInDropZone implements AfterViewInit {
55435543
@ViewChildren(CdkDrag) dragItems: QueryList<CdkDrag>;
55445544
@ViewChild(CdkDropList) dropInstance: CdkDropList;
55455545
items = [
@@ -5556,6 +5556,15 @@ class DraggableInDropZone {
55565556
moveItemInArray(this.items, event.previousIndex, event.currentIndex);
55575557
});
55585558
startedSpy = jasmine.createSpy('started spy');
5559+
5560+
constructor(protected _elementRef: ElementRef) {}
5561+
5562+
ngAfterViewInit() {
5563+
// Firefox preserves the `scrollTop` value from previous similar containers. This
5564+
// could throw off test assertions and result in flaky results.
5565+
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=959812.
5566+
this._elementRef.nativeElement.querySelector('.scroll-container').scrollTop = 0;
5567+
}
55595568
}
55605569

55615570
@Component({
@@ -5579,8 +5588,8 @@ class DraggableInOnPushDropZone extends DraggableInDropZone {}
55795588
`]
55805589
})
55815590
class DraggableInScrollableVerticalDropZone extends DraggableInDropZone {
5582-
constructor() {
5583-
super();
5591+
constructor(elementRef: ElementRef) {
5592+
super(elementRef);
55845593

55855594
for (let i = 0; i < 60; i++) {
55865595
this.items.push({value: `Extra item ${i}`, height: ITEM_HEIGHT, margin: 0});
@@ -5589,21 +5598,21 @@ class DraggableInScrollableVerticalDropZone extends DraggableInDropZone {
55895598
}
55905599

55915600
@Component({
5592-
template: '<div class="container" cdkScrollable>' + DROP_ZONE_FIXTURE_TEMPLATE + '</div>',
5601+
template: '<div class="scroll-container" cdkScrollable>' + DROP_ZONE_FIXTURE_TEMPLATE + '</div>',
55935602

55945603
// Note that it needs a margin to ensure that it's not flush against the viewport
55955604
// edge which will cause the viewport to scroll, rather than the list.
55965605
styles: [`
5597-
.container {
5606+
.scroll-container {
55985607
max-height: 200px;
55995608
overflow: auto;
56005609
margin: 10vw 0 0 10vw;
56015610
}
56025611
`]
56035612
})
56045613
class DraggableInScrollableParentContainer extends DraggableInDropZone {
5605-
constructor() {
5606-
super();
5614+
constructor(elementRef: ElementRef) {
5615+
super(elementRef);
56075616

56085617
for (let i = 0; i < 60; i++) {
56095618
this.items.push({value: `Extra item ${i}`, height: ITEM_HEIGHT, margin: 0});
@@ -5617,7 +5626,7 @@ class DraggableInScrollableParentContainer extends DraggableInDropZone {
56175626
template: `
56185627
<div
56195628
cdkDropList
5620-
class="drop-list"
5629+
class="drop-list scroll-container"
56215630
style="width: 100px; background: pink;"
56225631
[id]="dropZoneId"
56235632
[cdkDropListData]="items"
@@ -5656,7 +5665,7 @@ const HORIZONTAL_FIXTURE_STYLES = `
56565665

56575666
const HORIZONTAL_FIXTURE_TEMPLATE = `
56585667
<div
5659-
class="drop-list"
5668+
class="drop-list scroll-container"
56605669
cdkDropList
56615670
cdkDropListOrientation="horizontal"
56625671
[cdkDropListData]="items"
@@ -5675,7 +5684,7 @@ const HORIZONTAL_FIXTURE_TEMPLATE = `
56755684
styles: [HORIZONTAL_FIXTURE_STYLES],
56765685
template: HORIZONTAL_FIXTURE_TEMPLATE
56775686
})
5678-
class DraggableInHorizontalDropZone {
5687+
class DraggableInHorizontalDropZone implements AfterViewInit {
56795688
@ViewChildren(CdkDrag) dragItems: QueryList<CdkDrag>;
56805689
@ViewChild(CdkDropList) dropInstance: CdkDropList;
56815690
items = [
@@ -5688,6 +5697,15 @@ class DraggableInHorizontalDropZone {
56885697
droppedSpy = jasmine.createSpy('dropped spy').and.callFake((event: CdkDragDrop<string[]>) => {
56895698
moveItemInArray(this.items, event.previousIndex, event.currentIndex);
56905699
});
5700+
5701+
constructor(protected _elementRef: ElementRef) {}
5702+
5703+
ngAfterViewInit() {
5704+
// Firefox preserves the `scrollLeft` value from previous similar containers. This
5705+
// could throw off test assertions and result in flaky results.
5706+
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=959812.
5707+
this._elementRef.nativeElement.querySelector('.scroll-container').scrollLeft = 0;
5708+
}
56915709
}
56925710

56935711

@@ -5706,8 +5724,8 @@ class DraggableInHorizontalDropZone {
57065724
`]
57075725
})
57085726
class DraggableInScrollableHorizontalDropZone extends DraggableInHorizontalDropZone {
5709-
constructor() {
5710-
super();
5727+
constructor(elementRef: ElementRef) {
5728+
super(elementRef);
57115729

57125730
for (let i = 0; i < 60; i++) {
57135731
this.items.push({value: `Extra item ${i}`, width: ITEM_WIDTH, margin: 0});
@@ -6142,6 +6160,7 @@ class ConnectedWrappedDropZones {
61426160
@Component({
61436161
template: `
61446162
<div
6163+
class="drop-list scroll-container"
61456164
cdkDropList
61466165
style="width: 100px; background: pink;"
61476166
[id]="dropZoneId"
@@ -6162,11 +6181,12 @@ class ConnectedWrappedDropZones {
61626181
`
61636182
})
61646183
class DraggableWithCanvasInDropZone extends DraggableInDropZone implements AfterViewInit {
6165-
constructor(private _elementRef: ElementRef<HTMLElement>) {
6166-
super();
6184+
constructor(elementRef: ElementRef<HTMLElement>) {
6185+
super(elementRef);
61676186
}
61686187

61696188
ngAfterViewInit() {
6189+
super.ngAfterViewInit();
61706190
const canvases = this._elementRef.nativeElement.querySelectorAll('canvas');
61716191

61726192
// Add a circle to all the canvases.
@@ -6183,6 +6203,7 @@ class DraggableWithCanvasInDropZone extends DraggableInDropZone implements After
61836203
@Component({
61846204
template: `
61856205
<div
6206+
class="drop-list scroll-container"
61866207
cdkDropList
61876208
style="width: 100px; background: pink;"
61886209
[id]="dropZoneId"
@@ -6413,6 +6434,7 @@ class DraggableWithAlternateRootAndSelfHandle {
64136434
template: `
64146435
<div
64156436
cdkDropList
6437+
class="drop-list scroll-container"
64166438
style="width: 100px; background: pink;"
64176439
[id]="dropZoneId"
64186440
[cdkDropListData]="items"

src/cdk/scrolling/scrollable.spec.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,16 @@ describe('CdkScrollable', () => {
3131

3232
beforeEach(() => {
3333
fixture = TestBed.createComponent(ScrollableViewport);
34+
fixture.detectChanges();
3435
testComponent = fixture.componentInstance;
36+
// Firefox preserves the `scrollTop` value from previous similar containers. This
37+
// could throw off test assertions and result in flaky results.
38+
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=959812.
39+
testComponent.scrollContainer.nativeElement.scrollTop = 0;
3540
});
3641

3742
describe('in LTR context', () => {
3843
beforeEach(() => {
39-
fixture.detectChanges();
4044
maxOffset = testComponent.scrollContainer.nativeElement.scrollHeight -
4145
testComponent.scrollContainer.nativeElement.clientHeight;
4246
});

test/angular-test-init-spec.ts

+19-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import {NgModuleRef} from '@angular/core';
12
import {ComponentFixture, TestBed} from '@angular/core/testing';
23
import {
34
BrowserDynamicTestingModule,
@@ -8,8 +9,8 @@ import {
89
* Common setup / initialization for all unit tests in Angular Material and CDK.
910
*/
1011

11-
const testBed = TestBed.initTestEnvironment(
12-
[BrowserDynamicTestingModule], platformBrowserDynamicTesting());
12+
const testBed =
13+
TestBed.initTestEnvironment([BrowserDynamicTestingModule], platformBrowserDynamicTesting());
1314
patchTestBedToDestroyFixturesAfterEveryTest(testBed);
1415

1516
(window as any).module = {};
@@ -38,9 +39,24 @@ function patchTestBedToDestroyFixturesAfterEveryTest(testBedInstance: TestBed) {
3839
// Monkey-patch the resetTestingModule to destroy fixtures outside of a try/catch block.
3940
// With https://github.com/angular/angular/commit/2c5a67134198a090a24f6671dcdb7b102fea6eba
4041
// errors when destroying components are no longer causing Jasmine to fail.
41-
testBedInstance.resetTestingModule = function(this: {_activeFixtures: ComponentFixture<any>[]}) {
42+
testBedInstance.resetTestingModule = function(this: {
43+
/** List of active fixtures in the current testing module. */
44+
_activeFixtures: ComponentFixture<any>[],
45+
/** Module Ref used in the Ivy TestBed for creating components. */
46+
_testModuleRef?: NgModuleRef<unknown>|null,
47+
/** Module Ref used in the View Engine TestBed for creating components. */
48+
_moduleRef?: NgModuleRef<unknown>|null
49+
}) {
4250
try {
51+
const moduleRef = this._testModuleRef || this._moduleRef;
4352
this._activeFixtures.forEach((fixture: ComponentFixture<any>) => fixture.destroy());
53+
// Destroy the TestBed `NgModule` reference to clear out shared styles that would
54+
// otherwise remain in DOM and significantly increase memory consumption in browsers.
55+
// This increased consumption then results in noticeable test instability and slow-down.
56+
// See: https://github.com/angular/angular/issues/31834.
57+
if (moduleRef != null) {
58+
moduleRef.destroy();
59+
}
4460
} finally {
4561
this._activeFixtures = [];
4662
// Regardless of errors or not, run the original reset testing module function.

test/karma-test-shim.js

+8
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,15 @@ function patchTestBedToDestroyFixturesAfterEveryTest(testBed) {
7979
// errors when destroying components are no longer causing Jasmine to fail.
8080
testBed.resetTestingModule = function() {
8181
try {
82+
const moduleRef = this._testModuleRef || this._moduleRef;
8283
this._activeFixtures.forEach(function (fixture) { fixture.destroy(); });
84+
// Destroy the TestBed `NgModule` reference to clear out shared styles that would
85+
// otherwise remain in DOM and significantly increase memory consumption in browsers.
86+
// This increased consumption then results in noticeable test instability and slow-down.
87+
// See: https://github.com/angular/angular/issues/31834.
88+
if (moduleRef != null) {
89+
moduleRef.destroy();
90+
}
8391
} finally {
8492
this._activeFixtures = [];
8593
// Regardless of errors or not, run the original reset testing module function.

0 commit comments

Comments
 (0)