Skip to content

Commit 298e948

Browse files
committed
feat: add unit tests for Enum typings
1 parent ff22692 commit 298e948

File tree

5 files changed

+185
-53
lines changed

5 files changed

+185
-53
lines changed

src/enum-collection.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,13 @@ export class EnumCollectionClass<
7979

8080
constructor(init: T = {} as T, options?: EnumItemOptions) {
8181
super();
82-
this.__options__ = options;
82+
// Do not use class field here, because don't want print this field in Node.js
83+
Object.defineProperty(this, '__options__', {
84+
value: options,
85+
writable: false,
86+
enumerable: false,
87+
configurable: false,
88+
});
8389

8490
// Generate keys array
8591
// exclude number keys with a "reverse mapping" value, it means those "reverse mapping" keys of number enums

src/enum-item.ts

Lines changed: 57 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,58 @@ export class EnumItemClass<
1515
K extends EnumKey<any> = string,
1616
V extends EnumValue = ValueTypeFromSingleInit<T, K>,
1717
> {
18+
private _options: EnumItemOptions | undefined;
19+
20+
/**
21+
* Instantiate an enum item
22+
*
23+
* @param key Enum item key
24+
* @param value Enum item value
25+
* @param label Enum item display name
26+
* @param raw Original initialization object
27+
* @param options Construction options
28+
*/
29+
constructor(key: K, value: V, label: string, raw: T, options?: EnumItemOptions) {
30+
this.key = key;
31+
this.value = value;
32+
this.label = label;
33+
this.raw = raw;
34+
// Do not use class field here, because don't want print this field in Node.js
35+
Object.defineProperty(this, '_options', {
36+
value: options,
37+
writable: false,
38+
enumerable: false,
39+
configurable: false,
40+
});
41+
Object.defineProperties(this, {
42+
value: {
43+
get: () => value,
44+
set: () => console.warn(this._readonlyPropWarning('value')),
45+
enumerable: true,
46+
configurable: false,
47+
},
48+
label: {
49+
get: () => this._localize(label) ?? label,
50+
set: () => console.warn(this._readonlyPropWarning('label')),
51+
enumerable: true,
52+
configurable: false,
53+
},
54+
key: {
55+
get: () => key,
56+
set: () => console.warn(this._readonlyPropWarning('key')),
57+
enumerable: true,
58+
configurable: false,
59+
},
60+
raw: {
61+
get: () => raw,
62+
set: () => console.warn(this._readonlyPropWarning('raw')),
63+
enumerable: true,
64+
configurable: false,
65+
},
66+
});
67+
Object.freeze(this);
68+
}
69+
1870
/**
1971
* - **EN:** The value of the enum item
2072
* - **CN:** 枚举项的值
@@ -44,7 +96,11 @@ export class EnumItemClass<
4496
* - **EN:** A boolean value indicates that this is an enum item instance.
4597
* - **CN:** 布尔值,表示这是一个枚举项实例
4698
*/
47-
private readonly [IS_ENUM_ITEM] = true;
99+
// Do not use readonly field here, because don't want print this field in Node.js
100+
// eslint-disable-next-line @typescript-eslint/class-literal-property-style
101+
get [IS_ENUM_ITEM](): true {
102+
return true;
103+
}
48104
/**
49105
* Auto convert to a correct primitive type. This method is called when the object is used in a
50106
* context that requires a primitive value.
@@ -68,7 +124,6 @@ export class EnumItemClass<
68124
return this.valueOf();
69125
}
70126

71-
private _options: EnumItemOptions | undefined;
72127
// should use function here to avoid closure. this is important for the e2e test cases.
73128
private _localize(content: string | undefined) {
74129
const localize = this._options?.localize ?? localizer.localize;
@@ -81,55 +136,6 @@ export class EnumItemClass<
81136
return `Cannot modify property "${name}" on EnumItem. EnumItem instances are readonly and should not be mutated.`;
82137
}
83138

84-
/**
85-
* Instantiate an enum item
86-
*
87-
* @param key Enum item key
88-
* @param value Enum item value
89-
* @param label Enum item display name
90-
* @param raw Original initialization object
91-
* @param options Construction options
92-
*/
93-
constructor(key: K, value: V, label: string, raw: T, options?: EnumItemOptions) {
94-
this.key = key;
95-
this.value = value;
96-
this.label = label;
97-
this.raw = raw;
98-
this._options = options;
99-
Object.defineProperties(this, {
100-
value: {
101-
get: () => value,
102-
set: () => console.warn(this._readonlyPropWarning('value')),
103-
enumerable: true,
104-
configurable: false,
105-
},
106-
label: {
107-
get: () => this._localize(label) ?? label,
108-
set: () => console.warn(this._readonlyPropWarning('label')),
109-
enumerable: true,
110-
configurable: false,
111-
},
112-
key: {
113-
get: () => key,
114-
set: () => console.warn(this._readonlyPropWarning('key')),
115-
enumerable: true,
116-
configurable: false,
117-
},
118-
raw: {
119-
get: () => raw,
120-
set: () => console.warn(this._readonlyPropWarning('raw')),
121-
enumerable: true,
122-
configurable: false,
123-
},
124-
[IS_ENUM_ITEM]: {
125-
value: this[IS_ENUM_ITEM],
126-
writable: false,
127-
enumerable: true,
128-
configurable: false,
129-
},
130-
});
131-
Object.freeze(this);
132-
}
133139
// The priority of the toString method is lower than the valueOf method
134140
toString() {
135141
return this._localize(this.label);

src/enum-items.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,13 @@ export class EnumItemsArray<
5858
constructor(raw: T, options: EnumItemOptions | undefined, ...items: EnumItemClass<T[K], K, V>[]) {
5959
super(...items);
6060
this.__raw__ = raw;
61+
// Do not use class field here, because don't want print this field in Node.js
62+
Object.defineProperty(this, '__raw__', {
63+
value: raw,
64+
enumerable: false,
65+
writable: false,
66+
configurable: false,
67+
});
6168
}
6269
name?: string | undefined;
6370
[Symbol.hasInstance]<T>(instance: T): instance is Extract<T, K | V> {
@@ -393,7 +400,7 @@ export interface IEnumItems<
393400
*
394401
* @returns The found enumeration item or undefined if not found | 找到的枚举项,如果未找到则返回 undefined
395402
*/
396-
findBy<FK extends 'key' | 'value' | 'label' | Exclude<keyof T[keyof T], 'key' | 'value' | 'label'>, FV>(
403+
findBy<FK extends 'key' | 'value' | 'label' | Exclude<keyof T[keyof T], 'key' | 'value' | 'label'>, const FV>(
397404
field: FK,
398405
value: FV
399406
): FK extends 'key'

test/test-suites/test-typing.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import type { IEnum, IEnumItems } from '@enum-plus';
2+
import type { localeEN, StandardWeekConfig } from '../data/week-config';
3+
import type TestEngineBase from '../engines/base';
4+
5+
const testTyping = (engine: TestEngineBase) => {
6+
engine.describe('Enum typings', () => {
7+
engine.test(
8+
'the test codes should not report any TypeScript errors, even including @ts-expect-error comments',
9+
({ EnumPlus: { Enum, defaultLocalize }, WeekConfig: { StandardWeekConfig, setLang, getLocales, localeEN } }) => {
10+
setLang('en-US', Enum, getLocales, defaultLocalize);
11+
const WeekConfig = StandardWeekConfig;
12+
const weekEnum = Enum(StandardWeekConfig);
13+
return { weekEnum, Enum, WeekConfig, localeEN };
14+
},
15+
({ weekEnum, Enum, WeekConfig, localeEN }) => {
16+
const value = weekEnum as typeof weekEnum | number | string | undefined;
17+
if (Enum.isEnum(value)) {
18+
engine.expect(value).toBe(weekEnum);
19+
} else {
20+
// @ts-expect-error: because val is narrowed off Enum type, type casting will raise error
21+
console.log(value as typeof weekEnum);
22+
}
23+
24+
validateEnum(engine, weekEnum, localeEN, WeekConfig);
25+
validateEnum(engine, weekEnum.items, localeEN, WeekConfig);
26+
}
27+
);
28+
});
29+
};
30+
31+
export default testTyping;
32+
33+
/*
34+
!----------------------------------------------------------------------
35+
The below function should not contain any TypeScript errors, even the
36+
@ts-expect-error comments should not report any errors.
37+
If you see any TypeScript errors, it means the typing is incorrect!
38+
!----------------------------------------------------------------------
39+
*/
40+
function validateEnum<
41+
T extends
42+
| IEnum<
43+
typeof StandardWeekConfig,
44+
keyof typeof StandardWeekConfig,
45+
(typeof StandardWeekConfig)[keyof typeof StandardWeekConfig]['value']
46+
>
47+
| IEnumItems<
48+
typeof StandardWeekConfig,
49+
keyof typeof StandardWeekConfig,
50+
(typeof StandardWeekConfig)[keyof typeof StandardWeekConfig]['value']
51+
>,
52+
>(engine: TestEngineBase, weekEnum: T, locale: typeof localeEN, WeekConfig: typeof StandardWeekConfig) {
53+
const value2 = 1 as 1 | { foo: number };
54+
const value3 = 'Monday' as 'Monday' | { foo: number };
55+
if (value2 instanceof weekEnum) {
56+
engine.expect(value2.toFixed(1)).toBe('1.0');
57+
} else {
58+
console.log(value2 as { foo: number });
59+
}
60+
if (value3 instanceof weekEnum) {
61+
engine.expect(value3.trim()).toBe('Monday');
62+
} else {
63+
console.log(value3 as { foo: number });
64+
}
65+
66+
engine.expect(weekEnum.key(1).trim()).toBe('Monday');
67+
// @ts-expect-error: because key returns nullable value, should use optional chaining (?.) operator
68+
engine.expect(weekEnum.key(1 as number).trim()).toBe('Monday');
69+
// @ts-expect-error: because key returns nullable value, should use optional chaining (?.) operator
70+
engine.expect(() => weekEnum.key(8 as number).trim()).toThrow();
71+
// @ts-expect-error: because out-of-range literal values return undefined only, cannot be cased to number
72+
engine.expect(weekEnum.key(8) as number).toBe(undefined);
73+
74+
engine.expect(weekEnum.label(1)?.trim()).toBe(locale.Monday);
75+
// @ts-expect-error: because label returns nullable value, should use optional chaining (?.) operator
76+
engine.expect(weekEnum.label(1 as number).trim()).toBe(locale.Monday);
77+
// @ts-expect-error: because label returns nullable value, should use optional chaining (?.) operator
78+
engine.expect(() => weekEnum.label(8 as number).trim()).toThrow();
79+
// @ts-expect-error: because out-of-range literal values return undefined only, cannot be cased to string
80+
engine.expect(weekEnum.label(8) as string).toBe(undefined);
81+
82+
engine.expect(weekEnum.raw('Monday')).toBe(WeekConfig.Monday);
83+
// @ts-expect-error: because raw returns nullable object, should use optional chaining (?.) operator
84+
engine.expect(weekEnum.raw('Monday' as string).value).toBe(1);
85+
// @ts-expect-error: because raw returns nullable object, should use optional chaining (?.) operator
86+
engine.expect(() => weekEnum.raw('January' as string).value).toThrow();
87+
// @ts-expect-error: because out-of-range literal values return undefined only, cannot be cased to object
88+
engine.expect(weekEnum.raw('January') as { value: number }).toBe(undefined);
89+
90+
engine.expect(weekEnum.findBy('key', 'Monday').key).toBe('Monday');
91+
// @ts-expect-error: because findBy returns nullable object, should use optional chaining (?.) operator
92+
engine.expect(weekEnum.findBy('key', 'Monday' as string).key).toBe('Monday');
93+
// @ts-expect-error: because findBy returns nullable object, should use optional chaining (?.) operator
94+
engine.expect(() => weekEnum.findBy('key', 'January' as string).key).toThrow();
95+
engine.expect(weekEnum.findBy('value', 1).key).toBe('Monday');
96+
// @ts-expect-error: because findBy returns nullable object, should use optional chaining (?.) operator
97+
engine.expect(weekEnum.findBy('value', 1 as number).key).toBe('Monday');
98+
// @ts-expect-error: because findBy returns nullable object, should use optional chaining (?.) operator
99+
engine.expect(() => weekEnum.findBy('value', 8 as number).key).toThrow();
100+
// @ts-expect-error: because findBy label always return nullable string
101+
engine.expect(weekEnum.findBy('label', 'Monday').key).toBe('Monday');
102+
// @ts-expect-error: because findBy label always return nullable string
103+
engine.expect(() => weekEnum.findBy('label', 'January' as string).key).toThrow();
104+
engine.expect(weekEnum.findBy('status', 'warning').key).toBe('Monday');
105+
// @ts-expect-error: because findBy findBy returns nullable object, should use optional chaining (?.) operator
106+
engine.expect(weekEnum.findBy('status', 'warning' as string).key).toBe('Monday');
107+
// @ts-expect-error: because findBy returns nullable object, should use optional chaining (?.) operator
108+
engine.expect(() => weekEnum.findBy('status', 'foo' as string).key).toThrow();
109+
}

test/test-typing.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import jest from './engines/jest';
2+
import testTyping from './test-suites/test-typing';
3+
4+
testTyping(jest);

0 commit comments

Comments
 (0)