Skip to content

Commit 8949b37

Browse files
committed
Clean up options schemas
Coerce 'true'/'false' into actual booleans (Fixes #223) Better error messages
1 parent 5d635b3 commit 8949b37

File tree

10 files changed

+571
-46
lines changed

10 files changed

+571
-46
lines changed

README.md

Lines changed: 11 additions & 11 deletions
Large diffs are not rendered by default.

test/helpers/expect-logs.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { expect, jest } from '@jest/globals';
2+
import logger from '../../transforms/helpers/log-helper';
3+
4+
/**
5+
* Spies on the logger for messages matching those passed in the config.
6+
* If no messages are passed in the config, expects there to be no logs.
7+
*/
8+
export function expectLogs(
9+
callback: () => void,
10+
{
11+
info = [],
12+
warn = [],
13+
error = [],
14+
}: {
15+
info?: string[];
16+
warn?: string[];
17+
error?: string[];
18+
} = {}
19+
): void {
20+
const infoConfig = {
21+
level: 'info' as const,
22+
expectedMessages: info,
23+
restoreAllMocks: false,
24+
};
25+
const warnConfig = {
26+
level: 'warn' as const,
27+
expectedMessages: warn,
28+
restoreAllMocks: false,
29+
};
30+
const errorConfig = {
31+
level: 'error' as const,
32+
expectedMessages: error,
33+
restoreAllMocks: true,
34+
};
35+
36+
expectLogLevel(() => {
37+
expectLogLevel(() => {
38+
expectLogLevel(callback, infoConfig);
39+
}, warnConfig);
40+
}, errorConfig);
41+
42+
jest.restoreAllMocks();
43+
}
44+
45+
function expectLogLevel(
46+
callback: () => void,
47+
{
48+
level = 'error',
49+
expectedMessages = [],
50+
restoreAllMocks = true,
51+
}: {
52+
level?: 'info' | 'warn' | 'error';
53+
expectedMessages?: string[];
54+
restoreAllMocks?: boolean;
55+
} = {}
56+
): void {
57+
const spy = jest.spyOn(logger, level);
58+
59+
callback();
60+
61+
if (expectedMessages.length > 0) {
62+
expect(spy).toHaveBeenCalledTimes(expectedMessages.length);
63+
for (const [index, expectedError] of expectedMessages.entries()) {
64+
expect(spy).toHaveBeenNthCalledWith(
65+
index + 1,
66+
expect.stringMatching(expectedError)
67+
);
68+
}
69+
} else {
70+
expect(spy).not.toHaveBeenCalled();
71+
}
72+
73+
if (restoreAllMocks) {
74+
jest.restoreAllMocks();
75+
}
76+
}
77+
78+
/**
79+
* Makes a regexp pattern to match logs. String arguments passed to
80+
* `makeLogMatcher` will be escaped then merged together into a regexp that will
81+
* match partial lines of multi-line logs when paired with Jest
82+
* `expect.stringMatching`.
83+
*
84+
* @example
85+
* ```
86+
* const expected = makeLogMatcher('Line 1', 'Line 2', '3')
87+
* //=> 'Line 1[\S\s]*Line 2[\S\s]*3'
88+
*
89+
* expect('Line 1\nLine 2\nLine 3').toEqual(expect.stringMatching(expected));
90+
* //=> passes
91+
* ```
92+
*/
93+
export function makeLogMatcher(...parts: string[]): string {
94+
return parts.map(escapeRegExp).join('[\\S\\s]*');
95+
}
96+
97+
/**
98+
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
99+
*/
100+
function escapeRegExp(string: string): string {
101+
return string.replace(/[$()*+.?[\\\]^{|}]/g, '\\$&'); // $& means the whole matched string
102+
}

test/options.test.ts

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
import { describe, expect, test } from '@jest/globals';
2+
import { DEFAULT_OPTIONS, parseConfig } from '../transforms/helpers/options';
3+
import { expectLogs, makeLogMatcher } from './helpers/expect-logs';
4+
5+
describe('options', () => {
6+
describe('parseConfig', () => {
7+
test('it parses an empty config', () => {
8+
expectLogs(() => {
9+
const config = parseConfig('test', {});
10+
expect(config).toStrictEqual({});
11+
});
12+
});
13+
14+
test('it parses the DEFAULT_OPTIONS', () => {
15+
expectLogs(() => {
16+
const config = parseConfig('test', DEFAULT_OPTIONS);
17+
expect(config).toStrictEqual(DEFAULT_OPTIONS);
18+
});
19+
});
20+
21+
describe('decorators', () => {
22+
test('it parses `{ decorators: true }`', () => {
23+
expectLogs(() => {
24+
const config = parseConfig('test', { decorators: true });
25+
expect(config).toStrictEqual({
26+
decorators: { inObjectLiterals: [] },
27+
});
28+
});
29+
});
30+
31+
test('it parses `{ decorators: "true" }`', () => {
32+
expectLogs(() => {
33+
const config = parseConfig('test', { decorators: 'true' });
34+
expect(config).toStrictEqual({
35+
decorators: { inObjectLiterals: [] },
36+
});
37+
});
38+
});
39+
40+
test('it parses `{ decorators: false }`', () => {
41+
expectLogs(() => {
42+
const config = parseConfig('test', { decorators: false });
43+
expect(config).toStrictEqual({ decorators: false });
44+
});
45+
});
46+
47+
test('it parses `{ decorators: "false" }`', () => {
48+
expectLogs(() => {
49+
const config = parseConfig('test', { decorators: 'false' });
50+
expect(config).toStrictEqual({ decorators: false });
51+
});
52+
});
53+
54+
test('it parses DecoratorOptions.inObjectLiterals with array of strings', () => {
55+
expectLogs(() => {
56+
const config = parseConfig('test', {
57+
decorators: { inObjectLiterals: ['one', 'two', 'three'] },
58+
});
59+
expect(config).toStrictEqual({
60+
decorators: { inObjectLiterals: ['one', 'two', 'three'] },
61+
});
62+
});
63+
});
64+
65+
test('it parses DecoratorOptions.inObjectLiterals with string of strings', () => {
66+
expectLogs(() => {
67+
const config = parseConfig('test', {
68+
decorators: { inObjectLiterals: 'one,two , three' },
69+
});
70+
expect(config).toStrictEqual({
71+
decorators: { inObjectLiterals: ['one', 'two', 'three'] },
72+
});
73+
});
74+
});
75+
76+
test('it logs an error for invalid `decorators` config', () => {
77+
expectLogs(
78+
() => {
79+
const config = parseConfig('test', { decorators: 'oops' });
80+
expect(config).toStrictEqual({});
81+
},
82+
{
83+
error: [
84+
makeLogMatcher(
85+
'[test]: CONFIG ERROR:',
86+
"[decorators] Expected DecoratorOptions object or boolean, received 'oops'"
87+
),
88+
],
89+
}
90+
);
91+
});
92+
});
93+
94+
describe.each(['classFields', 'classicDecorator', 'partialTransforms'])(
95+
'%s (StringBooleanSchema)',
96+
(fieldName) => {
97+
test(`it parses \`{ ${fieldName}: true }\``, () => {
98+
expectLogs(() => {
99+
const config = parseConfig('test', { [fieldName]: true });
100+
expect(config).toStrictEqual({ [fieldName]: true });
101+
});
102+
});
103+
104+
test(`it parses \`{ ${fieldName}: "true" }\``, () => {
105+
expectLogs(() => {
106+
const config = parseConfig('test', { [fieldName]: 'true' });
107+
expect(config).toStrictEqual({ [fieldName]: true });
108+
});
109+
});
110+
111+
test(`it parses \`{ ${fieldName}: false }\``, () => {
112+
expectLogs(() => {
113+
const config = parseConfig('test', { [fieldName]: false });
114+
expect(config).toStrictEqual({ [fieldName]: false });
115+
});
116+
});
117+
118+
test(`it parses \`{ ${fieldName}: "false" }\``, () => {
119+
expectLogs(() => {
120+
const config = parseConfig('test', { [fieldName]: 'false' });
121+
expect(config).toStrictEqual({ [fieldName]: false });
122+
});
123+
});
124+
125+
test(`it logs an error for invalid \`${fieldName}\` config`, () => {
126+
expectLogs(
127+
() => {
128+
const config = parseConfig('test', { [fieldName]: 'oops' });
129+
expect(config).toStrictEqual({});
130+
},
131+
{
132+
error: [
133+
makeLogMatcher(
134+
'[test]: CONFIG ERROR:',
135+
`[${fieldName}] Expected boolean, received string`
136+
),
137+
],
138+
}
139+
);
140+
});
141+
}
142+
);
143+
144+
describe('quote', () => {
145+
test('it parses `{ quote: "single" }`', () => {
146+
expectLogs(() => {
147+
const config = parseConfig('test', { quote: 'single' });
148+
expect(config).toStrictEqual({ quote: 'single' });
149+
});
150+
});
151+
152+
test('it parses `{ quote: "double" }`', () => {
153+
expectLogs(() => {
154+
const config = parseConfig('test', { quote: 'double' });
155+
expect(config).toStrictEqual({ quote: 'double' });
156+
});
157+
});
158+
159+
test('it logs an error for invalid `quote` config', () => {
160+
expectLogs(
161+
() => {
162+
const config = parseConfig('test', { quote: 'oops' });
163+
expect(config).toStrictEqual({});
164+
},
165+
{
166+
error: [
167+
makeLogMatcher(
168+
'[test]: CONFIG ERROR:',
169+
"[quote] Expected 'single' or 'double', received 'oops"
170+
),
171+
],
172+
}
173+
);
174+
});
175+
});
176+
177+
describe('ignoreLeakingState', () => {
178+
test('it parses `ignoreLeakingState` with an empty array', () => {
179+
expectLogs(() => {
180+
const config = parseConfig('test', { ignoreLeakingState: [] });
181+
expect(config).toStrictEqual({ ignoreLeakingState: [] });
182+
});
183+
});
184+
185+
test('it parses `ignoreLeakingState` with array of strings', () => {
186+
expectLogs(() => {
187+
const config = parseConfig('test', {
188+
ignoreLeakingState: ['one', 'two', 'three'],
189+
});
190+
expect(config).toStrictEqual({
191+
ignoreLeakingState: ['one', 'two', 'three'],
192+
});
193+
});
194+
});
195+
196+
test('it parses `ignoreLeakingState` with string of strings', () => {
197+
expectLogs(() => {
198+
const config = parseConfig('test', {
199+
ignoreLeakingState: 'one,two , three',
200+
});
201+
expect(config).toStrictEqual({
202+
ignoreLeakingState: ['one', 'two', 'three'],
203+
});
204+
});
205+
});
206+
207+
test('it logs an error for invalid `ignoreLeakingState` config', () => {
208+
expectLogs(
209+
() => {
210+
const config = parseConfig('test', { ignoreLeakingState: false });
211+
expect(config).toStrictEqual({});
212+
},
213+
{
214+
error: [
215+
makeLogMatcher(
216+
'[test]: CONFIG ERROR:',
217+
'[ignoreLeakingState] Expected array of strings or comma-separated string, received false'
218+
),
219+
],
220+
}
221+
);
222+
});
223+
});
224+
225+
describe('type', () => {
226+
test.each(['services', 'routes', 'components', 'controllers'])(
227+
'it parses `{ type: "%s" }`',
228+
(type) => {
229+
expectLogs(() => {
230+
const config = parseConfig('test', { type });
231+
expect(config).toStrictEqual({ type });
232+
});
233+
}
234+
);
235+
236+
test('it logs an error for invalid `type` config', () => {
237+
expectLogs(
238+
() => {
239+
const config = parseConfig('test', { type: 'oops' });
240+
expect(config).toStrictEqual({});
241+
},
242+
{
243+
error: [
244+
makeLogMatcher(
245+
'[test]: CONFIG ERROR:',
246+
"[type] Expected 'services', 'routes', 'components', or 'controllers', received 'oops"
247+
),
248+
],
249+
}
250+
);
251+
});
252+
});
253+
});
254+
});
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
const Foo1 = EmberObject.extend({});
2+
3+
/**
4+
* Program comments
5+
*/
6+
const Foo2 = Test.extend({
7+
/**
8+
* Property comments
9+
*/
10+
prop: "defaultValue",
11+
boolProp: true,
12+
numProp: 123,
13+
[MY_VAL]: "val",
14+
15+
/**
16+
* Method comments
17+
*/
18+
method() {
19+
// do things
20+
},
21+
22+
otherMethod: function() {},
23+
24+
get accessor() {
25+
return this._value;
26+
},
27+
28+
set accessor(value) {
29+
this._value = value;
30+
},
31+
32+
anotherMethod() {
33+
this._super(...arguments);
34+
}
35+
});
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"classicDecorator": "false",
3+
"classFields": "false",
4+
"decorators": "true"
5+
}

0 commit comments

Comments
 (0)