Skip to content

Commit 2cdecb3

Browse files
feat(store): add createActionGroup function (#3381)
Closes #3337
1 parent 9fcd553 commit 2cdecb3

File tree

5 files changed

+616
-0
lines changed

5 files changed

+616
-0
lines changed
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { createActionGroup, emptyProps, props } from '@ngrx/store';
2+
3+
describe('createActionGroup', () => {
4+
const authApiActions = createActionGroup({
5+
source: 'Auth API',
6+
events: {
7+
'Login Success': props<{ userId: number; token: string }>(),
8+
'Login Failure': props<{ error: string }>(),
9+
'Logout Success': emptyProps(),
10+
'Logout Failure': (error: Error) => ({ error }),
11+
},
12+
});
13+
const booksApiActions = createActionGroup({
14+
source: 'Books API',
15+
events: {
16+
' Load BOOKS suCCess ': emptyProps(),
17+
},
18+
});
19+
20+
it('should create action name by camel casing the event name', () => {
21+
expect(booksApiActions.loadBooksSuccess).toBeDefined();
22+
});
23+
24+
it('should create action type using the "[Source] Event" pattern', () => {
25+
expect(booksApiActions.loadBooksSuccess().type).toBe(
26+
'[Books API] Load BOOKS suCCess '
27+
);
28+
});
29+
30+
it('should create action with props', () => {
31+
const loginSuccess = authApiActions.loginSuccess({
32+
userId: 10,
33+
token: 'ngrx',
34+
});
35+
expect(loginSuccess).toEqual({
36+
type: '[Auth API] Login Success',
37+
userId: 10,
38+
token: 'ngrx',
39+
});
40+
41+
const loginFailure = authApiActions.loginFailure({
42+
error: 'Login Failure!',
43+
});
44+
expect(loginFailure).toEqual({
45+
type: '[Auth API] Login Failure',
46+
error: 'Login Failure!',
47+
});
48+
});
49+
50+
it('should create action without props', () => {
51+
const logoutSuccess = authApiActions.logoutSuccess();
52+
expect(logoutSuccess).toEqual({ type: '[Auth API] Logout Success' });
53+
});
54+
55+
it('should create action with props factory', () => {
56+
const error = new Error('Logout Failure!');
57+
const logoutFailure = authApiActions.logoutFailure(error);
58+
expect(logoutFailure).toEqual({
59+
type: '[Auth API] Logout Failure',
60+
error,
61+
});
62+
});
63+
});
Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
import { Expect, expecter } from 'ts-snippet';
2+
import { compilerOptions } from './utils';
3+
4+
describe('createActionGroup', () => {
5+
const snippetFactory = (code: string): string => `
6+
import { createActionGroup, emptyProps, props } from '@ngrx/store';
7+
8+
${code}
9+
`;
10+
11+
function testWith(expectSnippet: (code: string) => Expect): void {
12+
it('should create action group', () => {
13+
const snippet = expectSnippet(`
14+
const authApiActions = createActionGroup({
15+
source: 'Auth API',
16+
events: {
17+
'Login Success': props<{ userId: number; token: string }>(),
18+
'Login Failure': props<{ error: string }>(),
19+
'Logout Success': emptyProps(),
20+
'Logout Failure': (error: Error) => ({ error }),
21+
},
22+
});
23+
24+
let loginSuccess: typeof authApiActions.loginSuccess;
25+
let loginFailure: typeof authApiActions.loginFailure;
26+
let logoutSuccess: typeof authApiActions.logoutSuccess;
27+
let logoutFailure: typeof authApiActions.logoutFailure;
28+
`);
29+
30+
snippet.toInfer(
31+
'loginSuccess',
32+
`ActionCreator<
33+
"[Auth API] Login Success",
34+
(props: { userId: number; token: string; }) =>
35+
{ userId: number; token: string; } & TypedAction<"[Auth API] Login Success">
36+
>`
37+
);
38+
snippet.toInfer(
39+
'loginFailure',
40+
`ActionCreator<
41+
"[Auth API] Login Failure",
42+
(props: { error: string; }) =>
43+
{ error: string; } & TypedAction<"[Auth API] Login Failure">
44+
>`
45+
);
46+
snippet.toInfer(
47+
'logoutSuccess',
48+
`ActionCreator<
49+
"[Auth API] Logout Success",
50+
() => TypedAction<"[Auth API] Logout Success">
51+
>`
52+
);
53+
snippet.toInfer(
54+
'logoutFailure',
55+
`FunctionWithParametersType<
56+
[error: Error],
57+
{ error: Error; } & TypedAction<"[Auth API] Logout Failure">
58+
> & TypedAction<"[Auth API] Logout Failure">`
59+
);
60+
});
61+
62+
describe('source', () => {
63+
it('should fail when source is not a template literal type', () => {
64+
expectSnippet(`
65+
const booksApiActions = createActionGroup({
66+
source: 'Books API' as string,
67+
events: {},
68+
});
69+
`).toFail(/source must be a template literal type/);
70+
});
71+
});
72+
73+
describe('event name', () => {
74+
it('should create action name by camel casing the event name', () => {
75+
expectSnippet(`
76+
const booksApiActions = createActionGroup({
77+
source: 'Books API',
78+
events: {
79+
' Load BOOKS suCCess ': emptyProps(),
80+
},
81+
});
82+
83+
let loadBooksSuccess: typeof booksApiActions.loadBooksSuccess;
84+
`).toInfer(
85+
'loadBooksSuccess',
86+
`ActionCreator<
87+
"[Books API] Load BOOKS suCCess ",
88+
() => TypedAction<"[Books API] Load BOOKS suCCess ">
89+
>`
90+
);
91+
});
92+
93+
it('should fail when event name is an empty string', () => {
94+
expectSnippet(`
95+
const booksApiActions = createActionGroup({
96+
source: 'Books API',
97+
events: {
98+
'': emptyProps(),
99+
},
100+
});
101+
`).toFail(
102+
/event name cannot be an empty string or contain only spaces/
103+
);
104+
});
105+
106+
it('should fail when event name contains only spaces', () => {
107+
expectSnippet(`
108+
const booksApiActions = createActionGroup({
109+
source: 'Books API',
110+
events: {
111+
' ': emptyProps(),
112+
},
113+
});
114+
`).toFail(
115+
/event name cannot be an empty string or contain only spaces/
116+
);
117+
});
118+
119+
it('should fail when event name is not a template literal type', () => {
120+
expectSnippet(`
121+
const booksApiActions = createActionGroup({
122+
source: 'Books API',
123+
events: {
124+
['Load Books Success' as string]: emptyProps()
125+
},
126+
});
127+
`).toFail(/event name must be a template literal type/);
128+
});
129+
130+
describe('forbidden characters', () => {
131+
[
132+
String.raw`\\`,
133+
'/',
134+
'|',
135+
'<',
136+
'>',
137+
'[',
138+
']',
139+
'{',
140+
'}',
141+
'(',
142+
')',
143+
'.',
144+
',',
145+
'!',
146+
'?',
147+
'#',
148+
'%',
149+
'^',
150+
'&',
151+
'*',
152+
'+',
153+
'-',
154+
'~',
155+
'"',
156+
String.raw`\'`,
157+
'`',
158+
].forEach((char) => {
159+
it(`should fail when event name contains ${char} in the beginning`, () => {
160+
expectSnippet(`
161+
const booksApiActions = createActionGroup({
162+
source: 'Books API',
163+
events: {
164+
'${char}Load Books Success': emptyProps(),
165+
},
166+
});
167+
`).toFail(/event name cannot contain/);
168+
});
169+
170+
it(`should fail when event name contains ${char} in the middle`, () => {
171+
expectSnippet(`
172+
const booksApiActions = createActionGroup({
173+
source: 'Books API',
174+
events: {
175+
'Load Books ${char} Success': emptyProps(),
176+
},
177+
});
178+
`).toFail(/event name cannot contain/);
179+
});
180+
181+
it(`should fail when event name contains ${char} in the end`, () => {
182+
expectSnippet(`
183+
const booksApiActions = createActionGroup({
184+
source: 'Books API',
185+
events: {
186+
'Load Books Success${char}': emptyProps(),
187+
},
188+
});
189+
`).toFail(/event name cannot contain/);
190+
});
191+
});
192+
});
193+
194+
it('should fail when two event names are mapped to the same action name', () => {
195+
expectSnippet(`
196+
const booksApiActions = createActionGroup({
197+
source: 'Books API',
198+
events: {
199+
' Load BOOks success ': emptyProps(),
200+
'load Books Success': props<{ books: string[] }>(),
201+
}
202+
});
203+
`).toFail(/loadBooksSuccess action is already defined/);
204+
});
205+
});
206+
207+
describe('props', () => {
208+
it('should fail when props contain a type property', () => {
209+
expectSnippet(`
210+
const booksApiActions = createActionGroup({
211+
source: 'Books API',
212+
events: {
213+
'Load Books Success': props<{ books: string[]; type: any }>(),
214+
},
215+
});
216+
`).toFail(
217+
/action creator cannot return an object with a property named `type`/
218+
);
219+
});
220+
221+
it('should fail when props are an array', () => {
222+
expectSnippet(`
223+
const booksApiActions = createActionGroup({
224+
source: 'Books API',
225+
events: {
226+
'Load Books Success': props<string[]>(),
227+
},
228+
});
229+
`).toFail(/action creator cannot return an array/);
230+
});
231+
232+
it('should fail when props are an empty object', () => {
233+
expectSnippet(`
234+
const booksApiActions = createActionGroup({
235+
source: 'Books API',
236+
events: {
237+
'Load Books Success': props<{}>(),
238+
},
239+
});
240+
`).toFail(/action creator cannot return an empty object/);
241+
});
242+
243+
it('should fail when props are a primitive value', () => {
244+
expectSnippet(`
245+
const booksApiActions = createActionGroup({
246+
source: 'Books API',
247+
events: {
248+
'Load Books Success': props<string>(),
249+
},
250+
});
251+
`).toFail(/action creator props cannot be a primitive value/);
252+
});
253+
});
254+
255+
describe('props factory', () => {
256+
it('should fail when props factory returns an object with type property', () => {
257+
expectSnippet(`
258+
const booksApiActions = createActionGroup({
259+
source: 'Books API',
260+
events: {
261+
'Load Books Success': (books: string[]) => ({ books, type: 'T' }),
262+
},
263+
});
264+
`).toFail(
265+
/action creator cannot return an object with a property named `type`/
266+
);
267+
});
268+
269+
it('should fail when props factory returns an array', () => {
270+
expectSnippet(`
271+
const booksApiActions = createActionGroup({
272+
source: 'Books API',
273+
events: {
274+
'Load Books Success': (books: string[]) => books,
275+
},
276+
});
277+
`).toFail(/action creator cannot return an array/);
278+
});
279+
280+
it('should fail when props factory returns an empty object', () => {
281+
expectSnippet(`
282+
const booksApiActions = createActionGroup({
283+
source: 'Books API',
284+
events: {
285+
'Load Books Success': () => ({}),
286+
},
287+
});
288+
`).toFail(/action creator cannot return an empty object/);
289+
});
290+
291+
it('should fail when props factory returns a primitive value', () => {
292+
expectSnippet(`
293+
const booksApiActions = createActionGroup({
294+
source: 'Books API',
295+
events: {
296+
'Load Books Success': () => '',
297+
},
298+
});
299+
`).toFail(/Type '\(\) => string' is not assignable to type 'never'/);
300+
});
301+
});
302+
}
303+
304+
describe('strict mode', () => {
305+
const expectSnippet = expecter(snippetFactory, {
306+
...compilerOptions(),
307+
strict: true,
308+
});
309+
310+
testWith(expectSnippet);
311+
});
312+
313+
describe('non-strict mode', () => {
314+
const expectSnippet = expecter(snippetFactory, {
315+
...compilerOptions(),
316+
strict: false,
317+
});
318+
319+
testWith(expectSnippet);
320+
});
321+
});

0 commit comments

Comments
 (0)