Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(effects): add ability to create functional effects #3669

Merged
merged 5 commits into from
Jan 25, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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