Skip to content

Commit bf7d2f0

Browse files
committed
Write unit tests for $enum function
1 parent 4e4e80b commit bf7d2f0

File tree

4 files changed

+245
-2
lines changed

4 files changed

+245
-2
lines changed

src/enum.ts

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { BasicEnum, LabeledEnum } from './types';
2+
3+
export function $enum<T extends string | number>(values: T[]): BasicEnum<T>;
4+
5+
export function $enum<const T extends Record<PropertyKey, string | number>>(
6+
obj: T
7+
): LabeledEnum<T>;
8+
9+
export function $enum(
10+
param: (string | number)[] | Record<PropertyKey, string | number>
11+
) {
12+
// TODO: implement
13+
return {} as any;
14+
}

src/index.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
// TODO
2-
export function $enum() {}
1+
export { $enum } from './enum';
2+
export { ValueOf } from './types';

src/types.ts

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
export type BasicEnum<T extends string | number> = {
2+
values: () => readonly T[];
3+
isValue: (value: unknown) => value is T;
4+
assertValue: (value: unknown) => asserts value is T;
5+
};
6+
7+
export type LabeledEnum<T extends Record<PropertyKey, string | number>> =
8+
BasicEnum<T[keyof T]> & {
9+
keys: () => readonly (keyof T)[];
10+
isKey: (key: unknown) => key is keyof T;
11+
assertKey: (key: unknown) => asserts key is keyof T;
12+
entries: () => readonly (readonly [keyof T, T[keyof T]])[];
13+
isEntry: (entry: unknown) => entry is [keyof T, T[keyof T]];
14+
assertEntry: (entry: unknown) => asserts entry is [keyof T, T[keyof T]];
15+
obj: T;
16+
keyOf: (value: EnumToUnion<T[keyof T]>) => keyof T;
17+
};
18+
19+
export type ValueOf<T extends BasicEnum<string | number>> = EnumToUnion<
20+
ReturnType<T['values']>[number]
21+
>;
22+
23+
export type EnumToUnion<T extends string | number> = T extends string
24+
? `${T}`
25+
: T;

test/enum.test.ts

+204
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
import { $enum, ValueOf } from '../src';
2+
3+
describe('$enum', () => {
4+
describe('basic enum', () => {
5+
const ROLE = $enum(['viewer', 'editor', 'owner']);
6+
type Role = ValueOf<typeof ROLE>;
7+
8+
test('values', () => {
9+
expect(ROLE.values()).toEqual<Role[]>(['viewer', 'editor', 'owner']);
10+
});
11+
12+
test('isValue', () => {
13+
expect(ROLE.isValue('editor')).toBe(true);
14+
expect(ROLE.isValue('admin')).toBe(false);
15+
expect(ROLE.isValue(null)).toBe(false);
16+
});
17+
18+
test('assertValue', () => {
19+
expect(() => ROLE.assertValue('owner')).not.toThrow();
20+
expect(() => ROLE.assertValue(0)).toThrowError(
21+
'Enum value out of range (received 0, expected one of: "viewer", "editor", "owner")'
22+
);
23+
});
24+
});
25+
26+
describe('labeled enum', () => {
27+
const BOOKING_STATUS = $enum({
28+
missingConfirmation: 'chybí potvrzení',
29+
awaitingPayment: 'čeká se na platbu',
30+
complete: 'zaplaceno',
31+
});
32+
type BookingStatus = ValueOf<typeof BOOKING_STATUS>;
33+
34+
test('values', () => {
35+
expect(BOOKING_STATUS.values()).toEqual<BookingStatus[]>([
36+
'chybí potvrzení',
37+
'čeká se na platbu',
38+
'zaplaceno',
39+
]);
40+
});
41+
42+
test('isValue', () => {
43+
expect(BOOKING_STATUS.isValue('zaplaceno')).toBe(true);
44+
});
45+
46+
test('assertValue', () => {
47+
expect(() => BOOKING_STATUS.assertValue('zaplaceno')).not.toThrowError();
48+
});
49+
50+
test('keys', () => {
51+
expect(BOOKING_STATUS.keys()).toEqual<
52+
ReturnType<(typeof BOOKING_STATUS)['keys']>
53+
>(['missingConfirmation', 'awaitingPayment', 'complete']);
54+
});
55+
56+
test('isKey', () => {
57+
expect(BOOKING_STATUS.isKey('awaitingPayment')).toBe(true);
58+
expect(BOOKING_STATUS.isKey('zaplaceno')).toBe(false);
59+
});
60+
61+
test('assertKey', () => {
62+
expect(() => BOOKING_STATUS.assertKey('complete')).not.toThrowError();
63+
expect(() => BOOKING_STATUS.assertKey(undefined)).toThrowError(
64+
`Enum key out of range (received undefined, expected one of: "missingConfirmation", "awaitingPayment", "complete")`
65+
);
66+
});
67+
68+
test('entries', () => {
69+
expect(BOOKING_STATUS.entries()).toEqual<
70+
ReturnType<(typeof BOOKING_STATUS)['entries']>
71+
>([
72+
['missingConfirmation', 'chybí potvrzení'],
73+
['awaitingPayment', 'čeká se na platbu'],
74+
['complete', 'zaplaceno'],
75+
]);
76+
});
77+
78+
test('isEntry', () => {
79+
expect(BOOKING_STATUS.isEntry(['awaitingPayment', 'zaplaceno'])).toBe(
80+
false
81+
);
82+
expect(BOOKING_STATUS.isEntry(['complete', 'zaplaceno'])).toBe(true);
83+
});
84+
85+
test('assertEntry', () => {
86+
expect(() =>
87+
BOOKING_STATUS.assertEntry(['missingConfirmation', 'chybí potvrzení'])
88+
).not.toThrowError();
89+
expect(() =>
90+
BOOKING_STATUS.assertEntry(['paid', 'zaplaceno'])
91+
).toThrowError(
92+
'Enum key out of range (received "paid", expected one of: "missingConfirmation", "awaitingPayment", "complete")'
93+
);
94+
expect(() =>
95+
BOOKING_STATUS.assertEntry(['awaitingPayment', 'zaplaceno'])
96+
).toThrowError(
97+
'Enum key and value don\'t match (expected ["awaitingPayment", "čeká se na platbu"] or ["complete", "zaplaceno"])'
98+
);
99+
expect(() => BOOKING_STATUS.assertEntry(['complete'])).toThrowError(
100+
'Enum entry must be a tuple (e.g. ["key", "value"])'
101+
);
102+
});
103+
104+
test('obj', () => {
105+
expect(BOOKING_STATUS.obj).toEqual({
106+
missingConfirmation: 'chybí potvrzení',
107+
awaitingPayment: 'čeká se na platbu',
108+
complete: 'zaplaceno',
109+
});
110+
});
111+
112+
test('keyOf', () => {
113+
expect(BOOKING_STATUS.keyOf('chybí potvrzení')).toBe(
114+
'missingConfirmation'
115+
);
116+
expect(BOOKING_STATUS.keyOf('zaplaceno')).toBe('complete');
117+
});
118+
});
119+
120+
describe('from TypeScript string enum', () => {
121+
enum Action {
122+
Allow = 'allow',
123+
Block = 'block',
124+
}
125+
const ACTION = $enum(Action);
126+
127+
test('values', () => {
128+
expect(ACTION.values()).toEqual<ValueOf<typeof ACTION>[]>([
129+
'allow',
130+
'block',
131+
]);
132+
});
133+
134+
test('isValue', () => {
135+
expect(ACTION.isValue(Action.Block)).toBe(true);
136+
expect(ACTION.isValue('block')).toBe(true);
137+
expect(ACTION.isValue('Block')).toBe(false);
138+
});
139+
140+
test('keys', () => {
141+
expect(ACTION.keys()).toEqual<(keyof typeof Action)[]>([
142+
'Allow',
143+
'Block',
144+
]);
145+
});
146+
147+
test('isKey', () => {
148+
expect(ACTION.isKey('Allow')).toBe(true);
149+
expect(ACTION.isKey('allow')).toBe(false);
150+
});
151+
152+
test('entries', () => {
153+
expect(ACTION.entries()).toEqual<[keyof typeof Action, `${Action}`][]>([
154+
['Allow', 'allow'],
155+
['Block', 'block'],
156+
]);
157+
});
158+
159+
test('obj', () => {
160+
expect(ACTION.obj).toEqual({
161+
Allow: 'allow',
162+
Block: 'block',
163+
});
164+
});
165+
166+
test('keyOf', () => {
167+
expect(ACTION.keyOf(Action.Allow)).toEqual('Allow');
168+
expect(ACTION.keyOf('block')).toEqual('Block');
169+
});
170+
});
171+
172+
describe('from TypeScript number enum', () => {
173+
enum Level {
174+
off,
175+
warn,
176+
error,
177+
}
178+
const LEVEL = $enum(Level);
179+
180+
test('values', () => {
181+
expect(LEVEL.values()).toEqual<Level[]>([0, 1, 2]);
182+
});
183+
184+
test('isValue', () => {
185+
expect(LEVEL.isValue(Level.warn)).toBe(true);
186+
expect(LEVEL.isValue('warn')).toBe(false);
187+
expect(LEVEL.isValue(1)).toBe(true);
188+
expect(LEVEL.isValue(3)).toBe(false);
189+
});
190+
191+
test('keys', () => {
192+
expect(LEVEL.keys()).toEqual<(keyof typeof Level)[]>([
193+
'off',
194+
'warn',
195+
'error',
196+
]);
197+
});
198+
199+
test('keyOf', () => {
200+
expect(LEVEL.keyOf(2)).toEqual('error');
201+
expect(LEVEL.keyOf(Level.warn)).toEqual('warn');
202+
});
203+
});
204+
});

0 commit comments

Comments
 (0)