Skip to content

Commit bfe8a58

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

File tree

4 files changed

+132
-15
lines changed

4 files changed

+132
-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: 97 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,108 @@ 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+
beforeEach(waitForAsync(() => {
205+
TestBed.configureTestingModule({
206+
imports: [ObserversModule, UnobservedComponentWithTextContent],
207+
});
208+
209+
TestBed.compileComponents();
210+
}));
211+
212+
it('should ignore addition or removal of comments', waitForAsync(
213+
inject([ContentObserver], async (contentObserver: ContentObserver) => {
214+
const spy = jasmine.createSpy('content observer');
215+
const comment = document.createComment('cool');
216+
217+
const fixture = TestBed.createComponent(UnobservedComponentWithTextContent);
218+
fixture.autoDetectChanges();
219+
contentObserver.observe(fixture.componentInstance.contentEl).subscribe(spy);
220+
await new Promise(r => setTimeout(r));
221+
spy.calls.reset();
222+
223+
fixture.componentInstance.contentEl.nativeElement.appendChild(comment);
224+
await new Promise(r => setTimeout(r));
225+
expect(spy).not.toHaveBeenCalled();
226+
227+
fixture.componentInstance.contentEl.nativeElement.remove(comment);
228+
await new Promise(r => setTimeout(r));
229+
expect(spy).not.toHaveBeenCalled();
230+
}),
231+
));
232+
233+
it('should not ignore addition or removal of text', waitForAsync(
234+
inject([ContentObserver], async (contentObserver: ContentObserver) => {
235+
const spy = jasmine.createSpy('content observer');
236+
const text = document.createTextNode('cool');
237+
238+
const fixture = TestBed.createComponent(UnobservedComponentWithTextContent);
239+
fixture.autoDetectChanges();
240+
contentObserver.observe(fixture.componentInstance.contentEl).subscribe(spy);
241+
await new Promise(r => setTimeout(r));
242+
243+
spy.calls.reset();
244+
fixture.componentInstance.contentEl.nativeElement.appendChild(text);
245+
await new Promise(r => setTimeout(r));
246+
expect(spy).toHaveBeenCalled();
247+
248+
spy.calls.reset();
249+
fixture.componentInstance.contentEl.nativeElement.remove(text);
250+
await new Promise(r => setTimeout(r));
251+
expect(spy).toHaveBeenCalled();
252+
}),
253+
));
254+
255+
it('should ignore comment content change', waitForAsync(
256+
inject([ContentObserver], async (contentObserver: ContentObserver) => {
257+
const spy = jasmine.createSpy('content observer');
258+
const comment = document.createComment('cool');
259+
260+
const fixture = TestBed.createComponent(UnobservedComponentWithTextContent);
261+
fixture.autoDetectChanges();
262+
contentObserver.observe(fixture.componentInstance.contentEl).subscribe(spy);
263+
fixture.componentInstance.contentEl.nativeElement.appendChild(comment);
264+
await new Promise(r => setTimeout(r));
265+
266+
spy.calls.reset();
267+
comment.textContent = 'beans';
268+
await new Promise(r => setTimeout(r));
269+
expect(spy).not.toHaveBeenCalled();
270+
}),
271+
));
272+
273+
it('should not ignore text content change', waitForAsync(
274+
inject([ContentObserver], async (contentObserver: ContentObserver) => {
275+
const spy = jasmine.createSpy('content observer');
276+
const text = document.createTextNode('cool');
277+
278+
const fixture = TestBed.createComponent(UnobservedComponentWithTextContent);
279+
fixture.autoDetectChanges();
280+
contentObserver.observe(fixture.componentInstance.contentEl).subscribe(spy);
281+
fixture.componentInstance.contentEl.nativeElement.appendChild(text);
282+
await new Promise(r => setTimeout(r));
283+
284+
spy.calls.reset();
285+
text.textContent = 'beans';
286+
await new Promise(r => setTimeout(r));
287+
expect(spy).toHaveBeenCalled();
288+
}),
289+
));
290+
});
202291
});
203292

204293
@Component({

src/cdk/observers/observe-content.ts

Lines changed: 32 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,31 @@ 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+
function shouldIgnoreRecord(record: MutationRecord) {
27+
// Ignore changes to comment text.
28+
if (record.type === 'characterData' && record.target instanceof Comment) {
29+
return true;
30+
}
31+
// Ignore addition / removal of comments.
32+
if (record.type === 'childList') {
33+
for (let i = 0; i < record.addedNodes.length; i++) {
34+
if (!(record.addedNodes[i] instanceof Comment)) {
35+
return false;
36+
}
37+
}
38+
for (let i = 0; i < record.removedNodes.length; i++) {
39+
if (!(record.removedNodes[i] instanceof Comment)) {
40+
return false;
41+
}
42+
}
43+
return true;
44+
}
45+
// Observe everything else.
46+
return false;
47+
}
2548

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

7194
return new Observable((observer: Observer<MutationRecord[]>) => {
7295
const stream = this._observeElement(element);
73-
const subscription = stream.subscribe(observer);
96+
const subscription = stream
97+
.pipe(
98+
map(records => records.filter(record => !shouldIgnoreRecord(record))),
99+
filter(records => !!records.length),
100+
)
101+
.subscribe(observer);
74102

75103
return () => {
76104
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)