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

Add assertions about <input> value dirty state #26626

Merged
merged 1 commit into from
Apr 18, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 65 additions & 3 deletions packages/react-dom/src/__tests__/ReactDOMInput-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ describe('ReactDOMInput', () => {
node.dispatchEvent(new Event(type, {bubbles: true, cancelable: true}));
}

function isValueDirty(node) {
// Return the "dirty value flag" as defined in the HTML spec. Cast to text
// input to sidestep complicated value sanitization behaviors.
const copy = node.cloneNode();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Interesting that cloning clones the dirty flag.

copy.type = 'text';
// If modifying the attribute now doesn't change the value, the value was already detached.
copy.defaultValue += Math.random();
return copy.value === node.value;
}

beforeEach(() => {
jest.resetModules();

Expand Down Expand Up @@ -128,6 +138,7 @@ describe('ReactDOMInput', () => {
}).toErrorDev(
'Warning: You provided a `value` prop to a form field without an `onChange` handler.',
);
expect(isValueDirty(node)).toBe(true);

setUntrackedValue.call(node, 'giraffe');

Expand All @@ -136,6 +147,7 @@ describe('ReactDOMInput', () => {
dispatchEventOnNode(node, 'input');

expect(node.value).toBe('lion');
expect(isValueDirty(node)).toBe(true);
});

it('should control a value in reentrant events', () => {
Expand Down Expand Up @@ -438,15 +450,22 @@ describe('ReactDOMInput', () => {

expect(node.value).toBe('0');
expect(node.defaultValue).toBe('0');
if (disableInputAttributeSyncing) {
expect(isValueDirty(node)).toBe(false);
} else {
expect(isValueDirty(node)).toBe(true);
}

ReactDOM.render(<input type="text" defaultValue="1" />, container);

if (disableInputAttributeSyncing) {
expect(node.value).toBe('1');
expect(node.defaultValue).toBe('1');
expect(isValueDirty(node)).toBe(false);
} else {
expect(node.value).toBe('0');
expect(node.defaultValue).toBe('1');
expect(isValueDirty(node)).toBe(true);
}
});

Expand Down Expand Up @@ -478,12 +497,14 @@ describe('ReactDOMInput', () => {
container,
);
expect(node.value).toBe('0');
expect(isValueDirty(node)).toBe(true);
expect(() =>
ReactDOM.render(<input type="text" defaultValue="1" />, container),
).toErrorDev(
'A component is changing a controlled input to be uncontrolled.',
);
expect(node.value).toBe('0');
expect(isValueDirty(node)).toBe(true);
});

it('should render defaultValue for SSR', () => {
Expand Down Expand Up @@ -794,13 +815,16 @@ describe('ReactDOMInput', () => {
<input type="text" value="" onChange={emptyFunction} />,
container,
);
const node = container.firstChild;
expect(isValueDirty(node)).toBe(false);

ReactDOM.render(
<input type="text" value={0} onChange={emptyFunction} />,
container,
);

const node = container.firstChild;
expect(node.value).toBe('0');
expect(isValueDirty(node)).toBe(true);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Interestingly, this sole test fails on main with disableInputAttributeSyncing off:

  ● ReactDOMInput › should properly transition from an empty value to 0

    expect(received).toBe(expected) // Object.is equality

    Expected: true
    Received: false

      825 |
      826 |     expect(node.value).toBe('0');
    > 827 |     expect(isValueDirty(node)).toBe(true);
          |                                ^
      828 |
      829 |     if (disableInputAttributeSyncing) {
      830 |       expect(node.hasAttribute('value')).toBe(false);

(It passes with the flag on.) So it regressed recently. Possibly related to the bug @rickhanlonii was debugging.

This comment was marked as off-topic.


if (disableInputAttributeSyncing) {
expect(node.hasAttribute('value')).toBe(false);
Expand All @@ -814,15 +838,17 @@ describe('ReactDOMInput', () => {
<input type="text" value={0} onChange={emptyFunction} />,
container,
);
const node = container.firstChild;
expect(isValueDirty(node)).toBe(true);

ReactDOM.render(
<input type="text" value="" onChange={emptyFunction} />,
container,
);

const node = container.firstChild;

expect(node.value).toBe('');
expect(node.defaultValue).toBe('');
expect(isValueDirty(node)).toBe(true);
});

it('should properly transition a text input from 0 to an empty 0.0', function () {
Expand Down Expand Up @@ -911,10 +937,16 @@ describe('ReactDOMInput', () => {
container,
);
expect(inputRef.current.value).toBe('default1');
if (disableInputAttributeSyncing) {
expect(isValueDirty(inputRef.current)).toBe(false);
} else {
expect(isValueDirty(inputRef.current)).toBe(true);
}

setUntrackedValue.call(inputRef.current, 'changed');
dispatchEventOnNode(inputRef.current, 'input');
expect(inputRef.current.value).toBe('changed');
expect(isValueDirty(inputRef.current)).toBe(true);

ReactDOM.render(
<form>
Expand All @@ -924,12 +956,14 @@ describe('ReactDOMInput', () => {
container,
);
expect(inputRef.current.value).toBe('changed');
expect(isValueDirty(inputRef.current)).toBe(true);

container.firstChild.reset();
// Note: I don't know if we want to always support this.
// But it's current behavior so worth being intentional if we break it.
// https://github.com/facebook/react/issues/4618
expect(inputRef.current.value).toBe('default2');
expect(isValueDirty(inputRef.current)).toBe(false);
});

it('should not set a value for submit buttons unnecessarily', () => {
Expand Down Expand Up @@ -1300,8 +1334,18 @@ describe('ReactDOMInput', () => {

it('should update defaultValue to empty string', () => {
ReactDOM.render(<input type="text" defaultValue={'foo'} />, container);
if (disableInputAttributeSyncing) {
expect(isValueDirty(container.firstChild)).toBe(false);
} else {
expect(isValueDirty(container.firstChild)).toBe(true);
}
ReactDOM.render(<input type="text" defaultValue={''} />, container);
expect(container.firstChild.defaultValue).toBe('');
if (disableInputAttributeSyncing) {
expect(isValueDirty(container.firstChild)).toBe(false);
} else {
expect(isValueDirty(container.firstChild)).toBe(true);
}
});

it('should warn if value is null', () => {
Expand Down Expand Up @@ -1838,10 +1882,12 @@ describe('ReactDOMInput', () => {
const Input = getTestInput();
const stub = ReactDOM.render(<Input type="text" />, container);
const node = ReactDOM.findDOMNode(stub);
expect(isValueDirty(node)).toBe(false);

setUntrackedValue.call(node, '2');
dispatchEventOnNode(node, 'input');

expect(isValueDirty(node)).toBe(true);
if (disableInputAttributeSyncing) {
expect(node.hasAttribute('value')).toBe(false);
} else {
Expand All @@ -1856,12 +1902,14 @@ describe('ReactDOMInput', () => {
container,
);
const node = ReactDOM.findDOMNode(stub);
expect(isValueDirty(node)).toBe(true);

node.focus();

setUntrackedValue.call(node, '2');
dispatchEventOnNode(node, 'input');

expect(isValueDirty(node)).toBe(true);
if (disableInputAttributeSyncing) {
expect(node.hasAttribute('value')).toBe(false);
} else {
Expand All @@ -1876,12 +1924,14 @@ describe('ReactDOMInput', () => {
container,
);
const node = ReactDOM.findDOMNode(stub);
expect(isValueDirty(node)).toBe(true);

node.focus();
setUntrackedValue.call(node, '2');
dispatchEventOnNode(node, 'input');
node.blur();

expect(isValueDirty(node)).toBe(true);
if (disableInputAttributeSyncing) {
expect(node.value).toBe('2');
expect(node.hasAttribute('value')).toBe(false);
Expand All @@ -1896,12 +1946,18 @@ describe('ReactDOMInput', () => {
<input type="number" defaultValue="1" />,
container,
);
if (disableInputAttributeSyncing) {
expect(isValueDirty(node)).toBe(false);
} else {
expect(isValueDirty(node)).toBe(true);
}

node.focus();
setUntrackedValue.call(node, 4);
dispatchEventOnNode(node, 'input');
node.blur();

expect(isValueDirty(node)).toBe(true);
expect(node.getAttribute('value')).toBe('1');
});

Expand All @@ -1910,12 +1966,18 @@ describe('ReactDOMInput', () => {
<input type="text" defaultValue="1" />,
container,
);
if (disableInputAttributeSyncing) {
expect(isValueDirty(node)).toBe(false);
} else {
expect(isValueDirty(node)).toBe(true);
}

node.focus();
setUntrackedValue.call(node, 4);
dispatchEventOnNode(node, 'input');
node.blur();

expect(isValueDirty(node)).toBe(true);
expect(node.getAttribute('value')).toBe('1');
});
});
Expand Down