Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
22 changes: 11 additions & 11 deletions README.md

Large diffs are not rendered by default.

118 changes: 118 additions & 0 deletions test/helpers/expect-logs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { expect, jest } from '@jest/globals';
import logger from '../../transforms/helpers/log-helper';

/**
* Spies on all logger log levels for messages matching those passed in the
* config.
*
* @param callback The callback expected to trigger (or not) the logs.
* @param config An optional object with an array of expected messages for each
* log level. If no array is passed, no messages will be expected for that
* level. If no object is passed, the function will expect that there are no
* logs.
*/
export function expectLogs(
callback: () => void,
{
info = [],
warn = [],
error = [],
}: {
info?: string[];
warn?: string[];
error?: string[];
} = {}
): void {
const infoConfig = {
level: 'info' as const,
expectedMessages: info,
restoreAllMocks: false,
};
const warnConfig = {
level: 'warn' as const,
expectedMessages: warn,
restoreAllMocks: false,
};
const errorConfig = {
level: 'error' as const,
expectedMessages: error,
restoreAllMocks: true,
};

expectLogLevel(() => {
expectLogLevel(() => {
expectLogLevel(callback, infoConfig);
}, warnConfig);
}, errorConfig);

jest.restoreAllMocks();
}

/**
* Spies on the logger for messages matching those passed in the config.
*
* @param callback The callback expected to trigger (or not) the logs.
* @param config An optional object with an specified log `level`, an array of
* `expectedMessages` for that log level, and an option to run
* `jest.restoreAllMocks()` after the callback and expectations are complete.
* If no object is passed, will default to spying on the `'error'` log level,
* expect that no messages are sent, and will restore all mocks after the test.
*/
function expectLogLevel(
callback: () => void,
{
level = 'error',
expectedMessages = [],
restoreAllMocks = true,
}: {
level?: 'info' | 'warn' | 'error';
expectedMessages?: string[];
restoreAllMocks?: boolean;
} = {}
): void {
const spy = jest.spyOn(logger, level);

callback();

if (expectedMessages.length > 0) {
expect(spy).toHaveBeenCalledTimes(expectedMessages.length);
for (const [index, expectedError] of expectedMessages.entries()) {
expect(spy).toHaveBeenNthCalledWith(
index + 1,
expect.stringMatching(expectedError)
);
}
} else {
expect(spy).not.toHaveBeenCalled();
}

if (restoreAllMocks) {
jest.restoreAllMocks();
}
}

/**
* Makes a regexp pattern to match logs. String arguments passed to
* `makeLogMatcher` will be escaped then merged together into a regexp that will
* match partial lines of multi-line logs when paired with Jest
* `expect.stringMatching`.
*
* @example
* ```
* const expected = makeLogMatcher('Line 1', 'Line 2', '3')
* //=> 'Line 1[\S\s]*Line 2[\S\s]*3'
*
* expect('Line 1\nLine 2\nLine 3').toEqual(expect.stringMatching(expected));
* //=> passes
* ```
*/
export function makeLogMatcher(...parts: string[]): string {
return parts.map(escapeRegExp).join('[\\S\\s]*');
}

/**
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
*/
function escapeRegExp(string: string): string {
return string.replace(/[$()*+.?[\\\]^{|}]/g, '\\$&'); // $& means the whole matched string
}
254 changes: 254 additions & 0 deletions test/options.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
import { describe, expect, test } from '@jest/globals';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm loving all these new tests. Great job!

import { DEFAULT_OPTIONS, parseConfig } from '../transforms/helpers/options';
import { expectLogs, makeLogMatcher } from './helpers/expect-logs';

describe('options', () => {
describe('parseConfig', () => {
test('it parses an empty config', () => {
expectLogs(() => {
const config = parseConfig('test', {});
expect(config).toStrictEqual({});
});
});

test('it parses the DEFAULT_OPTIONS', () => {
expectLogs(() => {
const config = parseConfig('test', DEFAULT_OPTIONS);
expect(config).toStrictEqual(DEFAULT_OPTIONS);
});
});

describe('decorators', () => {
test('it parses `{ decorators: true }`', () => {
expectLogs(() => {
const config = parseConfig('test', { decorators: true });
expect(config).toStrictEqual({
decorators: { inObjectLiterals: [] },
});
});
});

test('it parses `{ decorators: "true" }`', () => {
expectLogs(() => {
const config = parseConfig('test', { decorators: 'true' });
expect(config).toStrictEqual({
decorators: { inObjectLiterals: [] },
});
});
});

test('it parses `{ decorators: false }`', () => {
expectLogs(() => {
const config = parseConfig('test', { decorators: false });
expect(config).toStrictEqual({ decorators: false });
});
});

test('it parses `{ decorators: "false" }`', () => {
expectLogs(() => {
const config = parseConfig('test', { decorators: 'false' });
expect(config).toStrictEqual({ decorators: false });
});
});

test('it parses DecoratorOptions.inObjectLiterals with array of strings', () => {
expectLogs(() => {
const config = parseConfig('test', {
decorators: { inObjectLiterals: ['one', 'two', 'three'] },
});
expect(config).toStrictEqual({
decorators: { inObjectLiterals: ['one', 'two', 'three'] },
});
});
});

test('it parses DecoratorOptions.inObjectLiterals with string of strings', () => {
expectLogs(() => {
const config = parseConfig('test', {
decorators: { inObjectLiterals: 'one,two , three' },
});
expect(config).toStrictEqual({
decorators: { inObjectLiterals: ['one', 'two', 'three'] },
});
});
});

test('it logs an error for invalid `decorators` config', () => {
expectLogs(
() => {
const config = parseConfig('test', { decorators: 'oops' });
expect(config).toStrictEqual({});
},
{
error: [
makeLogMatcher(
'[test]: CONFIG ERROR:',
"[decorators] Expected DecoratorOptions object or boolean, received 'oops'"
),
],
}
);
});
});

describe.each(['classFields', 'classicDecorator', 'partialTransforms'])(
'%s (StringBooleanSchema)',
(fieldName) => {
test(`it parses \`{ ${fieldName}: true }\``, () => {
expectLogs(() => {
const config = parseConfig('test', { [fieldName]: true });
expect(config).toStrictEqual({ [fieldName]: true });
});
});

test(`it parses \`{ ${fieldName}: "true" }\``, () => {
expectLogs(() => {
const config = parseConfig('test', { [fieldName]: 'true' });
expect(config).toStrictEqual({ [fieldName]: true });
});
});

test(`it parses \`{ ${fieldName}: false }\``, () => {
expectLogs(() => {
const config = parseConfig('test', { [fieldName]: false });
expect(config).toStrictEqual({ [fieldName]: false });
});
});

test(`it parses \`{ ${fieldName}: "false" }\``, () => {
expectLogs(() => {
const config = parseConfig('test', { [fieldName]: 'false' });
expect(config).toStrictEqual({ [fieldName]: false });
});
});

test(`it logs an error for invalid \`${fieldName}\` config`, () => {
expectLogs(
() => {
const config = parseConfig('test', { [fieldName]: 'oops' });
expect(config).toStrictEqual({});
},
{
error: [
makeLogMatcher(
'[test]: CONFIG ERROR:',
`[${fieldName}] Expected boolean, received string`
),
],
}
);
});
}
);

describe('quote', () => {
test('it parses `{ quote: "single" }`', () => {
expectLogs(() => {
const config = parseConfig('test', { quote: 'single' });
expect(config).toStrictEqual({ quote: 'single' });
});
});

test('it parses `{ quote: "double" }`', () => {
expectLogs(() => {
const config = parseConfig('test', { quote: 'double' });
expect(config).toStrictEqual({ quote: 'double' });
});
});

test('it logs an error for invalid `quote` config', () => {
expectLogs(
() => {
const config = parseConfig('test', { quote: 'oops' });
expect(config).toStrictEqual({});
},
{
error: [
makeLogMatcher(
'[test]: CONFIG ERROR:',
"[quote] Expected 'single' or 'double', received 'oops"
),
],
}
);
});
});

describe('ignoreLeakingState', () => {
test('it parses `ignoreLeakingState` with an empty array', () => {
expectLogs(() => {
const config = parseConfig('test', { ignoreLeakingState: [] });
expect(config).toStrictEqual({ ignoreLeakingState: [] });
});
});

test('it parses `ignoreLeakingState` with array of strings', () => {
expectLogs(() => {
const config = parseConfig('test', {
ignoreLeakingState: ['one', 'two', 'three'],
});
expect(config).toStrictEqual({
ignoreLeakingState: ['one', 'two', 'three'],
});
});
});

test('it parses `ignoreLeakingState` with string of strings', () => {
expectLogs(() => {
const config = parseConfig('test', {
ignoreLeakingState: 'one,two , three',
});
expect(config).toStrictEqual({
ignoreLeakingState: ['one', 'two', 'three'],
});
});
});

test('it logs an error for invalid `ignoreLeakingState` config', () => {
expectLogs(
() => {
const config = parseConfig('test', { ignoreLeakingState: false });
expect(config).toStrictEqual({});
},
{
error: [
makeLogMatcher(
'[test]: CONFIG ERROR:',
'[ignoreLeakingState] Expected array of strings or comma-separated string, received false'
),
],
}
);
});
});

describe('type', () => {
test.each(['services', 'routes', 'components', 'controllers'])(
'it parses `{ type: "%s" }`',
(type) => {
expectLogs(() => {
const config = parseConfig('test', { type });
expect(config).toStrictEqual({ type });
});
}
);

test('it logs an error for invalid `type` config', () => {
expectLogs(
() => {
const config = parseConfig('test', { type: 'oops' });
expect(config).toStrictEqual({});
},
{
error: [
makeLogMatcher(
'[test]: CONFIG ERROR:',
"[type] Expected 'services', 'routes', 'components', or 'controllers', received 'oops"
),
],
}
);
});
});
});
});
Loading