Skip to content

Commit d25593f

Browse files
committed
test: Extend zoneless lint to cover tests (#29193)
Only allow `provideZoneChangeDetection` and `NgZone` in tests that explicitly test integration with Zone.js (and tests which have not yet been migrated to zoneless). (cherry picked from commit df8adda)
1 parent 65648a4 commit d25593f

File tree

9 files changed

+147
-66
lines changed

9 files changed

+147
-66
lines changed

src/cdk/scrolling/scroll-dispatcher.spec.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -71,18 +71,6 @@ describe('ScrollDispatcher', () => {
7171
expect(serviceSpy).toHaveBeenCalled();
7272
}));
7373

74-
it('should not execute the global events in the Angular zone', () => {
75-
scroll.scrolled(0).subscribe(() => {});
76-
dispatchFakeEvent(document, 'scroll', false);
77-
78-
expect(fixture.ngZone!.isStable).toBe(true);
79-
});
80-
81-
it('should not execute the scrollable events in the Angular zone', () => {
82-
dispatchFakeEvent(fixture.componentInstance.scrollingElement.nativeElement, 'scroll');
83-
expect(fixture.ngZone!.isStable).toBe(true);
84-
});
85-
8674
it('should be able to unsubscribe from the global scrollable', () => {
8775
const spy = jasmine.createSpy('global scroll callback');
8876
const subscription = scroll.scrolled(0).subscribe(spy);
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import {Component, ElementRef, ViewChild, provideZoneChangeDetection} from '@angular/core';
2+
import {ComponentFixture, TestBed, inject, waitForAsync} from '@angular/core/testing';
3+
import {dispatchFakeEvent} from '../testing/private';
4+
import {ScrollDispatcher} from './scroll-dispatcher';
5+
import {CdkScrollable} from './scrollable';
6+
import {ScrollingModule} from './scrolling-module';
7+
8+
describe('ScrollDispatcher Zone.js integration', () => {
9+
beforeEach(waitForAsync(() => {
10+
TestBed.configureTestingModule({
11+
imports: [ScrollingModule, ScrollingComponent],
12+
providers: [provideZoneChangeDetection()],
13+
});
14+
15+
TestBed.compileComponents();
16+
}));
17+
18+
describe('Basic usage', () => {
19+
let scroll: ScrollDispatcher;
20+
let fixture: ComponentFixture<ScrollingComponent>;
21+
22+
beforeEach(inject([ScrollDispatcher], (s: ScrollDispatcher) => {
23+
scroll = s;
24+
25+
fixture = TestBed.createComponent(ScrollingComponent);
26+
fixture.detectChanges();
27+
}));
28+
29+
it('should not execute the global events in the Angular zone', () => {
30+
scroll.scrolled(0).subscribe(() => {});
31+
dispatchFakeEvent(document, 'scroll', false);
32+
33+
expect(fixture.ngZone!.isStable).toBe(true);
34+
});
35+
36+
it('should not execute the scrollable events in the Angular zone', () => {
37+
dispatchFakeEvent(fixture.componentInstance.scrollingElement.nativeElement, 'scroll');
38+
expect(fixture.ngZone!.isStable).toBe(true);
39+
});
40+
});
41+
});
42+
43+
/** Simple component that contains a large div and can be scrolled. */
44+
@Component({
45+
template: `<div #scrollingElement cdkScrollable style="height: 9999px"></div>`,
46+
standalone: true,
47+
imports: [ScrollingModule],
48+
})
49+
class ScrollingComponent {
50+
@ViewChild(CdkScrollable) scrollable: CdkScrollable;
51+
@ViewChild('scrollingElement') scrollingElement: ElementRef<HTMLElement>;
52+
}

src/cdk/scrolling/viewport-ruler.spec.ts

Lines changed: 3 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
1-
import {TestBed, inject, fakeAsync, tick} from '@angular/core/testing';
1+
import {TestBed, fakeAsync, inject, tick} from '@angular/core/testing';
2+
import {dispatchFakeEvent} from '../testing/private';
23
import {ScrollingModule} from './public-api';
34
import {ViewportRuler} from './viewport-ruler';
4-
import {dispatchFakeEvent} from '../testing/private';
5-
import {NgZone} from '@angular/core';
6-
import {Subscription} from 'rxjs';
75

86
describe('ViewportRuler', () => {
97
let viewportRuler: ViewportRuler;
10-
let ngZone: NgZone;
118

129
let startingWindowWidth = window.innerWidth;
1310
let startingWindowHeight = window.innerHeight;
@@ -24,9 +21,8 @@ describe('ViewportRuler', () => {
2421
}),
2522
);
2623

27-
beforeEach(inject([ViewportRuler, NgZone], (v: ViewportRuler, n: NgZone) => {
24+
beforeEach(inject([ViewportRuler], (v: ViewportRuler) => {
2825
viewportRuler = v;
29-
ngZone = n;
3026
scrollTo(0, 0);
3127
}));
3228

@@ -133,27 +129,5 @@ describe('ViewportRuler', () => {
133129
expect(spy).toHaveBeenCalledTimes(1);
134130
subscription.unsubscribe();
135131
}));
136-
137-
it('should run the resize event outside the NgZone', () => {
138-
const spy = jasmine.createSpy('viewport changed spy');
139-
const subscription = viewportRuler.change(0).subscribe(() => spy(NgZone.isInAngularZone()));
140-
141-
dispatchFakeEvent(window, 'resize');
142-
expect(spy).toHaveBeenCalledWith(false);
143-
subscription.unsubscribe();
144-
});
145-
146-
it('should run events outside of the NgZone, even if the subcription is from inside', () => {
147-
const spy = jasmine.createSpy('viewport changed spy');
148-
let subscription: Subscription;
149-
150-
ngZone.run(() => {
151-
subscription = viewportRuler.change(0).subscribe(() => spy(NgZone.isInAngularZone()));
152-
dispatchFakeEvent(window, 'resize');
153-
});
154-
155-
expect(spy).toHaveBeenCalledWith(false);
156-
subscription!.unsubscribe();
157-
});
158132
});
159133
});
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import {NgZone, provideZoneChangeDetection} from '@angular/core';
2+
import {TestBed, inject} from '@angular/core/testing';
3+
import {Subscription} from 'rxjs';
4+
import {dispatchFakeEvent} from '../testing/private';
5+
import {ScrollingModule} from './scrolling-module';
6+
import {ViewportRuler} from './viewport-ruler';
7+
8+
describe('ViewportRuler', () => {
9+
let viewportRuler: ViewportRuler;
10+
let ngZone: NgZone;
11+
12+
// Create a very large element that will make the page scrollable.
13+
let veryLargeElement: HTMLElement = document.createElement('div');
14+
veryLargeElement.style.width = '6000px';
15+
veryLargeElement.style.height = '6000px';
16+
17+
beforeEach(() =>
18+
TestBed.configureTestingModule({
19+
imports: [ScrollingModule],
20+
providers: [provideZoneChangeDetection(), ViewportRuler],
21+
}),
22+
);
23+
24+
beforeEach(inject([ViewportRuler, NgZone], (v: ViewportRuler, n: NgZone) => {
25+
viewportRuler = v;
26+
ngZone = n;
27+
scrollTo(0, 0);
28+
}));
29+
30+
describe('changed event', () => {
31+
it('should run the resize event outside the NgZone', () => {
32+
const spy = jasmine.createSpy('viewport changed spy');
33+
const subscription = viewportRuler.change(0).subscribe(() => spy(NgZone.isInAngularZone()));
34+
35+
dispatchFakeEvent(window, 'resize');
36+
expect(spy).toHaveBeenCalledWith(false);
37+
subscription.unsubscribe();
38+
});
39+
40+
it('should run events outside of the NgZone, even if the subcription is from inside', () => {
41+
const spy = jasmine.createSpy('viewport changed spy');
42+
let subscription: Subscription;
43+
44+
ngZone.run(() => {
45+
subscription = viewportRuler.change(0).subscribe(() => spy(NgZone.isInAngularZone()));
46+
dispatchFakeEvent(window, 'resize');
47+
});
48+
49+
expect(spy).toHaveBeenCalledWith(false);
50+
subscription!.unsubscribe();
51+
});
52+
});
53+
});

src/cdk/text-field/autofill.zone.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {ComponentFixture, TestBed, inject} from '@angular/core/testing';
33
import {AutofillMonitor} from './autofill';
44
import {TextFieldModule} from './text-field-module';
55

6-
describe('AutofillMonitor', () => {
6+
describe('AutofillMonitor Zone.js integration', () => {
77
let autofillMonitor: AutofillMonitor;
88
let fixture: ComponentFixture<Inputs>;
99
let testComponent: Inputs;

src/dev-app/main.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,15 @@
99
// Load `$localize` for examples using it.
1010
import '@angular/localize/init';
1111

12+
import {provideHttpClient} from '@angular/common/http';
1213
import {
1314
importProvidersFrom,
14-
provideZoneChangeDetection,
1515
provideExperimentalZonelessChangeDetection,
16+
// tslint:disable-next-line:no-zone-dependencies -- Allow manual testing of dev-app with zones
17+
provideZoneChangeDetection,
1618
} from '@angular/core';
1719
import {bootstrapApplication} from '@angular/platform-browser';
1820
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
19-
import {provideHttpClient} from '@angular/common/http';
2021
import {RouterModule} from '@angular/router';
2122

2223
import {Directionality} from '@angular/cdk/bidi';

src/material-experimental/popover-edit/popover-edit.spec.ts

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,17 @@
11
import {DataSource} from '@angular/cdk/collections';
2-
import {LEFT_ARROW, UP_ARROW, RIGHT_ARROW, DOWN_ARROW, TAB} from '@angular/cdk/keycodes';
3-
import {MatTableModule} from '@angular/material/table';
4-
import {dispatchKeyboardEvent} from '../../cdk/testing/private';
2+
import {DOWN_ARROW, LEFT_ARROW, RIGHT_ARROW, TAB, UP_ARROW} from '@angular/cdk/keycodes';
53
import {CommonModule} from '@angular/common';
6-
import {
7-
Component,
8-
Directive,
9-
ElementRef,
10-
ViewChild,
11-
provideZoneChangeDetection,
12-
} from '@angular/core';
13-
import {ComponentFixture, fakeAsync, flush, TestBed, tick} from '@angular/core/testing';
4+
import {Component, Directive, ElementRef, ViewChild} from '@angular/core';
5+
import {ComponentFixture, TestBed, fakeAsync, flush, tick} from '@angular/core/testing';
146
import {FormsModule, NgForm} from '@angular/forms';
7+
import {MatTableModule} from '@angular/material/table';
158
import {BehaviorSubject} from 'rxjs';
9+
import {dispatchKeyboardEvent} from '../../cdk/testing/private';
1610

1711
import {
1812
CdkPopoverEditColspan,
19-
HoverContentState,
2013
FormValueContainer,
14+
HoverContentState,
2115
PopoverEditClickOutBehavior,
2216
} from '@angular/cdk-experimental/popover-edit';
2317
import {MatPopoverEditModule} from './index';
@@ -297,12 +291,6 @@ const testCases = [
297291
] as const;
298292

299293
describe('Material Popover Edit', () => {
300-
beforeEach(() => {
301-
TestBed.configureTestingModule({
302-
providers: [provideZoneChangeDetection()],
303-
});
304-
});
305-
306294
for (const [componentClass, label] of testCases) {
307295
describe(label, () => {
308296
let component: BaseTestComponent;
@@ -317,6 +305,7 @@ describe('Material Popover Edit', () => {
317305
component = fixture.componentInstance;
318306
fixture.detectChanges();
319307
tick(10);
308+
fixture.detectChanges();
320309
}));
321310

322311
describe('row hover content', () => {
@@ -432,6 +421,7 @@ describe('Material Popover Edit', () => {
432421

433422
it('does not trigger edit when disabled', fakeAsync(() => {
434423
component.nameEditDisabled = true;
424+
fixture.changeDetectorRef.markForCheck();
435425
fixture.detectChanges();
436426

437427
// Uses Enter to open the lens.
@@ -452,6 +442,7 @@ describe('Material Popover Edit', () => {
452442

453443
it('unsets tabindex to 0 on disabled cells', () => {
454444
component.nameEditDisabled = true;
445+
fixture.changeDetectorRef.markForCheck();
455446
fixture.detectChanges();
456447

457448
expect(component.getEditCell().hasAttribute('tabindex')).toBe(false);
@@ -594,6 +585,7 @@ matPopoverEditTabOut`, fakeAsync(() => {
594585

595586
it('positions the lens at the top left corner and spans the full width of the cell', fakeAsync(() => {
596587
component.openLens();
588+
fixture.detectChanges();
597589

598590
const paneRect = component.getEditPane()!.getBoundingClientRect();
599591
const cellRect = component.getEditCell().getBoundingClientRect();
@@ -610,16 +602,19 @@ matPopoverEditTabOut`, fakeAsync(() => {
610602
);
611603

612604
component.colspan = {before: 1};
605+
fixture.changeDetectorRef.markForCheck();
613606
fixture.detectChanges();
614607

615608
component.openLens();
609+
fixture.detectChanges();
616610

617611
let paneRect = component.getEditPane()!.getBoundingClientRect();
618612
expectPixelsToEqual(paneRect.top, cellRects[0].top);
619613
expectPixelsToEqual(paneRect.left, cellRects[0].left);
620614
expectPixelsToEqual(paneRect.right, cellRects[1].right);
621615

622616
component.colspan = {after: 1};
617+
fixture.changeDetectorRef.markForCheck();
623618
fixture.detectChanges();
624619

625620
paneRect = component.getEditPane()!.getBoundingClientRect();
@@ -630,6 +625,7 @@ matPopoverEditTabOut`, fakeAsync(() => {
630625
// expectPixelsToEqual(paneRect.right, cellRects[2].right);
631626

632627
component.colspan = {before: 1, after: 1};
628+
fixture.changeDetectorRef.markForCheck();
633629
fixture.detectChanges();
634630

635631
paneRect = component.getEditPane()!.getBoundingClientRect();
@@ -706,13 +702,15 @@ matPopoverEditTabOut`, fakeAsync(() => {
706702
expect(component.lensIsOpen()).toBe(false);
707703

708704
component.openLens();
705+
fixture.detectChanges();
709706

710707
expect(component.getInput()!.value).toBe('Hydragon');
711708
clearLeftoverTimers();
712709
}));
713710

714711
it('resets the lens to original value', fakeAsync(() => {
715712
component.openLens();
713+
fixture.detectChanges();
716714

717715
component.getInput()!.value = 'Hydragon';
718716
component.getInput()!.dispatchEvent(new Event('input'));
@@ -733,6 +731,7 @@ matPopoverEditTabOut`, fakeAsync(() => {
733731
fixture.detectChanges();
734732

735733
component.openLens();
734+
fixture.detectChanges();
736735

737736
component.getInput()!.value = 'Hydragon X';
738737
component.getInput()!.dispatchEvent(new Event('input'));

tools/tslint-rules/noZoneDependenciesRule.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
import minimatch from 'minimatch';
12
import * as Lint from 'tslint';
23
import ts from 'typescript';
3-
import minimatch from 'minimatch';
44

55
/**
66
* NgZone properties that are ok to access.
@@ -49,4 +49,16 @@ class Walker extends Lint.RuleWalker {
4949

5050
return super.visitPropertyAccessExpression(node);
5151
}
52+
53+
override visitNamedImports(node: ts.NamedImports): void {
54+
if (!this._enabled) {
55+
return;
56+
}
57+
58+
node.elements.forEach(specifier => {
59+
if (specifier.name.getText() === 'provideZoneChangeDetection') {
60+
this.addFailureAtNode(specifier, `Using zone change detection is not allowed.`);
61+
}
62+
});
63+
}
5264
}

tslint.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -185,10 +185,12 @@
185185
"no-zone-dependencies": [
186186
true,
187187
[
188-
// Tests may need to verify behavior with zones.
189-
"**/*.spec.ts",
188+
// Allow in tests that specficially test integration with Zone.js.
189+
"**/*.zone.spec.ts",
190190
// TODO(mmalerba): following files to be cleaned up and removed from this list:
191-
"**/cdk/a11y/focus-trap/focus-trap.ts"
191+
"**/src/cdk/a11y/focus-trap/focus-trap.ts",
192+
"**/src/cdk/testing/tests/testbed.spec.ts",
193+
"**/src/material/**/*.spec.ts"
192194
]
193195
]
194196
},

0 commit comments

Comments
 (0)