Skip to content

Commit

Permalink
feat(effects): add ability to create functional effects (#3669)
Browse files Browse the repository at this point in the history
Closes #3668
  • Loading branch information
markostanimirovic authored Jan 25, 2023
1 parent a170189 commit dd76c63
Show file tree
Hide file tree
Showing 20 changed files with 987 additions and 153 deletions.
134 changes: 122 additions & 12 deletions modules/effects/spec/effect_creator.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { of } from 'rxjs';
import { forkJoin, of } from 'rxjs';
import { createEffect, getCreateEffectMetadata } from '../src/effect_creator';

describe('createEffect()', () => {
Expand All @@ -12,7 +12,7 @@ describe('createEffect()', () => {
const effect = createEffect(() => of({ type: 'a' }));

expect(effect['__@ngrx/effects_create__']).toEqual(
jasmine.objectContaining({ dispatch: true })
expect.objectContaining({ dispatch: true })
);
});

Expand All @@ -22,7 +22,7 @@ describe('createEffect()', () => {
});

expect(effect['__@ngrx/effects_create__']).toEqual(
jasmine.objectContaining({ dispatch: true })
expect.objectContaining({ dispatch: true })
);
});

Expand All @@ -32,7 +32,7 @@ describe('createEffect()', () => {
});

expect(effect['__@ngrx/effects_create__']).toEqual(
jasmine.objectContaining({ dispatch: false })
expect.objectContaining({ dispatch: false })
);
});

Expand All @@ -42,7 +42,78 @@ describe('createEffect()', () => {
});

expect(effect['__@ngrx/effects_create__']).toEqual(
jasmine.objectContaining({ dispatch: false })
expect.objectContaining({ dispatch: false })
);
});

it('should create a non-functional effect by default', () => {
const obs$ = of({ type: 'a' });
const effect = createEffect(() => obs$);

expect(effect).toBe(obs$);
expect(effect['__@ngrx/effects_create__']).toEqual(
expect.objectContaining({ functional: false })
);
});

it('should be possible to explicitly create a non-functional effect', () => {
const obs$ = of({ type: 'a' });
const effect = createEffect(() => obs$, { functional: false });

expect(effect).toBe(obs$);
expect(effect['__@ngrx/effects_create__']).toEqual(
expect.objectContaining({ functional: false })
);
});

it('should be possible to create a functional effect', () => {
const source = () => of({ type: 'a' });
const effect = createEffect(source, { functional: true });

expect(effect).toBe(source);
expect(effect['__@ngrx/effects_create__']).toEqual(
expect.objectContaining({ functional: true })
);
});

it('should be possible to invoke functional effect as function', (done) => {
const sum = createEffect((x = 10, y = 20) => of(x + y), {
functional: true,
dispatch: false,
});

forkJoin([sum(), sum(100, 200)]).subscribe(([defaultResult, result]) => {
expect(defaultResult).toBe(30);
expect(result).toBe(300);
done();
});
});

it('should use effects error handler by default', () => {
const effect = createEffect(() => of({ type: 'a' }));

expect(effect['__@ngrx/effects_create__']).toEqual(
expect.objectContaining({ useEffectsErrorHandler: true })
);
});

it('should be possible to explicitly create an effect with error handler', () => {
const effect = createEffect(() => of({ type: 'a' }), {
useEffectsErrorHandler: true,
});

expect(effect['__@ngrx/effects_create__']).toEqual(
expect.objectContaining({ useEffectsErrorHandler: true })
);
});

it('should be possible to create an effect without error handler', () => {
const effect = createEffect(() => of({ type: 'a' }), {
useEffectsErrorHandler: false,
});

expect(effect['__@ngrx/effects_create__']).toEqual(
expect.objectContaining({ useEffectsErrorHandler: false })
);
});

Expand All @@ -54,12 +125,15 @@ describe('createEffect()', () => {
c = createEffect(() => of({ type: 'c' }), { dispatch: false });
d = createEffect(() => of({ type: 'd' }), {
useEffectsErrorHandler: true,
functional: false,
});
e = createEffect(() => of({ type: 'd' }), {
useEffectsErrorHandler: false,
functional: true,
});
f = createEffect(() => of({ type: 'e' }), {
dispatch: false,
functional: true,
useEffectsErrorHandler: false,
});
g = createEffect(() => of({ type: 'e' }), {
Expand All @@ -71,18 +145,54 @@ describe('createEffect()', () => {
const mock = new Fixture();

expect(getCreateEffectMetadata(mock)).toEqual([
{ propertyName: 'a', dispatch: true, useEffectsErrorHandler: true },
{ propertyName: 'b', dispatch: true, useEffectsErrorHandler: true },
{ propertyName: 'c', dispatch: false, useEffectsErrorHandler: true },
{ propertyName: 'd', dispatch: true, useEffectsErrorHandler: true },
{ propertyName: 'e', dispatch: true, useEffectsErrorHandler: false },
{ propertyName: 'f', dispatch: false, useEffectsErrorHandler: false },
{ propertyName: 'g', dispatch: true, useEffectsErrorHandler: false },
{
propertyName: 'a',
dispatch: true,
functional: false,
useEffectsErrorHandler: true,
},
{
propertyName: 'b',
dispatch: true,
functional: false,
useEffectsErrorHandler: true,
},
{
propertyName: 'c',
dispatch: false,
functional: false,
useEffectsErrorHandler: true,
},
{
propertyName: 'd',
dispatch: true,
functional: false,
useEffectsErrorHandler: true,
},
{
propertyName: 'e',
dispatch: true,
functional: true,
useEffectsErrorHandler: false,
},
{
propertyName: 'f',
dispatch: false,
functional: true,
useEffectsErrorHandler: false,
},
{
propertyName: 'g',
dispatch: true,
functional: false,
useEffectsErrorHandler: false,
},
]);
});

it('should return an empty array if the effect has not been created with createEffect()', () => {
const fakeCreateEffect: any = () => {};

class Fixture {
a = fakeCreateEffect(() => of({ type: 'A' }));
b = new Proxy(
Expand Down
50 changes: 45 additions & 5 deletions modules/effects/spec/effect_sources.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,16 +201,35 @@ describe('EffectSources', () => {
}
}

it('should resolve effects from instances', () => {
const sources$ = cold('--a--', { a: new SourceA() });
const expected = cold('--a--', { a });
const recordA = {
a: createEffect(() => alwaysOf(a), { functional: true }),
};
const recordB = {
b: createEffect(() => alwaysOf(b), { functional: true }),
};

it('should resolve effects from class instances', () => {
const sources$ = cold('--a--b--', {
a: new SourceA(),
b: new SourceB(),
});
const expected = cold('--a--b--', { a, b });

const output = toActions(sources$);

expect(output).toBeObservable(expected);
});

it('should resolve effects from records', () => {
const sources$ = cold('--a--b--', { a: recordA, b: recordB });
const expected = cold('--a--b--', { a, b });

const output = toActions(sources$);

expect(output).toBeObservable(expected);
});

it('should ignore duplicate sources', () => {
it('should ignore duplicate class instances', () => {
const sources$ = cold('--a--a--a--', {
a: new SourceA(),
});
Expand All @@ -221,6 +240,27 @@ describe('EffectSources', () => {
expect(output).toBeObservable(expected);
});

it('should ignore different instances of the same class', () => {
const sources$ = cold('--a--b--', {
a: new SourceA(),
b: new SourceA(),
});
const expected = cold('--a-----', { a });

const output = toActions(sources$);

expect(output).toBeObservable(expected);
});

it('should ignore duplicate records', () => {
const sources$ = cold('--a--b--', { a: recordA, b: recordA });
const expected = cold('--a-----', { a });

const output = toActions(sources$);

expect(output).toBeObservable(expected);
});

it('should resolve effects with different identifiers', () => {
const sources$ = cold('--a--b--c--', {
a: new SourceWithIdentifier('a'),
Expand Down Expand Up @@ -264,7 +304,7 @@ describe('EffectSources', () => {
expect(output).toBeObservable(expected);
});

it('should start with an action after being registered with OnInitEffects', () => {
it('should start with an action after being registered with OnInitEffects', () => {
const sources$ = cold('--a--', {
a: new SourceWithInitAction(new Subject()),
});
Expand Down
4 changes: 2 additions & 2 deletions modules/effects/spec/effects_feature_module.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { map, withLatestFrom } from 'rxjs/operators';
import { Actions, EffectsModule, ofType, createEffect } from '../';
import { EffectsFeatureModule } from '../src/effects_feature_module';
import { EffectsRootModule } from '../src/effects_root_module';
import { FEATURE_EFFECTS } from '../src/tokens';
import { _FEATURE_EFFECTS_INSTANCE_GROUPS } from '../src/tokens';

describe('Effects Feature Module', () => {
describe('when registered', () => {
Expand All @@ -33,7 +33,7 @@ describe('Effects Feature Module', () => {
},
},
{
provide: FEATURE_EFFECTS,
provide: _FEATURE_EFFECTS_INSTANCE_GROUPS,
useValue: effectSourceGroups,
},
EffectsFeatureModule,
Expand Down
5 changes: 5 additions & 0 deletions modules/effects/spec/effects_metadata.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ describe('Effects metadata', () => {
class Fixture {
effectSimple = createEffect(() => of({ type: 'a' }));
effectNoDispatch = createEffect(() => of({ type: 'a' }), {
functional: true,
dispatch: false,
});
noEffect: any;
Expand All @@ -27,21 +28,25 @@ describe('Effects metadata', () => {
{
propertyName: 'effectSimple',
dispatch: true,
functional: false,
useEffectsErrorHandler: true,
},
{
propertyName: 'effectNoDispatch',
dispatch: false,
functional: true,
useEffectsErrorHandler: true,
},
{
propertyName: 'effectWithMethod',
dispatch: true,
functional: false,
useEffectsErrorHandler: true,
},
{
propertyName: 'effectWithUseEffectsErrorHandler',
dispatch: true,
functional: false,
useEffectsErrorHandler: false,
},
];
Expand Down
51 changes: 49 additions & 2 deletions modules/effects/spec/integration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { Router } from '@angular/router';
import { Action, StoreModule, INIT } from '@ngrx/store';
import { concat, exhaustMap, map, NEVER, Observable, of, tap } from 'rxjs';
import {
EffectsModule,
OnInitEffects,
Expand All @@ -13,8 +14,6 @@ import {
USER_PROVIDED_EFFECTS,
} from '..';
import { ofType, createEffect, OnRunEffects, EffectNotification } from '../src';
import { map, exhaustMap, tap } from 'rxjs/operators';
import { Observable } from 'rxjs';

describe('NgRx Effects Integration spec', () => {
it('throws if forRoot() with Effects is used more than once', (done: any) => {
Expand Down Expand Up @@ -66,6 +65,54 @@ describe('NgRx Effects Integration spec', () => {
});
});

it('runs provided class and functional effects', () => {
const obs$ = concat(of('ngrx'), NEVER);
const classEffectRun = jest.fn<void, []>();
const functionalEffectRun = jest.fn<void, []>();
const classEffect$ = createEffect(() => obs$.pipe(tap(classEffectRun)), {
dispatch: false,
});
const functionalEffect = createEffect(
() => obs$.pipe(tap(functionalEffectRun)),
{
functional: true,
dispatch: false,
}
);

class ClassEffects1 {
classEffect$ = classEffect$;
}

class ClassEffects2 {
classEffect$ = classEffect$;
}

const functionalEffects1 = { functionalEffect };
const functionalEffects2 = { functionalEffect };

TestBed.configureTestingModule({
imports: [
StoreModule.forRoot(),
EffectsModule.forRoot(ClassEffects1, functionalEffects1),
EffectsModule.forFeature(
ClassEffects1,
functionalEffects2,
ClassEffects2
),
EffectsModule.forFeature(
functionalEffects1,
functionalEffects2,
ClassEffects2
),
],
});
TestBed.inject(EffectSources);

expect(classEffectRun).toHaveBeenCalledTimes(2);
expect(functionalEffectRun).toHaveBeenCalledTimes(2);
});

describe('actions', () => {
const createDispatchedReducer =
(dispatchedActions: string[] = []) =>
Expand Down
Loading

0 comments on commit dd76c63

Please sign in to comment.