diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalTriangle-test.js b/packages/react-reconciler/src/__tests__/ReactIncrementalTriangle-test.js index f122379299..485225f98a 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncrementalTriangle-test.js +++ b/packages/react-reconciler/src/__tests__/ReactIncrementalTriangle-test.js @@ -62,7 +62,77 @@ describe('ReactIncrementalTriangle', () => { }; } - function TriangleSimulator() { + const STOP = 'STOP'; + + function randomInteger(min, max) { + min = Math.ceil(min); + max = Math.floor(max); + return Math.floor(Math.random() * (max - min)) + min; + } + + function formatAction(action) { + switch (action.type) { + case FLUSH: + return `flush(${action.unitsOfWork})`; + case STEP: + return `step(${action.counter})`; + case INTERRUPT: + return 'interrupt()'; + case TOGGLE: + return `toggle(${action.childIndex})`; + case EXPIRE: + return `expire(${action.ms})`; + default: + throw new Error('Switch statement should be exhaustive'); + } + } + + function formatActions(actions) { + let result = 'simulate('; + for (let i = 0; i < actions.length; i++) { + const action = actions[i]; + result += formatAction(action); + if (i !== actions.length - 1) { + result += ', '; + } + } + result += ')'; + return result; + } + + const MAX_DEPTH = 3; + const TOTAL_CHILDREN = Math.pow(3, MAX_DEPTH); + let TOTAL_TRIANGLES = 0; + for (let i = 0; i <= MAX_DEPTH; i++) { + TOTAL_TRIANGLES += Math.pow(3, i); + } + + function randomAction() { + switch (randomInteger(0, 5)) { + case 0: + return flush(randomInteger(0, TOTAL_TRIANGLES * 1.5)); + case 1: + return step(randomInteger(0, 10)); + case 2: + return interrupt(); + case 3: + return toggle(randomInteger(0, TOTAL_CHILDREN)); + case 4: + return expire(randomInteger(0, 1500)); + 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; + } + + function TriangleSimulator(rootID) { let triangles = []; let leafTriangles = []; let yieldAfterEachRender = false; @@ -132,27 +202,35 @@ describe('ReactIncrementalTriangle', () => { } } - const depth = 3; - let keyCounter = 0; function reset(nextStep = 0) { triangles = []; leafTriangles = []; // Remounts the whole tree by changing the key - ReactNoop.render(); + if (rootID) { + ReactNoop.renderToRootWithID( + , + rootID, + ); + } else { + ReactNoop.render(); + } ReactNoop.flush(); assertConsistentTree(); return appInstance; } reset(); - const totalChildren = leafTriangles.length; - const totalTriangles = triangles.length; function assertConsistentTree(activeTriangle, counter) { const activeIndex = activeTriangle ? activeTriangle.leafIndex : -1; - const children = ReactNoop.getChildren(); + const children = ReactNoop.getChildren(rootID); + + if (children.length !== TOTAL_CHILDREN) { + throw new Error('Wrong number of children.'); + } + for (let i = 0; i < children.length; i++) { let child = children[i]; let num = child.prop; @@ -183,14 +261,17 @@ describe('ReactIncrementalTriangle', () => { } } - function simulate(...actions) { + function* simulateAndYield() { const app = reset(); let expectedCounterAtEnd = app.state.counter; let activeTriangle = null; - for (var i = 0; i < actions.length; i++) { + while (true) { + var action = yield; + if (action === STOP) { + break; + } ReactNoop.flushSync(() => { - const action = actions[i]; switch (action.type) { case FLUSH: ReactNoop.flushUnitsOfWork(action.unitsOfWork); @@ -234,84 +315,83 @@ describe('ReactIncrementalTriangle', () => { assertConsistentTree(activeTriangle, expectedCounterAtEnd); } - return {simulate, totalChildren, totalTriangles}; + function simulate(...actions) { + const gen = simulateAndYield(); + for (let action of actions) { + gen.next(action); + } + gen.next(STOP); + } + + return { + simulateAndYield, + simulate, + randomAction, + randomActions, + }; } - it('renders the triangle demo without inconsistencies', () => { - const {simulate} = TriangleSimulator(); - simulate(step(1)); - simulate(toggle(0), step(1), toggle(0)); - simulate(step(1), toggle(0), flush(2), step(2), toggle(0)); - simulate(step(1), flush(3), toggle(0), step(0)); - simulate(step(1), flush(3), toggle(18), step(0)); - simulate(step(4), flush(52), expire(1476), flush(17), step(0)); - simulate(interrupt(), toggle(10), step(2), expire(990), flush(46)); - simulate(interrupt(), step(6), step(7), toggle(6), interrupt()); + describe('single root', () => { + // These tests are not deterministic because the inputs are randomized. It + // runs a limited number of tests on every run. If it fails, it will output + // the case that led to the failure. Add the failing case to the test above + // to prevent future regressions. + it('hard-coded tests', () => { + const {simulate} = TriangleSimulator(); + simulate(step(1)); + simulate(toggle(0), step(1), toggle(0)); + simulate(step(1), toggle(0), flush(2), step(2), toggle(0)); + simulate(step(1), flush(3), toggle(0), step(0)); + simulate(step(1), flush(3), toggle(18), step(0)); + simulate(step(4), flush(52), expire(1476), flush(17), step(0)); + simulate(interrupt(), toggle(10), step(2), expire(990), flush(46)); + simulate(interrupt(), step(6), step(7), toggle(6), interrupt()); + }); + + it('generative tests', () => { + const {simulate} = TriangleSimulator(); + + const limit = 1000; + + for (let i = 0; i < limit; i++) { + const actions = randomActions(5); + try { + simulate(...actions); + } catch (e) { + console.error( + `Triangle fuzz tester error! Copy and paste the following line into the test suite: +${formatActions(actions)} + `, + ); + throw e; + } + } + }); }); - it('fuzz tester', () => { - // This test is not deterministic because the inputs are randomized. It runs - // a limited number of tests on every run. If it fails, it will output the - // case that led to the failure. Add the failing case to the test above - // to prevent future regressions. - const {simulate, totalTriangles, totalChildren} = TriangleSimulator(); + describe('multiple roots', () => { + const rootIDs = ['a', 'b', 'c']; - const limit = 1000; - - 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, 5)) { - case 0: - return flush(randomInteger(0, totalTriangles * 1.5)); - case 1: - return step(randomInteger(0, 10)); - case 2: - return interrupt(); - case 3: - return toggle(randomInteger(0, totalChildren)); - case 4: - return expire(randomInteger(0, 1500)); - default: - throw new Error('Switch statement should be exhaustive'); + function randomActionsPerRoot() { + function randomRootID() { + const index = randomInteger(0, rootIDs.length); + return rootIDs[index]; } - } - function randomActions(n) { - let actions = []; - for (let i = 0; i < n; i++) { - actions.push(randomAction()); + const actions = []; + for (let i = 0; i < 10; i++) { + const rootID = randomRootID(); + const action = randomAction(); + actions.push([rootID, action]); } return actions; } - function formatActions(actions) { - let result = 'simulate('; + function formatActionsPerRoot(actions) { + let result = 'simulateMultipleRoots('; for (let i = 0; i < actions.length; i++) { - const action = actions[i]; - switch (action.type) { - case FLUSH: - result += `flush(${action.unitsOfWork})`; - break; - case STEP: - result += `step(${action.counter})`; - break; - case INTERRUPT: - result += 'interrupt()'; - break; - case TOGGLE: - result += `toggle(${action.childIndex})`; - break; - case EXPIRE: - result += `expire(${action.ms})`; - break; - default: - throw new Error('Switch statement should be exhaustive'); - } + const [rootID, action] = actions[i]; + result += `['${rootID}', ${formatAction(action)}]`; if (i !== actions.length - 1) { result += ', '; } @@ -320,19 +400,52 @@ describe('ReactIncrementalTriangle', () => { return result; } - for (let i = 0; i < limit; i++) { - const actions = randomActions(5); - try { - simulate(...actions); - } catch (e) { - console.error( - ` -Triangle fuzz tester error! Copy and paste the following line into the test suite: - ${formatActions(actions)} - `, - ); - throw e; + function simulateMultipleRoots(...actions) { + const roots = new Map(); + for (let rootID of rootIDs) { + const simulator = TriangleSimulator(rootID); + const generator = simulator.simulateAndYield(); + // Call this once to prepare the generator + generator.next(); + roots.set(rootID, generator); } + + actions.forEach(([rootID, action]) => { + const generator = roots.get(rootID); + generator.next(action); + }); + roots.forEach(generator => { + generator.next(STOP); + }); } + + it('hard-coded tests', () => { + simulateMultipleRoots( + ['b', interrupt()], + ['a', toggle(22)], + ['c', step(4)], + ['a', expire(10)], + ['a', interrupt()], + ['c', step(2)], + ['b', interrupt()], + ); + }); + + it('generative tests', () => { + const limit = 100; + for (let i = 0; i < limit; i++) { + const actions = randomActionsPerRoot(); + try { + simulateMultipleRoots(...actions); + } catch (e) { + console.error( + `Triangle fuzz tester error! Copy and paste the following line into the test suite: +${formatActionsPerRoot(actions)} + `, + ); + throw e; + } + } + }); }); });