Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Failing test case for useMutableSource #18296

Closed
wants to merge 3 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -43,8 +43,8 @@ describe('useMutableSource', () => {
const callbacksA = [];
const callbacksB = [];
let revision = 0;
let valueA = 'a:one';
let valueB = 'b:one';
let valueA = initialValueA;
let valueB = initialValueB;

const subscribeHelper = (callbacks, callback) => {
if (callbacks.indexOf(callback) < 0) {
Expand Down Expand Up @@ -630,7 +630,7 @@ describe('useMutableSource', () => {
});

it('should only update components whose subscriptions fire', () => {
const source = createComplexSource('one', 'one');
const source = createComplexSource('a:one', 'b:one');
const mutableSource = createMutableSource(source);

// Subscribe to part of the store.
Expand Down Expand Up @@ -672,7 +672,7 @@ describe('useMutableSource', () => {
});

it('should detect tearing in part of the store not yet subscribed to', () => {
const source = createComplexSource('one', 'one');
const source = createComplexSource('a:one', 'b:one');
const mutableSource = createMutableSource(source);

// Subscribe to part of the store.
Expand Down Expand Up @@ -1265,6 +1265,248 @@ describe('useMutableSource', () => {
expect(Scheduler).toHaveYielded(['x: bar, y: bar']);
});

it('getSnapshot changes and then source is mutated in between paint and passive effect phase', async () => {
const source = createSource({
a: 'foo',
b: 'bar',
});
const mutableSource = createMutableSource(source);

function mutateB(newB) {
source.value = {
...source.value,
b: newB,
};
}

const getSnapshotA = () => source.value.a;
const getSnapshotB = () => source.value.b;

function App({getSnapshot}) {
const value = useMutableSource(
mutableSource,
getSnapshot,
defaultSubscribe,
);

Scheduler.unstable_yieldValue('Render: ' + value);
React.useEffect(() => {
Scheduler.unstable_yieldValue('Commit: ' + value);
}, [value]);

return value;
}

const root = ReactNoop.createRoot();
await act(async () => {
root.render(<App getSnapshot={getSnapshotA} />);
});
expect(Scheduler).toHaveYielded(['Render: foo', 'Commit: foo']);

await act(async () => {
// Switch getSnapshot to read from B instead
root.render(<App getSnapshot={getSnapshotB} />);
// Render and finish the tree, but yield right after paint, before
// the passive effects have fired.
expect(Scheduler).toFlushUntilNextPaint(['Render: bar']);
// Then mutate B.
mutateB('baz');
});
expect(Scheduler).toHaveYielded([
// Fires the effect from the previous render
'Commit: bar',
// During that effect, it should detect that the snapshot has changed
// and re-render.
'Render: baz',
'Commit: baz',
]);
expect(root).toMatchRenderedOutput('baz');
});

it('getSnapshot changes and then source is mutated in between paint and passive effect phase, case 2', async () => {
const source = createSource({
a: 'foo',
b: 'bar',
});
const mutableSource = createMutableSource(source);

const getSnapshotA = () => source.value.a;
const getSnapshotB = () => source.value.b;

function mutateA(newA) {
source.value = {
...source.value,
a: newA,
};
}

function App({getSnapshotFirst, getSnapshotSecond}) {
const first = useMutableSource(
mutableSource,
getSnapshotFirst,
defaultSubscribe,
);
const second = useMutableSource(
mutableSource,
getSnapshotSecond,
defaultSubscribe,
);

let result = `x: ${first}, y: ${second}`;

if (getSnapshotFirst === getSnapshotSecond) {
// When both getSnapshot functions are equal,
// the two values must be consistent.
if (first !== second) {
result = 'Oops, tearing!';
}
}

return result;
}

const root = ReactNoop.createRoot();
await act(async () => {
root.render(
<App
getSnapshotFirst={getSnapshotA}
getSnapshotSecond={getSnapshotB}
/>,
);
});

await act(async () => {
// Switch the second getSnapshot to also read from A
root.render(
<App
getSnapshotFirst={getSnapshotA}
getSnapshotSecond={getSnapshotA}
/>,
);
// Render and finish the tree, but yield right after paint, before
// the passive effects have fired.
expect(Scheduler).toFlushUntilNextPaint([]);
// Now mutate A. Both hooks should update.
// This is at high priority so that it doesn't get batched with default
// priority updates that might fire during the passive effect
ReactNoop.discreteUpdates(() => {
mutateA('baz');
});
expect(Scheduler).toFlushUntilNextPaint([]);
expect(root.getChildrenAsJSX()).not.toEqual('Oops, tearing!');
});
});

it('getSnapshot changes and then source is mutated during interleaved event', async () => {
const {useEffect} = React;

const source = createComplexSource('1', '2');
const mutableSource = createMutableSource(source);

// Subscribe to part of the store.
const getSnapshotA = s => s.valueA;
const subscribeA = (s, callback) => s.subscribeA(callback);
const configA = [getSnapshotA, subscribeA];

const getSnapshotB = s => s.valueB;
const subscribeB = (s, callback) => s.subscribeB(callback);
const configB = [getSnapshotB, subscribeB];

function App({parentConfig, childConfig}) {
const [getSnapshot, subscribe] = parentConfig;
const parentValue = useMutableSource(
mutableSource,
getSnapshot,
subscribe,
);

Scheduler.unstable_yieldValue('Parent: ' + parentValue);

return (
<Child
parentConfig={parentConfig}
childConfig={childConfig}
parentValue={parentValue}
/>
);
}

function Child({parentConfig, childConfig, parentValue}) {
const [getSnapshot, subscribe] = childConfig;
const childValue = useMutableSource(
mutableSource,
getSnapshot,
subscribe,
);

Scheduler.unstable_yieldValue('Child: ' + childValue);

let result = `${parentValue}, ${childValue}`;

if (parentConfig === childConfig) {
// When both components read using the same config, the two values
// must be consistent.
if (parentValue !== childValue) {
result = 'Oops, tearing!';
}
}

useEffect(() => {
Scheduler.unstable_yieldValue('Commit: ' + result);
}, [result]);

return result;
}

const root = ReactNoop.createRoot();
await act(async () => {
root.render(<App parentConfig={configA} childConfig={configB} />);
});
expect(Scheduler).toHaveYielded([
'Parent: 1',
'Child: 2',
'Commit: 1, 2',
]);

await act(async () => {
// Switch the parent and the child to read using the same config
root.render(<App parentConfig={configB} childConfig={configB} />);
// Start rendering the parent, but yield before rendering the child
expect(Scheduler).toFlushAndYieldThrough(['Parent: 2']);

// Mutate the config. This is at lower priority so that 1) to make sure
// it doesn't happen to get batched with the in-progress render, and 2)
// so it doesn't interrupt the in-progress render.
Scheduler.unstable_runWithPriority(
Scheduler.unstable_IdlePriority,
() => {
source.valueB = '3';
},
);
});
expect(Scheduler).toHaveYielded([
'TODO: This currently tears. Fill in with correct values once bug',
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this was meant to be a comment? 😄


// Here's the current behavior:

// The partial render completes
'Child: 2',
'Commit: 2, 2',

// Then we start rendering the low priority mutation
'Parent: 3',
// But the child never received a mutation event, because it hadn't
// mounted yet. So the render tears.
'Child: 2',
'Commit: Oops, tearing!',

// Eventually the child corrects itself, because of the check that
// occurs when re-subscribing.
'Child: 3',
'Commit: 3, 3',
]);
});

if (__DEV__) {
describe('dev warnings', () => {
it('should warn if the subscribe function does not return an unsubscribe function', () => {
Expand Down