Skip to content

Commit

Permalink
Shared implementation of disallowWarnings and disallowConsoleErrors
Browse files Browse the repository at this point in the history
Summary: Abstract out the implementation of disallowWarnings so that it can be shared with disallowConsoleErrors. This then allows us to have expected console errors, which wasn't implemented before.

Reviewed By: josephsavona

Differential Revision: D34540723

fbshipit-source-id: 93d526b2bb9bfb7e13ceb2a2bfb6b8b314e82122
  • Loading branch information
davidmccabe authored and facebook-github-bot committed Mar 9, 2022
1 parent 8313676 commit 3497ce7
Show file tree
Hide file tree
Showing 5 changed files with 192 additions and 83 deletions.
6 changes: 3 additions & 3 deletions packages/relay-test-utils-internal/__tests__/warnings-test.js
Expand Up @@ -31,7 +31,7 @@ describe('warnings', () => {
});
it('throws when disallow warnings is called twice', () => {
expect(disallowWarnings).toThrowError(
'`disallowWarnings` should be called at most once',
'disallowWarnings should be called only once',
);
});
it('throws when unexpected warning is fired', () => {
Expand All @@ -56,7 +56,7 @@ describe('warnings', () => {

it('warns on unfired contextual warning', () => {
expect(() => expectToWarn(expected_message1, () => {})).toThrowError(
'Expected callback to warn: ' + expected_message1,
'Expected warning in callback: ' + expected_message1,
);
});

Expand Down Expand Up @@ -96,6 +96,6 @@ describe('warnings', () => {
warning(false, expected_message2);
},
);
}).toThrowError('Expected callback to warn: ' + expected_message3);
}).toThrowError('Expected warning in callback: ' + expected_message3);
});
});
61 changes: 42 additions & 19 deletions packages/relay-test-utils-internal/consoleError.js
Expand Up @@ -11,32 +11,55 @@

'use strict';

/* global jest, afterEach */
/* global jest */

let installed = false;
const {createConsoleInterceptionSystem} = require('./consoleErrorsAndWarnings');

const consoleErrorsSystem = createConsoleInterceptionSystem(
'error',
'expectConsoleError',
impl => {
jest.spyOn(console, 'error').mockImplementation(impl);
},
);

/**
* Similar to disallowWarnings.
* This method mocks the console.error and throws in the error is printed in the console.
* Mocks console.error so that errors printed to the console are instead thrown.
* Any expected errors need to be explicitly expected with `expectConsoleErrorWillFire(message)`.
*
* NOTE: This should be called on top of a test file. The test should NOT
* use `jest.resetModules()` or manually mock `console`.
*/
function disallowConsoleErrors(): void {
if (installed) {
throw new Error('`disallowConsoleErrors` should be called at most once');
}
installed = true;
let errors = [];
jest.spyOn(console, 'error').mockImplementation(message => {
errors.push(`Unexpected \`console.error\`:\n${message}.`);
});
afterEach(() => {
if (errors.length > 0) {
const message = errors.join('\n');
errors = [];
throw new Error(message);
}
});
consoleErrorsSystem.disallowMessages();
}

/**
* Expect an error with the given message. If the message isn't fired in the
* current test, the test will fail.
*/
function expectConsoleErrorWillFire(message: string): void {
consoleErrorsSystem.expectMessageWillFire(message);
}

/**
* Expect the callback `fn` to print an error with the message, and otherwise fail.
*/
function expectConsoleError<T>(message: string, fn: () => T): T {
return consoleErrorsSystem.expectMessage(message, fn);
}

/**
* Expect the callback `fn` to trigger all console errors (in sequence),
* and otherwise fail.
*/
function expectConsoleErrorsMany<T>(messages: Array<string>, fn: () => T): T {
return consoleErrorsSystem.expectMessageMany(messages, fn);
}

module.exports = {
disallowConsoleErrors,
expectConsoleErrorWillFire,
expectConsoleError,
expectConsoleErrorsMany,
};
116 changes: 116 additions & 0 deletions packages/relay-test-utils-internal/consoleErrorsAndWarnings.js
@@ -0,0 +1,116 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails oncall+relay
* @flow strict-local
* @format
*/

'use strict';

/* global jest, afterEach */

type API = $ReadOnly<{|
disallowMessages: () => void,
expectMessageWillFire: string => void,
expectMessage: <T>(string, () => T) => T,
expectMessageMany: <T>(Array<string>, () => T) => T,
|}>;

const originalConsoleError = console.error;

function createConsoleInterceptionSystem(
typename: string,
expectFunctionName: string,
setUpMock: ((string) => void) => void,
): API {
let installed = false;
const expectedMessages: Array<string> = [];
const contextualExpectedMessage: Array<string> = [];

const typenameCap = typename.charAt(0).toUpperCase() + typename.slice(1);
const typenameCapPlural = typenameCap + 's';
const installerName = `disallow${typenameCap}s`;

function handleMessage(message: string): void {
const index = expectedMessages.findIndex(expected =>
message.startsWith(expected),
);
if (
contextualExpectedMessage.length > 0 &&
contextualExpectedMessage[0] === message
) {
contextualExpectedMessage.shift();
} else if (index >= 0) {
expectedMessages.splice(index, 1);
} else {
// log to console in case the error gets swallowed somewhere
originalConsoleError(`Unexpected ${typenameCap}: ` + message);
throw new Error(`${typenameCap}: ` + message);
}
}

function disallowMessages(): void {
if (installed) {
throw new Error(`${installerName} should be called only once.`);
}
installed = true;
setUpMock(handleMessage);

afterEach(() => {
contextualExpectedMessage.length = 0;
if (expectedMessages.length > 0) {
const error = new Error(
`Some expected ${typename}s where not triggered:\n\n` +
Array.from(expectedMessages, message => ` * ${message}`).join(
'\n',
) +
'\n',
);
expectedMessages.length = 0;
throw error;
}
});
}

function expectMessageWillFire(message: string): void {
if (!installed) {
throw new Error(
`${installerName} needs to be called before expect${typenameCapPlural}WillFire`,
);
}
expectedMessages.push(message);
}

function expectMessage<T>(message: string, fn: () => T): T {
return expectMessageMany([message], fn);
}

function expectMessageMany<T>(messages: Array<string>, fn: () => T): T {
if (contextualExpectedMessage.length > 0) {
throw new Error(`Cannot nest ${expectFunctionName}() calls.`);
}
contextualExpectedMessage.push(...messages);
const result = fn();
if (contextualExpectedMessage.length > 0) {
const notFired = contextualExpectedMessage.toString();
contextualExpectedMessage.length = 0;
throw new Error(`Expected ${typename} in callback: ${notFired}`);
}
return result;
}

return {
disallowMessages,
expectMessageWillFire,
expectMessage,
expectMessageMany,
};
}

module.exports = {
createConsoleInterceptionSystem,
};
10 changes: 9 additions & 1 deletion packages/relay-test-utils-internal/index.js
Expand Up @@ -12,7 +12,12 @@

'use strict';

const {disallowConsoleErrors} = require('./consoleError');
const {
disallowConsoleErrors,
expectConsoleError,
expectConsoleErrorsMany,
expectConsoleErrorWillFire,
} = require('./consoleError');
const describeWithFeatureFlags = require('./describeWithFeatureFlags');
const {
FIXTURE_TAG,
Expand Down Expand Up @@ -53,6 +58,9 @@ module.exports = {
describeWithFeatureFlags,
disallowConsoleErrors,
disallowWarnings,
expectConsoleError,
expectConsoleErrorsMany,
expectConsoleErrorWillFire,
expectToWarn,
expectToWarnMany,
expectWarningWillFire,
Expand Down
82 changes: 22 additions & 60 deletions packages/relay-test-utils-internal/warnings.js
Expand Up @@ -11,11 +11,25 @@

'use strict';

/* global jest, afterEach */
/* global jest */

let installed = false;
const expectedWarnings: Array<string> = [];
const contextualExpectedWarning: Array<string> = [];
const {createConsoleInterceptionSystem} = require('./consoleErrorsAndWarnings');

const warningsSystem = createConsoleInterceptionSystem(
'warning',
'expectToWarn',
impl => {
jest.mock('warning', () =>
jest.fn((condition, format, ...args) => {
if (!condition) {
let argIndex = 0;
const message = format.replace(/%s/g, () => String(args[argIndex++]));
impl(message);
}
}),
);
},
);

/**
* Mocks the `warning` module to turn warnings into errors. Any expected
Expand All @@ -25,82 +39,30 @@ const contextualExpectedWarning: Array<string> = [];
* use `jest.resetModules()` or manually mock `warning`.
*/
function disallowWarnings(): void {
if (installed) {
throw new Error('`disallowWarnings` should be called at most once');
}
installed = true;
jest.mock('warning', () => {
return jest.fn((condition, format, ...args) => {
if (!condition) {
let argIndex = 0;
const message = format.replace(/%s/g, () => String(args[argIndex++]));
const index = expectedWarnings.indexOf(message);

if (
contextualExpectedWarning.length > 0 &&
contextualExpectedWarning[0] === message
) {
contextualExpectedWarning.shift();
} else if (index >= 0) {
expectedWarnings.splice(index, 1);
} else {
// log to console in case the error gets swallowed somewhere
console.error('Unexpected Warning: ' + message);
throw new Error('Warning: ' + message);
}
}
});
});
afterEach(() => {
contextualExpectedWarning.length = 0;
if (expectedWarnings.length > 0) {
const error = new Error(
'Some expected warnings where not triggered:\n\n' +
Array.from(expectedWarnings, message => ` * ${message}`).join('\n') +
'\n',
);
expectedWarnings.length = 0;
throw error;
}
});
warningsSystem.disallowMessages();
}

/**
* Expect a warning with the given message. If the message isn't fired in the
* current test, the test will fail.
*/
function expectWarningWillFire(message: string): void {
if (!installed) {
throw new Error(
'`disallowWarnings` needs to be called before `expectWarningWillFire`',
);
}
expectedWarnings.push(message);
warningsSystem.expectMessageWillFire(message);
}

/**
* Expect the callback `fn` to trigger the warning message and otherwise fail.
*/
function expectToWarn<T>(message: string, fn: () => T): T {
return expectToWarnMany([message], fn);
return warningsSystem.expectMessage(message, fn);
}

/**
* Expect the callback `fn` to trigger all warning messages (in sequence)
* or otherwise fail.
*/
function expectToWarnMany<T>(messages: Array<string>, fn: () => T): T {
if (contextualExpectedWarning.length > 0) {
throw new Error('Cannot nest `expectToWarn()` calls.');
}
contextualExpectedWarning.push(...messages);
const result = fn();
if (contextualExpectedWarning.length > 0) {
const notFired = contextualExpectedWarning.toString();
contextualExpectedWarning.length = 0;
throw new Error(`Expected callback to warn: ${notFired}`);
}
return result;
return warningsSystem.expectMessageMany(messages, fn);
}

module.exports = {
Expand Down

0 comments on commit 3497ce7

Please sign in to comment.