Skip to content

Commit f10a158

Browse files
committed
fix(cdk/observers): don't observe content of comments
1 parent 7165a45 commit f10a158

File tree

4 files changed

+118
-15
lines changed

4 files changed

+118
-15
lines changed

src/cdk/a11y/live-announcer/live-announcer.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import {By} from '@angular/platform-browser';
77
import {A11yModule} from '../index';
88
import {LiveAnnouncer} from './live-announcer';
99
import {
10-
LIVE_ANNOUNCER_ELEMENT_TOKEN,
1110
LIVE_ANNOUNCER_DEFAULT_OPTIONS,
11+
LIVE_ANNOUNCER_ELEMENT_TOKEN,
1212
LiveAnnouncerDefaultOptions,
1313
} from './live-announcer-tokens';
1414

@@ -291,7 +291,7 @@ describe('CdkAriaLive', () => {
291291
let announcerSpy: jasmine.Spy;
292292
let fixture: ComponentFixture<DivWithCdkAriaLive>;
293293

294-
const invokeMutationCallbacks = () => mutationCallbacks.forEach(cb => cb());
294+
const invokeMutationCallbacks = () => mutationCallbacks.forEach(cb => cb([{type: 'fake'}]));
295295

296296
beforeEach(fakeAsync(() => {
297297
TestBed.configureTestingModule({

src/cdk/observers/observe-content.spec.ts

Lines changed: 79 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import {Component, ElementRef, ViewChild} from '@angular/core';
22
import {
3-
waitForAsync,
43
ComponentFixture,
4+
TestBed,
55
fakeAsync,
66
inject,
7-
TestBed,
87
tick,
8+
waitForAsync,
99
} from '@angular/core/testing';
1010
import {ContentObserver, MutationObserverFactory, ObserversModule} from './observe-content';
1111

@@ -112,9 +112,9 @@ describe('Observe content directive', () => {
112112
}));
113113

114114
it('should debounce the content changes', fakeAsync(() => {
115-
invokeCallbacks();
116-
invokeCallbacks();
117-
invokeCallbacks();
115+
invokeCallbacks([{type: 'fake'}]);
116+
invokeCallbacks([{type: 'fake'}]);
117+
invokeCallbacks([{type: 'fake'}]);
118118

119119
tick(500);
120120
expect(fixture.componentInstance.spy).toHaveBeenCalledTimes(1);
@@ -166,7 +166,7 @@ describe('ContentObserver injectable', () => {
166166
expect(spy).not.toHaveBeenCalled();
167167

168168
fixture.componentInstance.text = 'text';
169-
invokeCallbacks();
169+
invokeCallbacks([{type: 'fake'}]);
170170

171171
expect(spy).toHaveBeenCalled();
172172
}));
@@ -186,19 +186,90 @@ describe('ContentObserver injectable', () => {
186186
expect(mof.create).toHaveBeenCalledTimes(1);
187187

188188
fixture.componentInstance.text = 'text';
189-
invokeCallbacks();
189+
invokeCallbacks([{type: 'fake'}]);
190190

191191
expect(spy).toHaveBeenCalledTimes(2);
192192

193193
spy.calls.reset();
194194
sub1.unsubscribe();
195195
fixture.componentInstance.text = 'text text';
196-
invokeCallbacks();
196+
invokeCallbacks([{type: 'fake'}]);
197197

198198
expect(spy).toHaveBeenCalledTimes(1);
199199
}),
200200
));
201201
});
202+
203+
describe('real behavior', () => {
204+
let spy: jasmine.Spy;
205+
let contentEl: HTMLElement;
206+
let contentObserver: ContentObserver;
207+
208+
beforeEach(waitForAsync(() => {
209+
TestBed.configureTestingModule({
210+
imports: [ObserversModule, UnobservedComponentWithTextContent],
211+
});
212+
213+
TestBed.compileComponents();
214+
const fixture = TestBed.createComponent(UnobservedComponentWithTextContent);
215+
fixture.autoDetectChanges();
216+
spy = jasmine.createSpy('content observer');
217+
contentObserver = TestBed.inject(ContentObserver);
218+
contentEl = fixture.componentInstance.contentEl.nativeElement;
219+
contentObserver.observe(contentEl).subscribe(spy);
220+
}));
221+
222+
it('should ignore addition or removal of comments', waitForAsync(async () => {
223+
const comment = document.createComment('cool');
224+
await new Promise(r => setTimeout(r));
225+
226+
spy.calls.reset();
227+
contentEl.appendChild(comment);
228+
await new Promise(r => setTimeout(r));
229+
expect(spy).not.toHaveBeenCalled();
230+
231+
comment.remove();
232+
await new Promise(r => setTimeout(r));
233+
expect(spy).not.toHaveBeenCalled();
234+
}));
235+
236+
it('should not ignore addition or removal of text', waitForAsync(async () => {
237+
const text = document.createTextNode('cool');
238+
await new Promise(r => setTimeout(r));
239+
240+
spy.calls.reset();
241+
contentEl.appendChild(text);
242+
await new Promise(r => setTimeout(r));
243+
expect(spy).toHaveBeenCalled();
244+
245+
spy.calls.reset();
246+
text.remove();
247+
await new Promise(r => setTimeout(r));
248+
expect(spy).toHaveBeenCalled();
249+
}));
250+
251+
it('should ignore comment content change', waitForAsync(async () => {
252+
const comment = document.createComment('cool');
253+
contentEl.appendChild(comment);
254+
await new Promise(r => setTimeout(r));
255+
256+
spy.calls.reset();
257+
comment.textContent = 'beans';
258+
await new Promise(r => setTimeout(r));
259+
expect(spy).not.toHaveBeenCalled();
260+
}));
261+
262+
it('should not ignore text content change', waitForAsync(async () => {
263+
const text = document.createTextNode('cool');
264+
contentEl.appendChild(text);
265+
await new Promise(r => setTimeout(r));
266+
267+
spy.calls.reset();
268+
text.textContent = 'beans';
269+
await new Promise(r => setTimeout(r));
270+
expect(spy).toHaveBeenCalled();
271+
}));
272+
});
202273
});
203274

204275
@Component({

src/cdk/observers/observe-content.ts

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {coerceNumberProperty, coerceElement, NumberInput} from '@angular/cdk/coercion';
9+
import {NumberInput, coerceElement, coerceNumberProperty} from '@angular/cdk/coercion';
1010
import {
1111
AfterContentInit,
1212
Directive,
@@ -20,8 +20,35 @@ import {
2020
Output,
2121
booleanAttribute,
2222
} from '@angular/core';
23-
import {Observable, Subject, Subscription, Observer} from 'rxjs';
24-
import {debounceTime} from 'rxjs/operators';
23+
import {Observable, Observer, Subject, Subscription} from 'rxjs';
24+
import {debounceTime, filter, map} from 'rxjs/operators';
25+
26+
// Angular may add, remove, or edit comment nodes during change detection. We don't care about
27+
// these changes because they don't affect the user-preceived content, and worse it can cause
28+
// infinite change detection cycles where the change detection updates a comment, triggering the
29+
// MutationObserver, triggering another change detection and kicking the cycle off again.
30+
function shouldIgnoreRecord(record: MutationRecord) {
31+
// Ignore changes to comment text.
32+
if (record.type === 'characterData' && record.target instanceof Comment) {
33+
return true;
34+
}
35+
// Ignore addition / removal of comments.
36+
if (record.type === 'childList') {
37+
for (let i = 0; i < record.addedNodes.length; i++) {
38+
if (!(record.addedNodes[i] instanceof Comment)) {
39+
return false;
40+
}
41+
}
42+
for (let i = 0; i < record.removedNodes.length; i++) {
43+
if (!(record.removedNodes[i] instanceof Comment)) {
44+
return false;
45+
}
46+
}
47+
return true;
48+
}
49+
// Observe everything else.
50+
return false;
51+
}
2552

2653
/**
2754
* Factory that creates a new MutationObserver and allows us to stub it out in unit tests.
@@ -70,7 +97,12 @@ export class ContentObserver implements OnDestroy {
7097

7198
return new Observable((observer: Observer<MutationRecord[]>) => {
7299
const stream = this._observeElement(element);
73-
const subscription = stream.subscribe(observer);
100+
const subscription = stream
101+
.pipe(
102+
map(records => records.filter(record => !shouldIgnoreRecord(record))),
103+
filter(records => !!records.length),
104+
)
105+
.subscribe(observer);
74106

75107
return () => {
76108
subscription.unsubscribe();

src/material/tabs/tab-header.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -715,7 +715,7 @@ describe('MDC-based MatTabHeader', () => {
715715
label.textContent += extraText;
716716
});
717717

718-
mutationCallbacks.forEach(callback => callback());
718+
mutationCallbacks.forEach(callback => callback([{type: 'fake'}]));
719719
fixture.detectChanges();
720720

721721
expect(tabHeaderElement.classList).toContain(enabledClass);

0 commit comments

Comments
 (0)