diff --git a/package.json b/package.json
index 0ef3224f371ae..bb7822f56d387 100644
--- a/package.json
+++ b/package.json
@@ -79,6 +79,7 @@
"platform": "^1.1.0",
"prettier": "1.8.1",
"prop-types": "^15.6.0",
+ "random-seed": "^0.3.0",
"rimraf": "^2.6.1",
"rollup": "^0.51.7",
"rollup-plugin-babel": "^2.7.1",
diff --git a/packages/react-reconciler/src/__tests__/ReactNewContext-test.js b/packages/react-reconciler/src/__tests__/ReactNewContext-test.js
index 607b8385f7c7b..5886e695de8ea 100644
--- a/packages/react-reconciler/src/__tests__/ReactNewContext-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactNewContext-test.js
@@ -9,14 +9,16 @@
'use strict';
-let React;
+let React = require('React');
let ReactNoop;
+let gen;
describe('ReactNewContext', () => {
beforeEach(() => {
jest.resetModules();
React = require('react');
ReactNoop = require('react-noop-renderer');
+ gen = require('random-seed');
});
// function div(...children) {
@@ -303,4 +305,211 @@ describe('ReactNewContext', () => {
span('Result: 4'),
]);
});
+
+ describe('fuzz test', () => {
+ const contextKeys = ['A', 'B', 'C', 'D', 'E', 'F', 'G'];
+ const contexts = new Map(
+ contextKeys.map(key => {
+ const Context = React.createContext(0);
+ Context.displayName = 'Context' + key;
+ return [key, Context];
+ }),
+ );
+ const Fragment = React.Fragment;
+
+ const FLUSH_ALL = 'FLUSH_ALL';
+ function flushAll() {
+ return {
+ type: FLUSH_ALL,
+ toString() {
+ return `flushAll()`;
+ },
+ };
+ }
+
+ const FLUSH = 'FLUSH';
+ function flush(unitsOfWork) {
+ return {
+ type: FLUSH,
+ unitsOfWork,
+ toString() {
+ return `flush(${unitsOfWork})`;
+ },
+ };
+ }
+
+ const UPDATE = 'UPDATE';
+ function update(key, value) {
+ return {
+ type: UPDATE,
+ key,
+ value,
+ toString() {
+ return `update('${key}', ${value})`;
+ },
+ };
+ }
+
+ function randomInteger(min, max) {
+ min = Math.ceil(min);
+ max = Math.floor(max);
+ return Math.floor(Math.random() * (max - min)) + min;
+ }
+
+ function randomAction() {
+ switch (randomInteger(0, 3)) {
+ case 0:
+ return flushAll();
+ case 1:
+ return flush(randomInteger(0, 500));
+ case 2:
+ const key = contextKeys[randomInteger(0, contextKeys.length)];
+ const value = randomInteger(1, 10);
+ return update(key, value);
+ default:
+ throw new Error('Switch statement should be exhaustive');
+ }
+ }
+
+ function randomActions(n) {
+ let actions = [];
+ for (let i = 0; i < n; i++) {
+ actions.push(randomAction());
+ }
+ return actions;
+ }
+
+ class ConsumerTree extends React.Component {
+ shouldComponentUpdate() {
+ return false;
+ }
+ render() {
+ if (this.props.depth >= this.props.maxDepth) {
+ return null;
+ }
+ const consumers = [0, 1, 2].map(i => {
+ const randomKey =
+ contextKeys[this.props.rand.intBetween(0, contextKeys.length - 1)];
+ const Context = contexts.get(randomKey);
+ return Context.consume(
+ value => (
+
+
+
+
+ ),
+ i,
+ );
+ });
+ return consumers;
+ }
+ }
+
+ function Root(props) {
+ return contextKeys.reduceRight((children, key) => {
+ const Context = contexts.get(key);
+ const value = props.values[key];
+ return Context.provide(value, children);
+ }, );
+ }
+
+ const initialValues = contextKeys.reduce(
+ (result, key, i) => ({...result, [key]: i + 1}),
+ {},
+ );
+
+ function assertConsistentTree(expectedValues = {}) {
+ const children = ReactNoop.getChildren();
+ children.forEach(child => {
+ const text = child.prop;
+ const key = text[0];
+ const value = parseInt(text[2], 10);
+ const expectedValue = expectedValues[key];
+ if (expectedValue === undefined) {
+ // If an expected value was not explicitly passed to this function,
+ // use the first occurrence.
+ expectedValues[key] = value;
+ } else if (value !== expectedValue) {
+ throw new Error(
+ `Inconsistent value! Expected: ${key}:${expectedValue}. Actual: ${
+ text
+ }`,
+ );
+ }
+ });
+ }
+
+ function ContextSimulator(maxDepth) {
+ function simulate(seed, actions) {
+ const rand = gen.create(seed);
+ let finalExpectedValues = initialValues;
+ function updateRoot() {
+ ReactNoop.render(
+ ,
+ );
+ }
+ updateRoot();
+
+ actions.forEach(action => {
+ switch (action.type) {
+ case FLUSH_ALL:
+ ReactNoop.flush();
+ break;
+ case FLUSH:
+ ReactNoop.flushUnitsOfWork(action.unitsOfWork);
+ break;
+ case UPDATE:
+ finalExpectedValues = {
+ ...finalExpectedValues,
+ [action.key]: action.value,
+ };
+ updateRoot();
+ break;
+ default:
+ throw new Error('Switch statement should be exhaustive');
+ }
+ assertConsistentTree();
+ });
+
+ ReactNoop.flush();
+ assertConsistentTree(finalExpectedValues);
+ }
+
+ return {simulate};
+ }
+
+ it('hard-coded tests', () => {
+ const {simulate} = ContextSimulator(5);
+ simulate('randomSeed', [flush(3), update('A', 4)]);
+ });
+
+ it('generated tests', () => {
+ const {simulate} = ContextSimulator(5);
+
+ const LIMIT = 100;
+ for (let i = 0; i < LIMIT; i++) {
+ const seed = Math.random()
+ .toString(36)
+ .substr(2, 5);
+ const actions = randomActions(5);
+ try {
+ simulate(seed, actions);
+ } catch (error) {
+ console.error(`
+Context fuzz tester error! Copy and paste the following line into the test suite:
+ simulate('${seed}', ${actions.join(', ')});
+`);
+ throw error;
+ }
+ }
+ });
+ });
});
diff --git a/yarn.lock b/yarn.lock
index df11eb2e0b92c..e2457614fb730 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3045,7 +3045,7 @@ json-stable-stringify@^1.0.0, json-stable-stringify@^1.0.1:
dependencies:
jsonify "~0.0.0"
-json-stringify-safe@~5.0.1:
+json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
@@ -3811,6 +3811,12 @@ qs@~6.4.0:
version "6.4.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233"
+random-seed@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/random-seed/-/random-seed-0.3.0.tgz#d945f2e1f38f49e8d58913431b8bf6bb937556cd"
+ dependencies:
+ json-stringify-safe "^5.0.1"
+
randomatic@^1.1.3:
version "1.1.6"
resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.6.tgz#110dcabff397e9dcff7c0789ccc0a49adf1ec5bb"