fix(ivy): ensure errors are thrown during checkNoChanges for style/class bindings (#33103)

Prior to this fix, all style/class bindings (e.g. `[style]` and
`[class.foo]`) would quietly update a binding value if and when the
current binding value changes during checkNoChanges.

With this patch, all styling instructions will properly check to see
if the value has changed during the second pass of detectChanges()
if checkNoChanges is active.

PR Close #33103
This commit is contained in:
Matias Niemelä
2019-10-11 17:31:26 +02:00
parent 9d54679e66
commit f45c43188f
8 changed files with 116 additions and 37 deletions

View File

@ -1,5 +1,5 @@
import { NO_ERRORS_SCHEMA, DebugElement } from '@angular/core';
import { inject, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
import { inject, ComponentFixture, TestBed, fakeAsync, flushMicrotasks, tick } from '@angular/core/testing';
import { Title } from '@angular/platform-browser';
import { APP_BASE_HREF } from '@angular/common';
import { HttpClient } from '@angular/common/http';
@ -529,7 +529,8 @@ describe('AppComponent', () => {
it('should call `scrollAfterRender` (via `onDocInserted`) when navigate to a new Doc', fakeAsync(() => {
locationService.go('guide/pipes');
tick(1); // triggers the HTTP response for the document
fixture.detectChanges(); // triggers the event that calls `onDocInserted`
fixture.detectChanges(); // passes the new doc to the `DocViewer`
flushMicrotasks(); // triggers the `DocViewer` event that calls `onDocInserted`
expect(scrollAfterRenderSpy).toHaveBeenCalledWith(scrollDelay);

View File

@ -1,7 +1,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Meta, Title } from '@angular/platform-browser';
import { Observable, of } from 'rxjs';
import { Observable, asapScheduler, of } from 'rxjs';
import { FILE_NOT_FOUND_ID, FETCHING_ERROR_ID } from 'app/documents/document.service';
import { Logger } from 'app/shared/logger.service';
@ -21,6 +21,8 @@ describe('DocViewerComponent', () => {
let docViewerEl: HTMLElement;
let docViewer: TestDocViewerComponent;
const safeFlushAsapScheduler = () => asapScheduler.actions.length && asapScheduler.flush();
beforeEach(() => {
TestBed.configureTestingModule({
imports: [CustomElementsModule, TestModule],
@ -42,19 +44,20 @@ describe('DocViewerComponent', () => {
describe('#doc', () => {
let renderSpy: jasmine.Spy;
const setCurrentDoc = (contents: string|null, id = 'fizz/buzz') => {
parentComponent.currentDoc = {contents, id};
parentFixture.detectChanges();
const setCurrentDoc = (newDoc: TestParentComponent['currentDoc']) => {
parentComponent.currentDoc = newDoc && {id: 'fizz/buzz', ...newDoc};
parentFixture.detectChanges(); // Run change detection to propagate the new doc to `DocViewer`.
safeFlushAsapScheduler(); // Flush `asapScheduler` to trigger `DocViewer#render()`.
};
beforeEach(() => renderSpy = spyOn(docViewer, 'render').and.callFake(() => of(undefined)));
it('should render the new document', () => {
setCurrentDoc('foo', 'bar');
setCurrentDoc({contents: 'foo', id: 'bar'});
expect(renderSpy).toHaveBeenCalledTimes(1);
expect(renderSpy.calls.mostRecent().args).toEqual([{id: 'bar', contents: 'foo'}]);
setCurrentDoc(null, 'baz');
setCurrentDoc({contents: null, id: 'baz'});
expect(renderSpy).toHaveBeenCalledTimes(2);
expect(renderSpy.calls.mostRecent().args).toEqual([{id: 'baz', contents: null}]);
});
@ -63,24 +66,20 @@ describe('DocViewerComponent', () => {
const obs = new ObservableWithSubscriptionSpies();
renderSpy.and.returnValue(obs);
setCurrentDoc('foo', 'bar');
setCurrentDoc({contents: 'foo', id: 'bar'});
expect(obs.subscribeSpy).toHaveBeenCalledTimes(1);
expect(obs.unsubscribeSpies[0]).not.toHaveBeenCalled();
setCurrentDoc('baz', 'qux');
setCurrentDoc({contents: 'baz', id: 'qux'});
expect(obs.subscribeSpy).toHaveBeenCalledTimes(2);
expect(obs.unsubscribeSpies[0]).toHaveBeenCalledTimes(1);
});
it('should ignore falsy document values', () => {
parentComponent.currentDoc = null;
parentFixture.detectChanges();
setCurrentDoc(null);
expect(renderSpy).not.toHaveBeenCalled();
parentComponent.currentDoc = undefined;
parentFixture.detectChanges();
setCurrentDoc(undefined);
expect(renderSpy).not.toHaveBeenCalled();
});
});
@ -92,14 +91,17 @@ describe('DocViewerComponent', () => {
expect(renderSpy).not.toHaveBeenCalled();
docViewer.doc = {contents: 'Some content', id: 'some-id'};
safeFlushAsapScheduler();
expect(renderSpy).toHaveBeenCalledTimes(1);
docViewer.ngOnDestroy();
docViewer.doc = {contents: 'Other content', id: 'other-id'};
safeFlushAsapScheduler();
expect(renderSpy).toHaveBeenCalledTimes(1);
docViewer.doc = {contents: 'More content', id: 'more-id'};
safeFlushAsapScheduler();
expect(renderSpy).toHaveBeenCalledTimes(1);
});
});

View File

@ -1,8 +1,8 @@
import { Component, ElementRef, EventEmitter, Input, OnDestroy, Output } from '@angular/core';
import { Title, Meta } from '@angular/platform-browser';
import { Observable, of, timer } from 'rxjs';
import { catchError, switchMap, takeUntil, tap } from 'rxjs/operators';
import { asapScheduler, Observable, of, timer } from 'rxjs';
import { catchError, observeOn, switchMap, takeUntil, tap } from 'rxjs/operators';
import { DocumentContents, FILE_NOT_FOUND_ID, FETCHING_ERROR_ID } from 'app/documents/document.service';
import { Logger } from 'app/shared/logger.service';
@ -78,6 +78,7 @@ export class DocViewerComponent implements OnDestroy {
this.docContents$
.pipe(
observeOn(asapScheduler),
switchMap(newDoc => this.render(newDoc)),
takeUntil(this.onDestroy$),
)