Skip to content

Commit 1b4a0da

Browse files
authored
Add assertions about <input> value dirty state (#26626)
Since this is an observable behavior and is hard to think about, seems good to have tests for this. The expected value included in each test is the behavior that existed prior to #26546.
1 parent b433c37 commit 1b4a0da

File tree

1 file changed

+65
-3
lines changed

1 file changed

+65
-3
lines changed

packages/react-dom/src/__tests__/ReactDOMInput-test.js

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,16 @@ describe('ReactDOMInput', () => {
2626
node.dispatchEvent(new Event(type, {bubbles: true, cancelable: true}));
2727
}
2828

29+
function isValueDirty(node) {
30+
// Return the "dirty value flag" as defined in the HTML spec. Cast to text
31+
// input to sidestep complicated value sanitization behaviors.
32+
const copy = node.cloneNode();
33+
copy.type = 'text';
34+
// If modifying the attribute now doesn't change the value, the value was already detached.
35+
copy.defaultValue += Math.random();
36+
return copy.value === node.value;
37+
}
38+
2939
beforeEach(() => {
3040
jest.resetModules();
3141

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

132143
setUntrackedValue.call(node, 'giraffe');
133144

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

138149
expect(node.value).toBe('lion');
150+
expect(isValueDirty(node)).toBe(true);
139151
});
140152

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

439451
expect(node.value).toBe('0');
440452
expect(node.defaultValue).toBe('0');
453+
if (disableInputAttributeSyncing) {
454+
expect(isValueDirty(node)).toBe(false);
455+
} else {
456+
expect(isValueDirty(node)).toBe(true);
457+
}
441458

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

444461
if (disableInputAttributeSyncing) {
445462
expect(node.value).toBe('1');
446463
expect(node.defaultValue).toBe('1');
464+
expect(isValueDirty(node)).toBe(false);
447465
} else {
448466
expect(node.value).toBe('0');
449467
expect(node.defaultValue).toBe('1');
468+
expect(isValueDirty(node)).toBe(true);
450469
}
451470
});
452471

@@ -478,12 +497,14 @@ describe('ReactDOMInput', () => {
478497
container,
479498
);
480499
expect(node.value).toBe('0');
500+
expect(isValueDirty(node)).toBe(true);
481501
expect(() =>
482502
ReactDOM.render(<input type="text" defaultValue="1" />, container),
483503
).toErrorDev(
484504
'A component is changing a controlled input to be uncontrolled.',
485505
);
486506
expect(node.value).toBe('0');
507+
expect(isValueDirty(node)).toBe(true);
487508
});
488509

489510
it('should render defaultValue for SSR', () => {
@@ -794,13 +815,16 @@ describe('ReactDOMInput', () => {
794815
<input type="text" value="" onChange={emptyFunction} />,
795816
container,
796817
);
818+
const node = container.firstChild;
819+
expect(isValueDirty(node)).toBe(false);
820+
797821
ReactDOM.render(
798822
<input type="text" value={0} onChange={emptyFunction} />,
799823
container,
800824
);
801825

802-
const node = container.firstChild;
803826
expect(node.value).toBe('0');
827+
expect(isValueDirty(node)).toBe(true);
804828

805829
if (disableInputAttributeSyncing) {
806830
expect(node.hasAttribute('value')).toBe(false);
@@ -814,15 +838,17 @@ describe('ReactDOMInput', () => {
814838
<input type="text" value={0} onChange={emptyFunction} />,
815839
container,
816840
);
841+
const node = container.firstChild;
842+
expect(isValueDirty(node)).toBe(true);
843+
817844
ReactDOM.render(
818845
<input type="text" value="" onChange={emptyFunction} />,
819846
container,
820847
);
821848

822-
const node = container.firstChild;
823-
824849
expect(node.value).toBe('');
825850
expect(node.defaultValue).toBe('');
851+
expect(isValueDirty(node)).toBe(true);
826852
});
827853

828854
it('should properly transition a text input from 0 to an empty 0.0', function () {
@@ -911,10 +937,16 @@ describe('ReactDOMInput', () => {
911937
container,
912938
);
913939
expect(inputRef.current.value).toBe('default1');
940+
if (disableInputAttributeSyncing) {
941+
expect(isValueDirty(inputRef.current)).toBe(false);
942+
} else {
943+
expect(isValueDirty(inputRef.current)).toBe(true);
944+
}
914945

915946
setUntrackedValue.call(inputRef.current, 'changed');
916947
dispatchEventOnNode(inputRef.current, 'input');
917948
expect(inputRef.current.value).toBe('changed');
949+
expect(isValueDirty(inputRef.current)).toBe(true);
918950

919951
ReactDOM.render(
920952
<form>
@@ -924,12 +956,14 @@ describe('ReactDOMInput', () => {
924956
container,
925957
);
926958
expect(inputRef.current.value).toBe('changed');
959+
expect(isValueDirty(inputRef.current)).toBe(true);
927960

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

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

13011335
it('should update defaultValue to empty string', () => {
13021336
ReactDOM.render(<input type="text" defaultValue={'foo'} />, container);
1337+
if (disableInputAttributeSyncing) {
1338+
expect(isValueDirty(container.firstChild)).toBe(false);
1339+
} else {
1340+
expect(isValueDirty(container.firstChild)).toBe(true);
1341+
}
13031342
ReactDOM.render(<input type="text" defaultValue={''} />, container);
13041343
expect(container.firstChild.defaultValue).toBe('');
1344+
if (disableInputAttributeSyncing) {
1345+
expect(isValueDirty(container.firstChild)).toBe(false);
1346+
} else {
1347+
expect(isValueDirty(container.firstChild)).toBe(true);
1348+
}
13051349
});
13061350

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

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

1890+
expect(isValueDirty(node)).toBe(true);
18451891
if (disableInputAttributeSyncing) {
18461892
expect(node.hasAttribute('value')).toBe(false);
18471893
} else {
@@ -1856,12 +1902,14 @@ describe('ReactDOMInput', () => {
18561902
container,
18571903
);
18581904
const node = ReactDOM.findDOMNode(stub);
1905+
expect(isValueDirty(node)).toBe(true);
18591906

18601907
node.focus();
18611908

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

1912+
expect(isValueDirty(node)).toBe(true);
18651913
if (disableInputAttributeSyncing) {
18661914
expect(node.hasAttribute('value')).toBe(false);
18671915
} else {
@@ -1876,12 +1924,14 @@ describe('ReactDOMInput', () => {
18761924
container,
18771925
);
18781926
const node = ReactDOM.findDOMNode(stub);
1927+
expect(isValueDirty(node)).toBe(true);
18791928

18801929
node.focus();
18811930
setUntrackedValue.call(node, '2');
18821931
dispatchEventOnNode(node, 'input');
18831932
node.blur();
18841933

1934+
expect(isValueDirty(node)).toBe(true);
18851935
if (disableInputAttributeSyncing) {
18861936
expect(node.value).toBe('2');
18871937
expect(node.hasAttribute('value')).toBe(false);
@@ -1896,12 +1946,18 @@ describe('ReactDOMInput', () => {
18961946
<input type="number" defaultValue="1" />,
18971947
container,
18981948
);
1949+
if (disableInputAttributeSyncing) {
1950+
expect(isValueDirty(node)).toBe(false);
1951+
} else {
1952+
expect(isValueDirty(node)).toBe(true);
1953+
}
18991954

19001955
node.focus();
19011956
setUntrackedValue.call(node, 4);
19021957
dispatchEventOnNode(node, 'input');
19031958
node.blur();
19041959

1960+
expect(isValueDirty(node)).toBe(true);
19051961
expect(node.getAttribute('value')).toBe('1');
19061962
});
19071963

@@ -1910,12 +1966,18 @@ describe('ReactDOMInput', () => {
19101966
<input type="text" defaultValue="1" />,
19111967
container,
19121968
);
1969+
if (disableInputAttributeSyncing) {
1970+
expect(isValueDirty(node)).toBe(false);
1971+
} else {
1972+
expect(isValueDirty(node)).toBe(true);
1973+
}
19131974

19141975
node.focus();
19151976
setUntrackedValue.call(node, 4);
19161977
dispatchEventOnNode(node, 'input');
19171978
node.blur();
19181979

1980+
expect(isValueDirty(node)).toBe(true);
19191981
expect(node.getAttribute('value')).toBe('1');
19201982
});
19211983
});

0 commit comments

Comments
 (0)