Skip to content

Commit 52326c0

Browse files
authored
Make tests pass (#1)
Make tests pass
2 parents b1b27c0 + 104943b commit 52326c0

27 files changed

+5226
-3104
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules

babel.config.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
module.exports = {
2+
presets: ['@babel/preset-env', '@babel/preset-react'],
3+
plugins: [
4+
'@babel/plugin-proposal-class-properties',
5+
process.env.NODE_ENV === 'test' &&
6+
'@babel/plugin-transform-react-jsx-source',
7+
].filter(Boolean),
8+
};

index.js

Lines changed: 0 additions & 16 deletions
This file was deleted.

jest.config.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports = {
2+
setupFilesAfterEnv: ['./scripts/jest/setupTests.js'],
3+
};

npm/index.js

Lines changed: 0 additions & 7 deletions
This file was deleted.

package.json

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,20 @@
2424
"react-is": "^16.8.6",
2525
"scheduler": "^0.18.0"
2626
},
27+
"devDependencies": {
28+
"@babel/cli": "^7.8.3",
29+
"@babel/core": "^7.8.3",
30+
"@babel/plugin-proposal-class-properties": "^7.8.3",
31+
"@babel/plugin-syntax-class-properties": "^7.8.3",
32+
"@babel/plugin-transform-react-jsx-source": "^7.8.3",
33+
"@babel/preset-env": "^7.8.3",
34+
"@babel/preset-flow": "^7.8.3",
35+
"@babel/preset-react": "^7.8.3",
36+
"babel-jest": "^25.1.0",
37+
"jest": "^25.1.0",
38+
"jest-diff": "^25.1.0",
39+
"react": "^16.12.0"
40+
},
2741
"peerDependencies": {
2842
"react": "^16.0.0"
2943
},
@@ -35,5 +49,9 @@
3549
"shallow.js",
3650
"cjs/",
3751
"umd/"
38-
]
52+
],
53+
"scripts": {
54+
"test": "jest",
55+
"test:debug": "node --inspect-brk node_modules/jest/bin/jest.js --runInBand --no-cache"
56+
}
3957
}

scripts/jest/matchers/toWarnDev.js

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
'use strict';
2+
3+
const jestDiff = require('jest-diff').default;
4+
const util = require('util');
5+
const shouldIgnoreConsoleError = require('../shouldIgnoreConsoleError');
6+
7+
function normalizeCodeLocInfo(str) {
8+
return str && str.replace(/at .+?:\d+/g, 'at **');
9+
}
10+
11+
const createMatcherFor = (consoleMethod, matcherName) =>
12+
function matcher(callback, expectedMessages, options = {}) {
13+
if (process.env.NODE_ENV !== 'production') {
14+
// Warn about incorrect usage of matcher.
15+
if (typeof expectedMessages === 'string') {
16+
expectedMessages = [expectedMessages];
17+
} else if (!Array.isArray(expectedMessages)) {
18+
throw Error(
19+
`${matcherName}() requires a parameter of type string or an array of strings ` +
20+
`but was given ${typeof expectedMessages}.`,
21+
);
22+
}
23+
if (
24+
options != null &&
25+
(typeof options !== 'object' || Array.isArray(options))
26+
) {
27+
throw new Error(
28+
`${matcherName}() second argument, when present, should be an object. ` +
29+
'Did you forget to wrap the messages into an array?',
30+
);
31+
}
32+
if (arguments.length > 3) {
33+
// `matcher` comes from Jest, so it's more than 2 in practice
34+
throw new Error(
35+
`${matcherName}() received more than two arguments. ` +
36+
'Did you forget to wrap the messages into an array?',
37+
);
38+
}
39+
40+
const withoutStack = options.withoutStack;
41+
const logAllErrors = options.logAllErrors;
42+
const warningsWithoutComponentStack = [];
43+
const warningsWithComponentStack = [];
44+
const unexpectedWarnings = [];
45+
46+
let lastWarningWithMismatchingFormat = null;
47+
let lastWarningWithExtraComponentStack = null;
48+
49+
// Catch errors thrown by the callback,
50+
// But only rethrow them if all test expectations have been satisfied.
51+
// Otherwise an Error in the callback can mask a failed expectation,
52+
// and result in a test that passes when it shouldn't.
53+
let caughtError;
54+
55+
const isLikelyAComponentStack = message =>
56+
typeof message === 'string' && message.includes('\n in ');
57+
58+
const consoleSpy = (format, ...args) => {
59+
// Ignore uncaught errors reported by jsdom
60+
// and React addendums because they're too noisy.
61+
if (
62+
!logAllErrors &&
63+
consoleMethod === 'error' &&
64+
shouldIgnoreConsoleError(format, args)
65+
) {
66+
return;
67+
}
68+
69+
const message = util.format(format, ...args);
70+
const normalizedMessage = normalizeCodeLocInfo(message);
71+
72+
// Remember if the number of %s interpolations
73+
// doesn't match the number of arguments.
74+
// We'll fail the test if it happens.
75+
let argIndex = 0;
76+
format.replace(/%s/g, () => argIndex++);
77+
if (argIndex !== args.length) {
78+
lastWarningWithMismatchingFormat = {
79+
format,
80+
args,
81+
expectedArgCount: argIndex,
82+
};
83+
}
84+
85+
// Protect against accidentally passing a component stack
86+
// to warning() which already injects the component stack.
87+
if (
88+
args.length >= 2 &&
89+
isLikelyAComponentStack(args[args.length - 1]) &&
90+
isLikelyAComponentStack(args[args.length - 2])
91+
) {
92+
lastWarningWithExtraComponentStack = {
93+
format,
94+
};
95+
}
96+
97+
for (let index = 0; index < expectedMessages.length; index++) {
98+
const expectedMessage = expectedMessages[index];
99+
if (
100+
normalizedMessage === expectedMessage ||
101+
normalizedMessage.includes(expectedMessage)
102+
) {
103+
if (isLikelyAComponentStack(normalizedMessage)) {
104+
warningsWithComponentStack.push(normalizedMessage);
105+
} else {
106+
warningsWithoutComponentStack.push(normalizedMessage);
107+
}
108+
expectedMessages.splice(index, 1);
109+
return;
110+
}
111+
}
112+
113+
let errorMessage;
114+
if (expectedMessages.length === 0) {
115+
errorMessage =
116+
'Unexpected warning recorded: ' +
117+
this.utils.printReceived(normalizedMessage);
118+
} else if (expectedMessages.length === 1) {
119+
errorMessage =
120+
'Unexpected warning recorded: ' +
121+
jestDiff(expectedMessages[0], normalizedMessage);
122+
} else {
123+
errorMessage =
124+
'Unexpected warning recorded: ' +
125+
jestDiff(expectedMessages, [normalizedMessage]);
126+
}
127+
128+
// Record the call stack for unexpected warnings.
129+
// We don't throw an Error here though,
130+
// Because it might be suppressed by ReactFiberScheduler.
131+
unexpectedWarnings.push(new Error(errorMessage));
132+
};
133+
134+
// TODO Decide whether we need to support nested toWarn* expectations.
135+
// If we don't need it, add a check here to see if this is already our spy,
136+
// And throw an error.
137+
const originalMethod = console[consoleMethod];
138+
139+
// Avoid using Jest's built-in spy since it can't be removed.
140+
console[consoleMethod] = consoleSpy;
141+
142+
try {
143+
callback();
144+
} catch (error) {
145+
caughtError = error;
146+
} finally {
147+
// Restore the unspied method so that unexpected errors fail tests.
148+
console[consoleMethod] = originalMethod;
149+
150+
// Any unexpected Errors thrown by the callback should fail the test.
151+
// This should take precedence since unexpected errors could block warnings.
152+
if (caughtError) {
153+
throw caughtError;
154+
}
155+
156+
// Any unexpected warnings should be treated as a failure.
157+
if (unexpectedWarnings.length > 0) {
158+
return {
159+
message: () => unexpectedWarnings[0].stack,
160+
pass: false,
161+
};
162+
}
163+
164+
// Any remaining messages indicate a failed expectations.
165+
if (expectedMessages.length > 0) {
166+
return {
167+
message: () =>
168+
`Expected warning was not recorded:\n ${this.utils.printReceived(
169+
expectedMessages[0],
170+
)}`,
171+
pass: false,
172+
};
173+
}
174+
175+
if (typeof withoutStack === 'number') {
176+
// We're expecting a particular number of warnings without stacks.
177+
if (withoutStack !== warningsWithoutComponentStack.length) {
178+
return {
179+
message: () =>
180+
`Expected ${withoutStack} warnings without a component stack but received ${warningsWithoutComponentStack.length}:\n` +
181+
warningsWithoutComponentStack.map(warning =>
182+
this.utils.printReceived(warning),
183+
),
184+
pass: false,
185+
};
186+
}
187+
} else if (withoutStack === true) {
188+
// We're expecting that all warnings won't have the stack.
189+
// If some warnings have it, it's an error.
190+
if (warningsWithComponentStack.length > 0) {
191+
return {
192+
message: () =>
193+
`Received warning unexpectedly includes a component stack:\n ${this.utils.printReceived(
194+
warningsWithComponentStack[0],
195+
)}\nIf this warning intentionally includes the component stack, remove ` +
196+
`{withoutStack: true} from the ${matcherName}() call. If you have a mix of ` +
197+
`warnings with and without stack in one ${matcherName}() call, pass ` +
198+
`{withoutStack: N} where N is the number of warnings without stacks.`,
199+
pass: false,
200+
};
201+
}
202+
} else if (withoutStack === false || withoutStack === undefined) {
203+
// We're expecting that all warnings *do* have the stack (default).
204+
// If some warnings don't have it, it's an error.
205+
if (warningsWithoutComponentStack.length > 0) {
206+
return {
207+
message: () =>
208+
`Received warning unexpectedly does not include a component stack:\n ${this.utils.printReceived(
209+
warningsWithoutComponentStack[0],
210+
)}\nIf this warning intentionally omits the component stack, add ` +
211+
`{withoutStack: true} to the ${matcherName} call.`,
212+
pass: false,
213+
};
214+
}
215+
} else {
216+
throw Error(
217+
`The second argument for ${matcherName}(), when specified, must be an object. It may have a ` +
218+
`property called "withoutStack" whose value may be undefined, boolean, or a number. ` +
219+
`Instead received ${typeof withoutStack}.`,
220+
);
221+
}
222+
223+
if (lastWarningWithMismatchingFormat !== null) {
224+
return {
225+
message: () =>
226+
`Received ${
227+
lastWarningWithMismatchingFormat.args.length
228+
} arguments for a message with ${
229+
lastWarningWithMismatchingFormat.expectedArgCount
230+
} placeholders:\n ${this.utils.printReceived(
231+
lastWarningWithMismatchingFormat.format,
232+
)}`,
233+
pass: false,
234+
};
235+
}
236+
237+
if (lastWarningWithExtraComponentStack !== null) {
238+
return {
239+
message: () =>
240+
`Received more than one component stack for a warning:\n ${this.utils.printReceived(
241+
lastWarningWithExtraComponentStack.format,
242+
)}\nDid you accidentally pass a stack to warning() as the last argument? ` +
243+
`Don't forget warning() already injects the component stack automatically.`,
244+
pass: false,
245+
};
246+
}
247+
248+
return {pass: true};
249+
}
250+
} else {
251+
// Any uncaught errors or warnings should fail tests in production mode.
252+
callback();
253+
254+
return {pass: true};
255+
}
256+
};
257+
258+
module.exports = {
259+
toWarnDev: createMatcherFor('warn', 'toWarnDev'),
260+
toErrorDev: createMatcherFor('error', 'toErrorDev'),
261+
};

scripts/jest/setupTests.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
expect.extend(require('./matchers/toWarnDev'));
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
'use strict';
2+
3+
module.exports = function shouldIgnoreConsoleError(format, args) {
4+
if (process.env.NODE_ENV !== 'production') {
5+
if (typeof format === 'string') {
6+
if (format.indexOf('Error: Uncaught [') === 0) {
7+
// This looks like an uncaught error from invokeGuardedCallback() wrapper
8+
// in development that is reported by jsdom. Ignore because it's noisy.
9+
return true;
10+
}
11+
if (format.indexOf('The above error occurred') === 0) {
12+
// This looks like an error addendum from ReactFiberErrorLogger.
13+
// Ignore it too.
14+
return true;
15+
}
16+
}
17+
} else {
18+
if (
19+
format != null &&
20+
typeof format.message === 'string' &&
21+
typeof format.stack === 'string' &&
22+
args.length === 0
23+
) {
24+
// In production, ReactFiberErrorLogger logs error objects directly.
25+
// They are noisy too so we'll try to ignore them.
26+
return true;
27+
}
28+
if (
29+
format.indexOf(
30+
'act(...) is not supported in production builds of React',
31+
) === 0
32+
) {
33+
// We don't yet support act() for prod builds, and warn for it.
34+
// But we'd like to use act() ourselves for prod builds.
35+
// Let's ignore the warning and #yolo.
36+
return true;
37+
}
38+
}
39+
// Looks legit
40+
return false;
41+
};

0 commit comments

Comments
 (0)