Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions modules/signals/events/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './src/index';
5 changes: 5 additions & 0 deletions modules/signals/events/ng-package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"lib": {
"entryFile": "index.ts"
}
}
48 changes: 48 additions & 0 deletions modules/signals/events/spec/dispatcher.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { TestBed } from '@angular/core/testing';
import { take } from 'rxjs';
import { Dispatcher, eventCreator, Events, props } from '../src';
import { ReducerEvents } from '../src/events';

describe('Dispatcher', () => {
it('is provided at the root level', () => {
const dispatcher = TestBed.inject(Dispatcher);
expect(dispatcher).toBeDefined();
});

it('emits dispatched events to the ReducerEvents service before the Events service', () => {
const dispatcher = TestBed.inject(Dispatcher);
const events = TestBed.inject(Events);
const reducerEvents = TestBed.inject(ReducerEvents);
const set = eventCreator('set', props<{ count: number }>());
const result: Array<ReturnType<typeof set> & { order: number }> = [];

events
.on(set)
.pipe(take(1))
.subscribe((event) => result.push({ ...event, order: 2 }));
reducerEvents
.on(set)
.pipe(take(1))
.subscribe((event) => result.push({ ...event, order: 1 }));

dispatcher.dispatch(set({ count: 10 }));

expect(result).toEqual([
{ type: 'set', count: 10, order: 1 },
{ type: 'set', count: 10, order: 2 },
]);
});

it('displays a warning when event creator is dispatched', () => {
const dispatcher = TestBed.inject(Dispatcher);
const increment = eventCreator('increment');
vitest.spyOn(console, 'warn').mockImplementation(() => {});

dispatcher.dispatch(increment);

expect(console.warn).toHaveBeenCalledWith(
'@ngrx/signals/events: Event creator should not be dispatched.',
'Did you forget to call it?'
);
});
});
64 changes: 64 additions & 0 deletions modules/signals/events/spec/event-creator-group.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { type } from '@ngrx/signals';
import {
emptyProps,
EventCreator,
eventCreatorGroup,
EventCreatorWithProps,
props,
} from '../src';

describe('eventCreatorGroup', () => {
it('creates an event creator group', () => {
const counterPageEvents = eventCreatorGroup({
source: 'Counter Page',
events: {
increment: emptyProps(),
decrement: emptyProps(),
set: props<{ count: number }>(),
},
});

const incrementEvent = counterPageEvents.increment();
const decrementEvent = counterPageEvents.decrement();
const setEvent = counterPageEvents.set({ count: 10 });

expect(incrementEvent).toEqual({ type: '[Counter Page] increment' });
expect(decrementEvent).toEqual({ type: '[Counter Page] decrement' });
expect(setEvent).toEqual({ type: '[Counter Page] set', count: 10 });
});

it('allows creating custom event creator group factories', () => {
function apiEventCreatorGroup<Source extends string, Entity>(
source: Source,
_entity: Entity
): {
loadedSuccess: EventCreatorWithProps<
`[${Source} API] loadedSuccess`,
{ entities: Entity[] }
>;
loadedFailure: EventCreator<`[${Source} API] loadedFailure`>;
} {
return eventCreatorGroup({
source: `${source} API`,
events: {
loadedSuccess: props<{ entities: Entity[] }>(),
loadedFailure: emptyProps(),
},
});
}

type User = { id: number; name: string };
const usersApiEvents = apiEventCreatorGroup('Users', type<User>());

const loadedSuccessEvent = usersApiEvents.loadedSuccess({
entities: [{ id: 1, name: 'John Doe' }],
});
const loadedFailureEvent = usersApiEvents.loadedFailure();

expect(loadedSuccessEvent).toEqual({
type: '[Users API] loadedSuccess',
entities: [{ id: 1, name: 'John Doe' }],
});
expect(loadedFailureEvent).toEqual({ type: '[Users API] loadedFailure' });
});
});
53 changes: 53 additions & 0 deletions modules/signals/events/spec/event-creator.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { EventCreator, eventCreator, props } from '../src';

describe('eventCreator', () => {
it('creates an event creator without additional properties', () => {
const increment = eventCreator('increment');
const event = increment();

expect(event).toEqual({ type: 'increment' });
});

it('creates an event creator with additional properties', () => {
const set = eventCreator('set', props<{ count: number }>());
const event = set({ count: 10 });

expect(event).toEqual({ type: 'set', count: 10 });
});

it('displays a warning when event props are an array', () => {
const set = eventCreator('set', props<number[]>());
vitest.spyOn(console, 'warn').mockImplementation(() => {});

set([1, 2, 3]);

expect(console.warn).toHaveBeenCalledWith(
'@ngrx/signals/events: Event props cannot be an array.'
);
});

it('displays a warning when event props contain a type property', () => {
const set = eventCreator('set', props<{ type: number }>());
vitest.spyOn(console, 'warn').mockImplementation(() => {});

set({ type: 10 });

expect(console.warn).toHaveBeenCalledWith(
'@ngrx/signals/events: Event props cannot contain a type property.'
);
});

it('allows creating custom event creator factories', () => {
function formattedEventCreator<Source extends string, Event extends string>(
source: Source,
event: Event
): EventCreator<`[${Source}] ${Event}`> {
return eventCreator(`[${source}] ${event}`);
}

const increment = formattedEventCreator('Counter Page', 'Increment');
const event = increment();

expect(event).toEqual({ type: '[Counter Page] Increment' });
});
});
70 changes: 70 additions & 0 deletions modules/signals/events/spec/events.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { TestBed } from '@angular/core/testing';
import { Dispatcher, Event, eventCreator, Events, props } from '../src';
import { SOURCE_TYPE } from '../src/events';

describe('Events', () => {
it('is provided at the root level', () => {
const events = TestBed.inject(Events);
expect(events).toBeDefined();
});

describe('on', () => {
const foo = eventCreator('foo');
const bar = eventCreator('bar', props<{ value: number }>());
const baz = eventCreator('baz');

it('emits events matching the provided event creators', () => {
const events = TestBed.inject(Events);
const dispatcher = TestBed.inject(Dispatcher);
const emittedEvents: Event[] = [];

events.on(foo, bar).subscribe((event) => emittedEvents.push(event));

dispatcher.dispatch(bar({ value: 10 }));
dispatcher.dispatch(foo());
dispatcher.dispatch(baz());
dispatcher.dispatch(bar({ value: 100 }));

expect(emittedEvents).toEqual([
{ type: 'bar', value: 10 },
{ type: 'foo' },
{ type: 'bar', value: 100 },
]);
});

it('emits all events when called without arguments', () => {
const events = TestBed.inject(Events);
const dispatcher = TestBed.inject(Dispatcher);
const emittedEvents: Event[] = [];

events.on().subscribe((event) => emittedEvents.push(event));

dispatcher.dispatch(foo());
dispatcher.dispatch(bar({ value: 10 }));
dispatcher.dispatch(baz());
dispatcher.dispatch(foo());

expect(emittedEvents).toEqual([
{ type: 'foo' },
{ type: 'bar', value: 10 },
{ type: 'baz' },
{ type: 'foo' },
]);
});

it('adds SOURCE_TYPE to emitted events', () => {
const events = TestBed.inject(Events);
const dispatcher = TestBed.inject(Dispatcher);
const sourceTypes: string[] = [];

events
.on()
.subscribe((event) => sourceTypes.push((event as any)[SOURCE_TYPE]));

dispatcher.dispatch(foo());
dispatcher.dispatch(bar({ value: 10 }));

expect(sourceTypes).toEqual(['foo', 'bar']);
});
});
});
57 changes: 57 additions & 0 deletions modules/signals/events/spec/inject-dispatch.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { EnvironmentInjector } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import {
Dispatcher,
emptyProps,
eventCreator,
eventCreatorGroup,
injectDispatch,
props,
} from '../src';

describe('injectDispatch', () => {
it('creates self-dispatching events', () => {
const counterPageEvents = eventCreatorGroup({
source: 'Counter Page',
events: {
increment: emptyProps(),
set: props<{ count: number }>(),
},
});
const dispatcher = TestBed.inject(Dispatcher);
const dispatch = TestBed.runInInjectionContext(() =>
injectDispatch(counterPageEvents)
);
vitest.spyOn(dispatcher, 'dispatch');

dispatch.increment();
expect(dispatcher.dispatch).toHaveBeenCalledWith({
type: '[Counter Page] increment',
});

dispatch.set({ count: 10 });
expect(dispatcher.dispatch).toHaveBeenCalledWith({
type: '[Counter Page] set',
count: 10,
});
});

it('creates self-dispatching events with a custom injector', () => {
const increment = eventCreator('increment');
const injector = TestBed.inject(EnvironmentInjector);
const dispatcher = TestBed.inject(Dispatcher);
const dispatch = injectDispatch({ increment }, { injector });
vitest.spyOn(dispatcher, 'dispatch');

dispatch.increment();
expect(dispatcher.dispatch).toHaveBeenCalledWith({ type: 'increment' });
});

it('throws an error when called outside of an injection context', () => {
const increment = eventCreator('increment');

expect(() => injectDispatch({ increment })).toThrowError(
'injectDispatch() can only be used within an injection context'
);
});
});
Loading
Loading