Skip to content

Commit

Permalink
feat(component): add suspense template input to LetDirective (#3377)
Browse files Browse the repository at this point in the history
Closes #3340
  • Loading branch information
markostanimirovic committed May 18, 2022
1 parent d54b033 commit 345ee53
Show file tree
Hide file tree
Showing 5 changed files with 318 additions and 141 deletions.
3 changes: 2 additions & 1 deletion modules/component/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
},
"rules": {
"@angular-eslint/directive-selector": "off",
"@angular-eslint/component-selector": "off"
"@angular-eslint/component-selector": "off",
"@angular-eslint/no-input-rename": "off"
},
"plugins": ["@typescript-eslint"]
},
Expand Down
31 changes: 8 additions & 23 deletions modules/component/spec/fixtures/fixtures.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import createSpy = jasmine.createSpy;
import { ChangeDetectorRef, NgZone } from '@angular/core';
import { NgZone } from '@angular/core';
import { MockNoopNgZone } from './mock-noop-ng-zone';

/**
Expand All @@ -17,27 +16,13 @@ export const manualInstanceNoopNgZone = new NoopNgZone({
});

export class MockChangeDetectorRef {
markForCheck = createSpy('markForCheck');
detectChanges = createSpy('detectChanges');
checkNoChanges = createSpy('checkNoChanges');
detach = createSpy('detach');
reattach = createSpy('reattach');
markForCheck = jest.fn();
detectChanges = jest.fn();
checkNoChanges = jest.fn();
detach = jest.fn();
reattach = jest.fn();
}

export const mockPromise = {
then: () => {},
};

export function getMockOptimizedStrategyConfig() {
return {
component: {},
cdRef: (new MockChangeDetectorRef() as any) as ChangeDetectorRef,
};
}

export function getMockNoopStrategyConfig() {
return {
component: {},
cdRef: (new MockChangeDetectorRef() as any) as ChangeDetectorRef,
};
export class MockErrorHandler {
handleError = jest.fn();
}
159 changes: 155 additions & 4 deletions modules/component/spec/let/let.directive.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
ChangeDetectorRef,
Component,
Directive,
ErrorHandler,
TemplateRef,
ViewContainerRef,
} from '@angular/core';
Expand All @@ -15,17 +16,20 @@ import {
} from '@angular/core/testing';
import {
BehaviorSubject,
delay,
EMPTY,
interval,
NEVER,
Observable,
ObservableInput,
of,
switchMap,
take,
throwError,
timer,
} from 'rxjs';
import { take } from 'rxjs/operators';
import { LetDirective } from '../../src/let/let.directive';
import { MockChangeDetectorRef } from '../fixtures/fixtures';
import { MockChangeDetectorRef, MockErrorHandler } from '../fixtures/fixtures';

@Component({
template: `
Expand All @@ -47,7 +51,7 @@ class LetDirectiveTestComponent {
`,
})
class LetDirectiveTestErrorComponent {
value$: Observable<number> = of(42);
value$ = of(42);
}

@Component({
Expand All @@ -58,7 +62,30 @@ class LetDirectiveTestErrorComponent {
`,
})
class LetDirectiveTestCompleteComponent {
value$: Observable<number> = of(42);
value$ = of(42);
}

@Component({
template: `
<ng-container *ngrxLet="value$ as value; $suspense as s">{{
s ? 'suspense' : value
}}</ng-container>
`,
})
class LetDirectiveTestSuspenseComponent {
value$ = of(42);
}

@Component({
template: `
<ng-container *ngrxLet="value$ as value; suspenseTpl: loading">{{
value === undefined ? 'undefined' : value
}}</ng-container>
<ng-template #loading>Loading...</ng-template>
`,
})
class LetDirectiveTestSuspenseTplComponent {
value$ = of(42);
}

@Directive({
Expand All @@ -79,6 +106,7 @@ export class RecursiveDirective {
})
class LetDirectiveTestRecursionComponent {
constructor(public subject: BehaviorSubject<number>) {}

get value$() {
return this.subject;
}
Expand Down Expand Up @@ -106,11 +134,13 @@ const setupLetDirectiveTestComponent = (): void => {
fixtureLetDirectiveTestComponent.componentInstance;
componentNativeElement = fixtureLetDirectiveTestComponent.nativeElement;
};

const setupLetDirectiveTestComponentError = (): void => {
TestBed.configureTestingModule({
declarations: [LetDirectiveTestErrorComponent, LetDirective],
providers: [
{ provide: ChangeDetectorRef, useClass: MockChangeDetectorRef },
{ provide: ErrorHandler, useClass: MockErrorHandler },
TemplateRef,
ViewContainerRef,
],
Expand All @@ -123,6 +153,7 @@ const setupLetDirectiveTestComponentError = (): void => {
fixtureLetDirectiveTestComponent.componentInstance;
componentNativeElement = fixtureLetDirectiveTestComponent.nativeElement;
};

const setupLetDirectiveTestComponentComplete = (): void => {
TestBed.configureTestingModule({
declarations: [LetDirectiveTestCompleteComponent, LetDirective],
Expand All @@ -141,6 +172,44 @@ const setupLetDirectiveTestComponentComplete = (): void => {
componentNativeElement = fixtureLetDirectiveTestComponent.nativeElement;
};

const setupLetDirectiveTestComponentSuspense = (): void => {
TestBed.configureTestingModule({
declarations: [LetDirectiveTestSuspenseComponent, LetDirective],
providers: [
{ provide: ChangeDetectorRef, useClass: MockChangeDetectorRef },
{ provide: ErrorHandler, useClass: MockErrorHandler },
TemplateRef,
ViewContainerRef,
],
});

fixtureLetDirectiveTestComponent = TestBed.createComponent(
LetDirectiveTestSuspenseComponent
);
letDirectiveTestComponent =
fixtureLetDirectiveTestComponent.componentInstance;
componentNativeElement = fixtureLetDirectiveTestComponent.nativeElement;
};

const setupLetDirectiveTestComponentSuspenseTpl = (): void => {
TestBed.configureTestingModule({
declarations: [LetDirectiveTestSuspenseTplComponent, LetDirective],
providers: [
{ provide: ChangeDetectorRef, useClass: MockChangeDetectorRef },
{ provide: ErrorHandler, useClass: MockErrorHandler },
TemplateRef,
ViewContainerRef,
],
});

fixtureLetDirectiveTestComponent = TestBed.createComponent(
LetDirectiveTestSuspenseTplComponent
);
letDirectiveTestComponent =
fixtureLetDirectiveTestComponent.componentInstance;
componentNativeElement = fixtureLetDirectiveTestComponent.nativeElement;
};

const setupLetDirectiveTestRecursionComponent = (): void => {
const subject = new BehaviorSubject(0);
TestBed.configureTestingModule({
Expand Down Expand Up @@ -320,6 +389,14 @@ describe('LetDirective', () => {
fixtureLetDirectiveTestComponent.detectChanges();
expect(componentNativeElement.textContent).toBe('true');
});

it('should call error handler', () => {
const errorHandler = TestBed.inject(ErrorHandler);
const error = new Error('ERROR');
letDirectiveTestComponent.value$ = throwError(() => error);
fixtureLetDirectiveTestComponent.detectChanges();
expect(errorHandler.handleError).toHaveBeenCalledWith(error);
});
});

describe('when complete', () => {
Expand All @@ -332,6 +409,80 @@ describe('LetDirective', () => {
});
});

describe('when suspense', () => {
beforeEach(waitForAsync(setupLetDirectiveTestComponentSuspense));

it('should not render when first observable is in suspense state', fakeAsync(() => {
letDirectiveTestComponent.value$ = of(true).pipe(delay(1000));
fixtureLetDirectiveTestComponent.detectChanges();
expect(componentNativeElement.textContent).toBe('');
tick(1000);
fixtureLetDirectiveTestComponent.detectChanges();
expect(componentNativeElement.textContent).toBe('true');
}));

it('should render suspense when next observable is in suspense state', fakeAsync(() => {
letDirectiveTestComponent.value$ = of(true);
fixtureLetDirectiveTestComponent.detectChanges();
letDirectiveTestComponent.value$ = of(false).pipe(delay(1000));
fixtureLetDirectiveTestComponent.detectChanges();
expect(componentNativeElement.textContent).toBe('suspense');
tick(1000);
fixtureLetDirectiveTestComponent.detectChanges();
expect(componentNativeElement.textContent).toBe('false');
}));
});

describe('when suspense template is passed', () => {
beforeEach(waitForAsync(setupLetDirectiveTestComponentSuspenseTpl));

it('should render main template when observable emits next event', () => {
letDirectiveTestComponent.value$ = new BehaviorSubject('ngrx');
fixtureLetDirectiveTestComponent.detectChanges();
expect(componentNativeElement.textContent).toBe('ngrx');
});

it('should render main template when observable emits error event', () => {
letDirectiveTestComponent.value$ = throwError(() => 'ERROR!');
fixtureLetDirectiveTestComponent.detectChanges();
expect(componentNativeElement.textContent).toBe('undefined');
});

it('should render main template when observable emits complete event', () => {
letDirectiveTestComponent.value$ = EMPTY;
fixtureLetDirectiveTestComponent.detectChanges();
expect(componentNativeElement.textContent).toBe('undefined');
});

it('should render suspense template when observable does not emit', () => {
letDirectiveTestComponent.value$ = NEVER;
fixtureLetDirectiveTestComponent.detectChanges();
expect(componentNativeElement.textContent).toBe('Loading...');
});

it('should render suspense template when initial observable is in suspense state', fakeAsync(() => {
letDirectiveTestComponent.value$ = of('component').pipe(delay(100));
fixtureLetDirectiveTestComponent.detectChanges();
expect(componentNativeElement.textContent).toBe('Loading...');
tick(100);
fixtureLetDirectiveTestComponent.detectChanges();
expect(componentNativeElement.textContent).toBe('component');
}));

it('should render suspense template when next observable is in suspense state', fakeAsync(() => {
letDirectiveTestComponent.value$ = new BehaviorSubject('ngrx');
fixtureLetDirectiveTestComponent.detectChanges();
letDirectiveTestComponent.value$ = timer(100).pipe(
switchMap(() => throwError(() => 'ERROR!'))
);
fixtureLetDirectiveTestComponent.detectChanges();
expect(componentNativeElement.textContent).toBe('Loading...');
tick(100);
fixtureLetDirectiveTestComponent.detectChanges();
expect(componentNativeElement.textContent).toBe('undefined');
}));
});

describe('when rendering recursively', () => {
beforeEach(waitForAsync(setupLetDirectiveTestRecursionComponent));

Expand Down

0 comments on commit 345ee53

Please sign in to comment.