Skip to content

Commit dd76c63

Browse files
feat(effects): add ability to create functional effects (#3669)
Closes #3668
1 parent a170189 commit dd76c63

20 files changed

+987
-153
lines changed

modules/effects/spec/effect_creator.spec.ts

Lines changed: 122 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { of } from 'rxjs';
1+
import { forkJoin, of } from 'rxjs';
22
import { createEffect, getCreateEffectMetadata } from '../src/effect_creator';
33

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

1414
expect(effect['__@ngrx/effects_create__']).toEqual(
15-
jasmine.objectContaining({ dispatch: true })
15+
expect.objectContaining({ dispatch: true })
1616
);
1717
});
1818

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

2424
expect(effect['__@ngrx/effects_create__']).toEqual(
25-
jasmine.objectContaining({ dispatch: true })
25+
expect.objectContaining({ dispatch: true })
2626
);
2727
});
2828

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

3434
expect(effect['__@ngrx/effects_create__']).toEqual(
35-
jasmine.objectContaining({ dispatch: false })
35+
expect.objectContaining({ dispatch: false })
3636
);
3737
});
3838

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

4444
expect(effect['__@ngrx/effects_create__']).toEqual(
45-
jasmine.objectContaining({ dispatch: false })
45+
expect.objectContaining({ dispatch: false })
46+
);
47+
});
48+
49+
it('should create a non-functional effect by default', () => {
50+
const obs$ = of({ type: 'a' });
51+
const effect = createEffect(() => obs$);
52+
53+
expect(effect).toBe(obs$);
54+
expect(effect['__@ngrx/effects_create__']).toEqual(
55+
expect.objectContaining({ functional: false })
56+
);
57+
});
58+
59+
it('should be possible to explicitly create a non-functional effect', () => {
60+
const obs$ = of({ type: 'a' });
61+
const effect = createEffect(() => obs$, { functional: false });
62+
63+
expect(effect).toBe(obs$);
64+
expect(effect['__@ngrx/effects_create__']).toEqual(
65+
expect.objectContaining({ functional: false })
66+
);
67+
});
68+
69+
it('should be possible to create a functional effect', () => {
70+
const source = () => of({ type: 'a' });
71+
const effect = createEffect(source, { functional: true });
72+
73+
expect(effect).toBe(source);
74+
expect(effect['__@ngrx/effects_create__']).toEqual(
75+
expect.objectContaining({ functional: true })
76+
);
77+
});
78+
79+
it('should be possible to invoke functional effect as function', (done) => {
80+
const sum = createEffect((x = 10, y = 20) => of(x + y), {
81+
functional: true,
82+
dispatch: false,
83+
});
84+
85+
forkJoin([sum(), sum(100, 200)]).subscribe(([defaultResult, result]) => {
86+
expect(defaultResult).toBe(30);
87+
expect(result).toBe(300);
88+
done();
89+
});
90+
});
91+
92+
it('should use effects error handler by default', () => {
93+
const effect = createEffect(() => of({ type: 'a' }));
94+
95+
expect(effect['__@ngrx/effects_create__']).toEqual(
96+
expect.objectContaining({ useEffectsErrorHandler: true })
97+
);
98+
});
99+
100+
it('should be possible to explicitly create an effect with error handler', () => {
101+
const effect = createEffect(() => of({ type: 'a' }), {
102+
useEffectsErrorHandler: true,
103+
});
104+
105+
expect(effect['__@ngrx/effects_create__']).toEqual(
106+
expect.objectContaining({ useEffectsErrorHandler: true })
107+
);
108+
});
109+
110+
it('should be possible to create an effect without error handler', () => {
111+
const effect = createEffect(() => of({ type: 'a' }), {
112+
useEffectsErrorHandler: false,
113+
});
114+
115+
expect(effect['__@ngrx/effects_create__']).toEqual(
116+
expect.objectContaining({ useEffectsErrorHandler: false })
46117
);
47118
});
48119

@@ -54,12 +125,15 @@ describe('createEffect()', () => {
54125
c = createEffect(() => of({ type: 'c' }), { dispatch: false });
55126
d = createEffect(() => of({ type: 'd' }), {
56127
useEffectsErrorHandler: true,
128+
functional: false,
57129
});
58130
e = createEffect(() => of({ type: 'd' }), {
59131
useEffectsErrorHandler: false,
132+
functional: true,
60133
});
61134
f = createEffect(() => of({ type: 'e' }), {
62135
dispatch: false,
136+
functional: true,
63137
useEffectsErrorHandler: false,
64138
});
65139
g = createEffect(() => of({ type: 'e' }), {
@@ -71,18 +145,54 @@ describe('createEffect()', () => {
71145
const mock = new Fixture();
72146

73147
expect(getCreateEffectMetadata(mock)).toEqual([
74-
{ propertyName: 'a', dispatch: true, useEffectsErrorHandler: true },
75-
{ propertyName: 'b', dispatch: true, useEffectsErrorHandler: true },
76-
{ propertyName: 'c', dispatch: false, useEffectsErrorHandler: true },
77-
{ propertyName: 'd', dispatch: true, useEffectsErrorHandler: true },
78-
{ propertyName: 'e', dispatch: true, useEffectsErrorHandler: false },
79-
{ propertyName: 'f', dispatch: false, useEffectsErrorHandler: false },
80-
{ propertyName: 'g', dispatch: true, useEffectsErrorHandler: false },
148+
{
149+
propertyName: 'a',
150+
dispatch: true,
151+
functional: false,
152+
useEffectsErrorHandler: true,
153+
},
154+
{
155+
propertyName: 'b',
156+
dispatch: true,
157+
functional: false,
158+
useEffectsErrorHandler: true,
159+
},
160+
{
161+
propertyName: 'c',
162+
dispatch: false,
163+
functional: false,
164+
useEffectsErrorHandler: true,
165+
},
166+
{
167+
propertyName: 'd',
168+
dispatch: true,
169+
functional: false,
170+
useEffectsErrorHandler: true,
171+
},
172+
{
173+
propertyName: 'e',
174+
dispatch: true,
175+
functional: true,
176+
useEffectsErrorHandler: false,
177+
},
178+
{
179+
propertyName: 'f',
180+
dispatch: false,
181+
functional: true,
182+
useEffectsErrorHandler: false,
183+
},
184+
{
185+
propertyName: 'g',
186+
dispatch: true,
187+
functional: false,
188+
useEffectsErrorHandler: false,
189+
},
81190
]);
82191
});
83192

84193
it('should return an empty array if the effect has not been created with createEffect()', () => {
85194
const fakeCreateEffect: any = () => {};
195+
86196
class Fixture {
87197
a = fakeCreateEffect(() => of({ type: 'A' }));
88198
b = new Proxy(

modules/effects/spec/effect_sources.spec.ts

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -201,16 +201,35 @@ describe('EffectSources', () => {
201201
}
202202
}
203203

204-
it('should resolve effects from instances', () => {
205-
const sources$ = cold('--a--', { a: new SourceA() });
206-
const expected = cold('--a--', { a });
204+
const recordA = {
205+
a: createEffect(() => alwaysOf(a), { functional: true }),
206+
};
207+
const recordB = {
208+
b: createEffect(() => alwaysOf(b), { functional: true }),
209+
};
210+
211+
it('should resolve effects from class instances', () => {
212+
const sources$ = cold('--a--b--', {
213+
a: new SourceA(),
214+
b: new SourceB(),
215+
});
216+
const expected = cold('--a--b--', { a, b });
217+
218+
const output = toActions(sources$);
219+
220+
expect(output).toBeObservable(expected);
221+
});
222+
223+
it('should resolve effects from records', () => {
224+
const sources$ = cold('--a--b--', { a: recordA, b: recordB });
225+
const expected = cold('--a--b--', { a, b });
207226

208227
const output = toActions(sources$);
209228

210229
expect(output).toBeObservable(expected);
211230
});
212231

213-
it('should ignore duplicate sources', () => {
232+
it('should ignore duplicate class instances', () => {
214233
const sources$ = cold('--a--a--a--', {
215234
a: new SourceA(),
216235
});
@@ -221,6 +240,27 @@ describe('EffectSources', () => {
221240
expect(output).toBeObservable(expected);
222241
});
223242

243+
it('should ignore different instances of the same class', () => {
244+
const sources$ = cold('--a--b--', {
245+
a: new SourceA(),
246+
b: new SourceA(),
247+
});
248+
const expected = cold('--a-----', { a });
249+
250+
const output = toActions(sources$);
251+
252+
expect(output).toBeObservable(expected);
253+
});
254+
255+
it('should ignore duplicate records', () => {
256+
const sources$ = cold('--a--b--', { a: recordA, b: recordA });
257+
const expected = cold('--a-----', { a });
258+
259+
const output = toActions(sources$);
260+
261+
expect(output).toBeObservable(expected);
262+
});
263+
224264
it('should resolve effects with different identifiers', () => {
225265
const sources$ = cold('--a--b--c--', {
226266
a: new SourceWithIdentifier('a'),
@@ -264,7 +304,7 @@ describe('EffectSources', () => {
264304
expect(output).toBeObservable(expected);
265305
});
266306

267-
it('should start with an action after being registered with OnInitEffects', () => {
307+
it('should start with an action after being registered with OnInitEffects', () => {
268308
const sources$ = cold('--a--', {
269309
a: new SourceWithInitAction(new Subject()),
270310
});

modules/effects/spec/effects_feature_module.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { map, withLatestFrom } from 'rxjs/operators';
1212
import { Actions, EffectsModule, ofType, createEffect } from '../';
1313
import { EffectsFeatureModule } from '../src/effects_feature_module';
1414
import { EffectsRootModule } from '../src/effects_root_module';
15-
import { FEATURE_EFFECTS } from '../src/tokens';
15+
import { _FEATURE_EFFECTS_INSTANCE_GROUPS } from '../src/tokens';
1616

1717
describe('Effects Feature Module', () => {
1818
describe('when registered', () => {
@@ -33,7 +33,7 @@ describe('Effects Feature Module', () => {
3333
},
3434
},
3535
{
36-
provide: FEATURE_EFFECTS,
36+
provide: _FEATURE_EFFECTS_INSTANCE_GROUPS,
3737
useValue: effectSourceGroups,
3838
},
3939
EffectsFeatureModule,

modules/effects/spec/effects_metadata.spec.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ describe('Effects metadata', () => {
99
class Fixture {
1010
effectSimple = createEffect(() => of({ type: 'a' }));
1111
effectNoDispatch = createEffect(() => of({ type: 'a' }), {
12+
functional: true,
1213
dispatch: false,
1314
});
1415
noEffect: any;
@@ -27,21 +28,25 @@ describe('Effects metadata', () => {
2728
{
2829
propertyName: 'effectSimple',
2930
dispatch: true,
31+
functional: false,
3032
useEffectsErrorHandler: true,
3133
},
3234
{
3335
propertyName: 'effectNoDispatch',
3436
dispatch: false,
37+
functional: true,
3538
useEffectsErrorHandler: true,
3639
},
3740
{
3841
propertyName: 'effectWithMethod',
3942
dispatch: true,
43+
functional: false,
4044
useEffectsErrorHandler: true,
4145
},
4246
{
4347
propertyName: 'effectWithUseEffectsErrorHandler',
4448
dispatch: true,
49+
functional: false,
4550
useEffectsErrorHandler: false,
4651
},
4752
];

modules/effects/spec/integration.spec.ts

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { TestBed } from '@angular/core/testing';
33
import { RouterTestingModule } from '@angular/router/testing';
44
import { Router } from '@angular/router';
55
import { Action, StoreModule, INIT } from '@ngrx/store';
6+
import { concat, exhaustMap, map, NEVER, Observable, of, tap } from 'rxjs';
67
import {
78
EffectsModule,
89
OnInitEffects,
@@ -13,8 +14,6 @@ import {
1314
USER_PROVIDED_EFFECTS,
1415
} from '..';
1516
import { ofType, createEffect, OnRunEffects, EffectNotification } from '../src';
16-
import { map, exhaustMap, tap } from 'rxjs/operators';
17-
import { Observable } from 'rxjs';
1817

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

68+
it('runs provided class and functional effects', () => {
69+
const obs$ = concat(of('ngrx'), NEVER);
70+
const classEffectRun = jest.fn<void, []>();
71+
const functionalEffectRun = jest.fn<void, []>();
72+
const classEffect$ = createEffect(() => obs$.pipe(tap(classEffectRun)), {
73+
dispatch: false,
74+
});
75+
const functionalEffect = createEffect(
76+
() => obs$.pipe(tap(functionalEffectRun)),
77+
{
78+
functional: true,
79+
dispatch: false,
80+
}
81+
);
82+
83+
class ClassEffects1 {
84+
classEffect$ = classEffect$;
85+
}
86+
87+
class ClassEffects2 {
88+
classEffect$ = classEffect$;
89+
}
90+
91+
const functionalEffects1 = { functionalEffect };
92+
const functionalEffects2 = { functionalEffect };
93+
94+
TestBed.configureTestingModule({
95+
imports: [
96+
StoreModule.forRoot(),
97+
EffectsModule.forRoot(ClassEffects1, functionalEffects1),
98+
EffectsModule.forFeature(
99+
ClassEffects1,
100+
functionalEffects2,
101+
ClassEffects2
102+
),
103+
EffectsModule.forFeature(
104+
functionalEffects1,
105+
functionalEffects2,
106+
ClassEffects2
107+
),
108+
],
109+
});
110+
TestBed.inject(EffectSources);
111+
112+
expect(classEffectRun).toHaveBeenCalledTimes(2);
113+
expect(functionalEffectRun).toHaveBeenCalledTimes(2);
114+
});
115+
69116
describe('actions', () => {
70117
const createDispatchedReducer =
71118
(dispatchedActions: string[] = []) =>

0 commit comments

Comments
 (0)