mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
6786563f3c
Stacked on top of #28498 for test fixes. ### Don't Rethrow When we started React it was 1:1 setState calls a series of renders and if they error, it errors where the setState was called. Simple. However, then batching came and the error actually got thrown somewhere else. With concurrent mode, it's not even possible to get setState itself to throw anymore. In fact, all APIs that can rethrow out of React are executed either at the root of the scheduler or inside a DOM event handler. If you throw inside a React.startTransition callback that's sync, then that will bubble out of the startTransition but if you throw inside an async callback or a useTransition we now need to handle it at the hook site. So in 19 we need to make all React.startTransition swallow the error (and report them to reportError). The only one remaining that can throw is flushSync but it doesn't really make sense for it to throw at the callsite neither because batching. Just because something rendered in this flush doesn't mean it was rendered due to what was just scheduled and doesn't mean that it should abort any of the remaining code afterwards. setState is fire and forget. It's send an instruction elsewhere, it's not part of the current imperative code. Error boundaries never rethrow. Since you should really always have error boundaries, most of the time, it wouldn't rethrow anyway. Rethrowing also actually currently drops errors on the floor since we can only rethrow the first error, so to avoid that we'd need to call reportError anyway. This happens in RN events. The other issue with rethrowing is that it logs an extra console.error. Since we're not sure that user code will actually log it anywhere we still log it too just like we do with errors inside error boundaries which leads all of these to log twice. The goal of this PR is to never rethrow out of React instead, errors outside of error boundaries get logged to reportError. Event system errors too. ### Breaking Changes The main thing this affects is testing where you want to inspect the errors thrown. To make it easier to port, if you're inside `act` we track the error into act in an aggregate error and then rethrow it at the root of `act`. Unlike before though, if you flush synchronously inside of act it'll still continue until the end of act before rethrowing. I expect most user code breakages would be to migrate from `flushSync` to `act` if you assert on throwing. However, in the React repo we also have `internalAct` and the `waitForThrow` helpers. Since these have to use public production implementations we track these using the global onerror or process uncaughtException. Unlike regular act, includes both event handler errors and onRecoverableError by default too. Not just render/commit errors. So I had to account for that in our tests. We restore logging an extra log for uncaught errors after the main log with the component stack in it. We use `console.warn`. This is not yet ignorable if you preventDefault to the main error event. To avoid confusion if you don't end up logging the error to console I just added `An error occurred`. ### Polyfill All browsers we support really supports `reportError` but not all test and server environments do, so I implemented a polyfill for browser and node in `shared/reportGlobalError`. I don't love that this is included in all builds and gets duplicated into isomorphic even though it's not actually needed in production. Maybe in the future we can require a polyfill for this. ### Follow Ups In a follow up, I'll make caught vs uncaught error handling be configurable too. --------- Co-authored-by: Ricky Hanlon <rickhanlonii@gmail.com>
1768 lines
57 KiB
JavaScript
1768 lines
57 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @emails react-core
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
// Fix JSDOM. setAttribute is supposed to throw on things that can't be implicitly toStringed.
|
|
const setAttribute = Element.prototype.setAttribute;
|
|
Element.prototype.setAttribute = function (name, value) {
|
|
// eslint-disable-next-line react-internal/safe-string-coercion
|
|
return setAttribute.call(this, name, '' + value);
|
|
};
|
|
|
|
describe('ReactDOMSelect', () => {
|
|
let React;
|
|
let ReactDOM;
|
|
let ReactDOMClient;
|
|
let ReactDOMServer;
|
|
let act;
|
|
|
|
const noop = function () {};
|
|
|
|
beforeEach(() => {
|
|
jest.resetModules();
|
|
React = require('react');
|
|
ReactDOM = require('react-dom');
|
|
ReactDOMClient = require('react-dom/client');
|
|
ReactDOMServer = require('react-dom/server');
|
|
act = require('internal-test-utils').act;
|
|
});
|
|
|
|
it('should allow setting `defaultValue`', async () => {
|
|
const stub = (
|
|
<select defaultValue="giraffe">
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
<option value="gorilla">A gorilla!</option>
|
|
</select>
|
|
);
|
|
const options = stub.props.children;
|
|
const container = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(container);
|
|
await act(() => {
|
|
root.render(stub);
|
|
});
|
|
|
|
const node = container.firstChild;
|
|
|
|
expect(node.value).toBe('giraffe');
|
|
|
|
await act(() => {
|
|
root.render(<select defaultValue="gorilla">{options}</select>);
|
|
});
|
|
|
|
expect(node.value).toEqual('giraffe');
|
|
});
|
|
|
|
it('should not throw with `defaultValue` and without children', () => {
|
|
const stub = <select defaultValue="dummy" />;
|
|
|
|
expect(async () => {
|
|
const container = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(container);
|
|
|
|
await act(() => {
|
|
root.render(stub);
|
|
});
|
|
}).not.toThrow();
|
|
});
|
|
|
|
it('should not control when using `defaultValue`', async () => {
|
|
const el = (
|
|
<select defaultValue="giraffe">
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
<option value="gorilla">A gorilla!</option>
|
|
</select>
|
|
);
|
|
const container = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(container);
|
|
|
|
await act(() => {
|
|
root.render(el);
|
|
});
|
|
|
|
const node = container.firstChild;
|
|
|
|
expect(node.value).toBe('giraffe');
|
|
|
|
node.value = 'monkey';
|
|
await act(() => {
|
|
root.render(el);
|
|
});
|
|
|
|
// Uncontrolled selects shouldn't change the value after first mounting
|
|
expect(node.value).toEqual('monkey');
|
|
});
|
|
|
|
it('should allow setting `defaultValue` with multiple', async () => {
|
|
const stub = (
|
|
<select multiple={true} defaultValue={['giraffe', 'gorilla']}>
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
<option value="gorilla">A gorilla!</option>
|
|
</select>
|
|
);
|
|
const options = stub.props.children;
|
|
const container = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(container);
|
|
|
|
await act(() => {
|
|
root.render(stub);
|
|
});
|
|
|
|
const node = container.firstChild;
|
|
|
|
expect(node.options[0].selected).toBe(false); // monkey
|
|
expect(node.options[1].selected).toBe(true); // giraffe
|
|
expect(node.options[2].selected).toBe(true); // gorilla
|
|
|
|
await act(() => {
|
|
root.render(
|
|
<select multiple={true} defaultValue={['monkey']}>
|
|
{options}
|
|
</select>,
|
|
);
|
|
});
|
|
|
|
expect(node.options[0].selected).toBe(false); // monkey
|
|
expect(node.options[1].selected).toBe(true); // giraffe
|
|
expect(node.options[2].selected).toBe(true); // gorilla
|
|
});
|
|
|
|
it('should allow setting `value`', async () => {
|
|
const stub = (
|
|
<select value="giraffe" onChange={noop}>
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
<option value="gorilla">A gorilla!</option>
|
|
</select>
|
|
);
|
|
const options = stub.props.children;
|
|
const container = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(container);
|
|
|
|
await act(() => {
|
|
root.render(stub);
|
|
});
|
|
|
|
const node = container.firstChild;
|
|
|
|
expect(node.value).toBe('giraffe');
|
|
|
|
await act(() => {
|
|
root.render(
|
|
<select value="gorilla" onChange={noop}>
|
|
{options}
|
|
</select>,
|
|
);
|
|
});
|
|
|
|
expect(node.value).toEqual('gorilla');
|
|
});
|
|
|
|
it('should default to the first non-disabled option', async () => {
|
|
const stub = (
|
|
<select defaultValue="">
|
|
<option disabled={true}>Disabled</option>
|
|
<option disabled={true}>Still Disabled</option>
|
|
<option>0</option>
|
|
<option disabled={true}>Also Disabled</option>
|
|
</select>
|
|
);
|
|
const container = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(container);
|
|
|
|
await act(() => {
|
|
root.render(stub);
|
|
});
|
|
|
|
const node = container.firstChild;
|
|
expect(node.options[0].selected).toBe(false);
|
|
expect(node.options[2].selected).toBe(true);
|
|
});
|
|
|
|
it('should allow setting `value` to __proto__', async () => {
|
|
const stub = (
|
|
<select value="__proto__" onChange={noop}>
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="__proto__">A giraffe!</option>
|
|
<option value="gorilla">A gorilla!</option>
|
|
</select>
|
|
);
|
|
const options = stub.props.children;
|
|
const container = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(container);
|
|
await act(() => {
|
|
root.render(stub);
|
|
});
|
|
|
|
const node = container.firstChild;
|
|
|
|
expect(node.value).toBe('__proto__');
|
|
|
|
await act(() => {
|
|
root.render(
|
|
<select value="gorilla" onChange={noop}>
|
|
{options}
|
|
</select>,
|
|
);
|
|
});
|
|
|
|
expect(node.value).toEqual('gorilla');
|
|
});
|
|
|
|
it('should not throw with `value` and without children', () => {
|
|
const stub = <select value="dummy" onChange={noop} />;
|
|
|
|
expect(async () => {
|
|
const container = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(container);
|
|
|
|
await act(() => {
|
|
root.render(stub);
|
|
});
|
|
}).not.toThrow();
|
|
});
|
|
|
|
it('should allow setting `value` with multiple', async () => {
|
|
const stub = (
|
|
<select multiple={true} value={['giraffe', 'gorilla']} onChange={noop}>
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
<option value="gorilla">A gorilla!</option>
|
|
</select>
|
|
);
|
|
const options = stub.props.children;
|
|
const container = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(container);
|
|
|
|
await act(() => {
|
|
root.render(stub);
|
|
});
|
|
|
|
const node = container.firstChild;
|
|
|
|
expect(node.options[0].selected).toBe(false); // monkey
|
|
expect(node.options[1].selected).toBe(true); // giraffe
|
|
expect(node.options[2].selected).toBe(true); // gorilla
|
|
|
|
await act(() => {
|
|
root.render(
|
|
<select multiple={true} value={['monkey']} onChange={noop}>
|
|
{options}
|
|
</select>,
|
|
);
|
|
});
|
|
|
|
expect(node.options[0].selected).toBe(true); // monkey
|
|
expect(node.options[1].selected).toBe(false); // giraffe
|
|
expect(node.options[2].selected).toBe(false); // gorilla
|
|
});
|
|
|
|
it('should allow setting `value` to __proto__ with multiple', async () => {
|
|
const stub = (
|
|
<select multiple={true} value={['__proto__', 'gorilla']} onChange={noop}>
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="__proto__">A __proto__!</option>
|
|
<option value="gorilla">A gorilla!</option>
|
|
</select>
|
|
);
|
|
const options = stub.props.children;
|
|
const container = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(container);
|
|
await act(() => {
|
|
root.render(stub);
|
|
});
|
|
|
|
const node = container.firstChild;
|
|
|
|
expect(node.options[0].selected).toBe(false); // monkey
|
|
expect(node.options[1].selected).toBe(true); // __proto__
|
|
expect(node.options[2].selected).toBe(true); // gorilla
|
|
|
|
await act(() => {
|
|
root.render(
|
|
<select multiple={true} value={['monkey']} onChange={noop}>
|
|
{options}
|
|
</select>,
|
|
);
|
|
});
|
|
|
|
expect(node.options[0].selected).toBe(true); // monkey
|
|
expect(node.options[1].selected).toBe(false); // __proto__
|
|
expect(node.options[2].selected).toBe(false); // gorilla
|
|
});
|
|
|
|
it('should not select other options automatically', async () => {
|
|
const stub = (
|
|
<select multiple={true} value={['12']} onChange={noop}>
|
|
<option value="1">one</option>
|
|
<option value="2">two</option>
|
|
<option value="12">twelve</option>
|
|
</select>
|
|
);
|
|
const container = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(container);
|
|
|
|
await act(() => {
|
|
root.render(stub);
|
|
});
|
|
|
|
const node = container.firstChild;
|
|
|
|
expect(node.options[0].selected).toBe(false); // one
|
|
expect(node.options[1].selected).toBe(false); // two
|
|
expect(node.options[2].selected).toBe(true); // twelve
|
|
});
|
|
|
|
it('should reset child options selected when they are changed and `value` is set', async () => {
|
|
const stub = <select multiple={true} value={['a', 'b']} onChange={noop} />;
|
|
const container = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(container);
|
|
await act(() => {
|
|
root.render(stub);
|
|
});
|
|
|
|
const node = container.firstChild;
|
|
|
|
await act(() => {
|
|
root.render(
|
|
<select multiple={true} value={['a', 'b']} onChange={noop}>
|
|
<option value="a">a</option>
|
|
<option value="b">b</option>
|
|
<option value="c">c</option>
|
|
</select>,
|
|
);
|
|
});
|
|
|
|
expect(node.options[0].selected).toBe(true); // a
|
|
expect(node.options[1].selected).toBe(true); // b
|
|
expect(node.options[2].selected).toBe(false); // c
|
|
});
|
|
|
|
it('should allow setting `value` with `objectToString`', async () => {
|
|
const objectToString = {
|
|
animal: 'giraffe',
|
|
toString: function () {
|
|
return this.animal;
|
|
},
|
|
};
|
|
|
|
const el = (
|
|
<select multiple={true} value={[objectToString]} onChange={noop}>
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
<option value="gorilla">A gorilla!</option>
|
|
</select>
|
|
);
|
|
const container = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(container);
|
|
await act(() => {
|
|
root.render(el);
|
|
});
|
|
|
|
const node = container.firstChild;
|
|
|
|
expect(node.options[0].selected).toBe(false); // monkey
|
|
expect(node.options[1].selected).toBe(true); // giraffe
|
|
expect(node.options[2].selected).toBe(false); // gorilla
|
|
|
|
// Changing the `value` prop should change the selected options.
|
|
objectToString.animal = 'monkey';
|
|
|
|
const el2 = (
|
|
<select multiple={true} value={[objectToString]}>
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
<option value="gorilla">A gorilla!</option>
|
|
</select>
|
|
);
|
|
|
|
await act(() => {
|
|
root.render(el2);
|
|
});
|
|
|
|
expect(node.options[0].selected).toBe(true); // monkey
|
|
expect(node.options[1].selected).toBe(false); // giraffe
|
|
expect(node.options[2].selected).toBe(false); // gorilla
|
|
});
|
|
|
|
it('should allow switching to multiple', async () => {
|
|
const stub = (
|
|
<select defaultValue="giraffe">
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
<option value="gorilla">A gorilla!</option>
|
|
</select>
|
|
);
|
|
const options = stub.props.children;
|
|
const container = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(container);
|
|
await act(() => {
|
|
root.render(stub);
|
|
});
|
|
|
|
const node = container.firstChild;
|
|
|
|
expect(node.options[0].selected).toBe(false); // monkey
|
|
expect(node.options[1].selected).toBe(true); // giraffe
|
|
expect(node.options[2].selected).toBe(false); // gorilla
|
|
|
|
await act(() => {
|
|
root.render(
|
|
<select multiple={true} defaultValue={['giraffe', 'gorilla']}>
|
|
{options}
|
|
</select>,
|
|
);
|
|
});
|
|
|
|
expect(node.options[0].selected).toBe(false); // monkey
|
|
expect(node.options[1].selected).toBe(true); // giraffe
|
|
expect(node.options[2].selected).toBe(true); // gorilla
|
|
});
|
|
|
|
it('should allow switching from multiple', async () => {
|
|
const stub = (
|
|
<select multiple={true} defaultValue={['giraffe', 'gorilla']}>
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
<option value="gorilla">A gorilla!</option>
|
|
</select>
|
|
);
|
|
const options = stub.props.children;
|
|
const container = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(container);
|
|
await act(() => {
|
|
root.render(stub);
|
|
});
|
|
|
|
const node = container.firstChild;
|
|
|
|
expect(node.options[0].selected).toBe(false); // monkey
|
|
expect(node.options[1].selected).toBe(true); // giraffe
|
|
expect(node.options[2].selected).toBe(true); // gorilla
|
|
|
|
await act(() => {
|
|
root.render(<select defaultValue="gorilla">{options}</select>);
|
|
});
|
|
|
|
expect(node.options[0].selected).toBe(false); // monkey
|
|
expect(node.options[1].selected).toBe(false); // giraffe
|
|
expect(node.options[2].selected).toBe(true); // gorilla
|
|
});
|
|
|
|
it('does not select an item when size is initially set to greater than 1', async () => {
|
|
const stub = (
|
|
<select size="2">
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
<option value="gorilla">A gorilla!</option>
|
|
</select>
|
|
);
|
|
const container = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(container);
|
|
|
|
await act(() => {
|
|
root.render(stub);
|
|
});
|
|
|
|
const select = container.firstChild;
|
|
|
|
expect(select.options[0].selected).toBe(false);
|
|
expect(select.options[1].selected).toBe(false);
|
|
expect(select.options[2].selected).toBe(false);
|
|
|
|
expect(select.value).toBe('');
|
|
expect(select.selectedIndex).toBe(-1);
|
|
});
|
|
|
|
it('should remember value when switching to uncontrolled', async () => {
|
|
const stub = (
|
|
<select value={'giraffe'} onChange={noop}>
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
<option value="gorilla">A gorilla!</option>
|
|
</select>
|
|
);
|
|
const options = stub.props.children;
|
|
const container = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(container);
|
|
await act(() => {
|
|
root.render(stub);
|
|
});
|
|
|
|
const node = container.firstChild;
|
|
|
|
expect(node.options[0].selected).toBe(false); // monkey
|
|
expect(node.options[1].selected).toBe(true); // giraffe
|
|
expect(node.options[2].selected).toBe(false); // gorilla
|
|
|
|
await act(() => {
|
|
root.render(<select>{options}</select>);
|
|
});
|
|
|
|
expect(node.options[0].selected).toBe(false); // monkey
|
|
expect(node.options[1].selected).toBe(true); // giraffe
|
|
expect(node.options[2].selected).toBe(false); // gorilla
|
|
});
|
|
|
|
it('should remember updated value when switching to uncontrolled', async () => {
|
|
const stub = (
|
|
<select value={'giraffe'} onChange={noop}>
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
<option value="gorilla">A gorilla!</option>
|
|
</select>
|
|
);
|
|
const options = stub.props.children;
|
|
const container = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(container);
|
|
await act(() => {
|
|
root.render(stub);
|
|
});
|
|
|
|
const node = container.firstChild;
|
|
await act(() => {
|
|
root.render(
|
|
<select value="gorilla" onChange={noop}>
|
|
{options}
|
|
</select>,
|
|
);
|
|
});
|
|
|
|
expect(node.options[0].selected).toBe(false); // monkey
|
|
expect(node.options[1].selected).toBe(false); // giraffe
|
|
expect(node.options[2].selected).toBe(true); // gorilla
|
|
|
|
await act(() => {
|
|
root.render(<select>{options}</select>);
|
|
});
|
|
|
|
expect(node.options[0].selected).toBe(false); // monkey
|
|
expect(node.options[1].selected).toBe(false); // giraffe
|
|
expect(node.options[2].selected).toBe(true); // gorilla
|
|
});
|
|
|
|
it('should support server-side rendering', () => {
|
|
const stub = (
|
|
<select value="giraffe" onChange={noop}>
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
<option value="gorilla">A gorilla!</option>
|
|
</select>
|
|
);
|
|
const container = document.createElement('div');
|
|
container.innerHTML = ReactDOMServer.renderToString(stub);
|
|
const options = container.firstChild.options;
|
|
expect(options[0].value).toBe('monkey');
|
|
expect(options[0].selected).toBe(false);
|
|
expect(options[1].value).toBe('giraffe');
|
|
expect(options[1].selected).toBe(true);
|
|
expect(options[2].value).toBe('gorilla');
|
|
expect(options[2].selected).toBe(false);
|
|
});
|
|
|
|
it('should support server-side rendering with defaultValue', () => {
|
|
const stub = (
|
|
<select defaultValue="giraffe">
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
<option value="gorilla">A gorilla!</option>
|
|
</select>
|
|
);
|
|
const container = document.createElement('div');
|
|
container.innerHTML = ReactDOMServer.renderToString(stub);
|
|
const options = container.firstChild.options;
|
|
expect(options[0].value).toBe('monkey');
|
|
expect(options[0].selected).toBe(false);
|
|
expect(options[1].value).toBe('giraffe');
|
|
expect(options[1].selected).toBe(true);
|
|
expect(options[2].value).toBe('gorilla');
|
|
expect(options[2].selected).toBe(false);
|
|
});
|
|
|
|
it('should support server-side rendering with dangerouslySetInnerHTML', () => {
|
|
const stub = (
|
|
<select defaultValue="giraffe">
|
|
<option
|
|
value="monkey"
|
|
dangerouslySetInnerHTML={{
|
|
__html: 'A monkey!',
|
|
}}>
|
|
{undefined}
|
|
</option>
|
|
<option
|
|
value="giraffe"
|
|
dangerouslySetInnerHTML={{
|
|
__html: 'A giraffe!',
|
|
}}>
|
|
{null}
|
|
</option>
|
|
<option
|
|
value="gorilla"
|
|
dangerouslySetInnerHTML={{
|
|
__html: 'A gorilla!',
|
|
}}
|
|
/>
|
|
</select>
|
|
);
|
|
const container = document.createElement('div');
|
|
container.innerHTML = ReactDOMServer.renderToString(stub);
|
|
const options = container.firstChild.options;
|
|
expect(options[0].value).toBe('monkey');
|
|
expect(options[0].selected).toBe(false);
|
|
expect(options[1].value).toBe('giraffe');
|
|
expect(options[1].selected).toBe(true);
|
|
expect(options[2].value).toBe('gorilla');
|
|
expect(options[2].selected).toBe(false);
|
|
});
|
|
|
|
it('should support server-side rendering with multiple', () => {
|
|
const stub = (
|
|
<select multiple={true} value={['giraffe', 'gorilla']} onChange={noop}>
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
<option value="gorilla">A gorilla!</option>
|
|
</select>
|
|
);
|
|
const container = document.createElement('div');
|
|
container.innerHTML = ReactDOMServer.renderToString(stub);
|
|
const options = container.firstChild.options;
|
|
expect(options[0].value).toBe('monkey');
|
|
expect(options[0].selected).toBe(false);
|
|
expect(options[1].value).toBe('giraffe');
|
|
expect(options[1].selected).toBe(true);
|
|
expect(options[2].value).toBe('gorilla');
|
|
expect(options[2].selected).toBe(true);
|
|
});
|
|
|
|
it('should not control defaultValue if re-adding options', async () => {
|
|
const container = document.createElement('div');
|
|
|
|
const root = ReactDOMClient.createRoot(container);
|
|
|
|
await act(() => {
|
|
root.render(
|
|
<select multiple={true} defaultValue={['giraffe']}>
|
|
<option key="monkey" value="monkey">
|
|
A monkey!
|
|
</option>
|
|
<option key="giraffe" value="giraffe">
|
|
A giraffe!
|
|
</option>
|
|
<option key="gorilla" value="gorilla">
|
|
A gorilla!
|
|
</option>
|
|
</select>,
|
|
);
|
|
});
|
|
|
|
const node = container.firstChild;
|
|
|
|
expect(node.options[0].selected).toBe(false); // monkey
|
|
expect(node.options[1].selected).toBe(true); // giraffe
|
|
expect(node.options[2].selected).toBe(false); // gorilla
|
|
|
|
await act(() => {
|
|
root.render(
|
|
<select multiple={true} defaultValue={['giraffe']}>
|
|
<option key="monkey" value="monkey">
|
|
A monkey!
|
|
</option>
|
|
<option key="gorilla" value="gorilla">
|
|
A gorilla!
|
|
</option>
|
|
</select>,
|
|
);
|
|
});
|
|
|
|
expect(node.options[0].selected).toBe(false); // monkey
|
|
expect(node.options[1].selected).toBe(false); // gorilla
|
|
|
|
await act(() => {
|
|
root.render(
|
|
<select multiple={true} defaultValue={['giraffe']}>
|
|
<option key="monkey" value="monkey">
|
|
A monkey!
|
|
</option>
|
|
<option key="giraffe" value="giraffe">
|
|
A giraffe!
|
|
</option>
|
|
<option key="gorilla" value="gorilla">
|
|
A gorilla!
|
|
</option>
|
|
</select>,
|
|
);
|
|
});
|
|
|
|
expect(node.options[0].selected).toBe(false); // monkey
|
|
expect(node.options[1].selected).toBe(false); // giraffe
|
|
expect(node.options[2].selected).toBe(false); // gorilla
|
|
});
|
|
|
|
it('should support options with dynamic children', async () => {
|
|
const container = document.createElement('div');
|
|
|
|
let node;
|
|
|
|
function App({value}) {
|
|
return (
|
|
<select value={value} ref={n => (node = n)} onChange={noop}>
|
|
<option key="monkey" value="monkey">
|
|
A monkey {value === 'monkey' ? 'is chosen' : null}!
|
|
</option>
|
|
<option key="giraffe" value="giraffe">
|
|
A giraffe {value === 'giraffe' && 'is chosen'}!
|
|
</option>
|
|
<option key="gorilla" value="gorilla">
|
|
A gorilla {value === 'gorilla' && 'is chosen'}!
|
|
</option>
|
|
</select>
|
|
);
|
|
}
|
|
|
|
const root = ReactDOMClient.createRoot(container);
|
|
await act(() => {
|
|
root.render(<App value="monkey" />);
|
|
});
|
|
|
|
expect(node.options[0].selected).toBe(true); // monkey
|
|
expect(node.options[1].selected).toBe(false); // giraffe
|
|
expect(node.options[2].selected).toBe(false); // gorilla
|
|
|
|
await act(() => {
|
|
root.render(<App value="giraffe" />);
|
|
});
|
|
|
|
expect(node.options[0].selected).toBe(false); // monkey
|
|
expect(node.options[1].selected).toBe(true); // giraffe
|
|
expect(node.options[2].selected).toBe(false); // gorilla
|
|
});
|
|
|
|
it('should warn if value is null', async () => {
|
|
const container = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(container);
|
|
await expect(async () => {
|
|
await act(() => {
|
|
root.render(
|
|
<select value={null}>
|
|
<option value="test" />
|
|
</select>,
|
|
);
|
|
});
|
|
}).toErrorDev(
|
|
'`value` prop on `select` should not be null. ' +
|
|
'Consider using an empty string to clear the component or `undefined` ' +
|
|
'for uncontrolled components.',
|
|
);
|
|
|
|
await act(() => {
|
|
root.render(
|
|
<select value={null}>
|
|
<option value="test" />
|
|
</select>,
|
|
);
|
|
});
|
|
});
|
|
|
|
it('should warn if selected is set on <option>', async () => {
|
|
function App() {
|
|
return (
|
|
<select>
|
|
<option selected={true} />
|
|
<option selected={true} />
|
|
</select>
|
|
);
|
|
}
|
|
|
|
const container = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(container);
|
|
await expect(async () => {
|
|
await act(() => {
|
|
root.render(<App />);
|
|
});
|
|
}).toErrorDev(
|
|
'Use the `defaultValue` or `value` props on <select> instead of ' +
|
|
'setting `selected` on <option>.',
|
|
);
|
|
|
|
await act(() => {
|
|
root.render(<App />);
|
|
});
|
|
});
|
|
|
|
it('should warn if value is null and multiple is true', async () => {
|
|
const container = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(container);
|
|
await expect(async () => {
|
|
await act(() => {
|
|
root.render(
|
|
<select value={null} multiple={true}>
|
|
<option value="test" />
|
|
</select>,
|
|
);
|
|
});
|
|
}).toErrorDev(
|
|
'`value` prop on `select` should not be null. ' +
|
|
'Consider using an empty array when `multiple` is ' +
|
|
'set to `true` to clear the component or `undefined` ' +
|
|
'for uncontrolled components.',
|
|
);
|
|
|
|
await act(() => {
|
|
root.render(
|
|
<select value={null} multiple={true}>
|
|
<option value="test" />
|
|
</select>,
|
|
);
|
|
});
|
|
});
|
|
|
|
it('should refresh state on change', async () => {
|
|
const stub = (
|
|
<select value="giraffe" onChange={noop}>
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
<option value="gorilla">A gorilla!</option>
|
|
</select>
|
|
);
|
|
const container = document.createElement('div');
|
|
document.body.appendChild(container);
|
|
|
|
try {
|
|
const root = ReactDOMClient.createRoot(container);
|
|
|
|
await act(() => {
|
|
root.render(stub);
|
|
});
|
|
|
|
const node = container.firstChild;
|
|
|
|
await act(() => {
|
|
node.dispatchEvent(
|
|
new Event('change', {bubbles: true, cancelable: false}),
|
|
);
|
|
});
|
|
|
|
expect(node.value).toBe('giraffe');
|
|
} finally {
|
|
document.body.removeChild(container);
|
|
}
|
|
});
|
|
|
|
it('should warn if value and defaultValue props are specified', async () => {
|
|
const container = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(container);
|
|
await expect(async () => {
|
|
await act(() => {
|
|
root.render(
|
|
<select value="giraffe" defaultValue="giraffe" readOnly={true}>
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
<option value="gorilla">A gorilla!</option>
|
|
</select>,
|
|
);
|
|
});
|
|
}).toErrorDev(
|
|
'Select elements must be either controlled or uncontrolled ' +
|
|
'(specify either the value prop, or the defaultValue prop, but not ' +
|
|
'both). Decide between using a controlled or uncontrolled select ' +
|
|
'element and remove one of these props. More info: ' +
|
|
'https://react.dev/link/controlled-components',
|
|
);
|
|
|
|
await act(() => {
|
|
root.render(
|
|
<select value="giraffe" defaultValue="giraffe" readOnly={true}>
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
<option value="gorilla">A gorilla!</option>
|
|
</select>,
|
|
);
|
|
});
|
|
});
|
|
|
|
it('should not warn about missing onChange in uncontrolled textareas', async () => {
|
|
const container = document.createElement('div');
|
|
let root = ReactDOMClient.createRoot(container);
|
|
|
|
await act(() => {
|
|
root.render(<select />);
|
|
});
|
|
|
|
await act(() => {
|
|
root.unmount();
|
|
});
|
|
root = ReactDOMClient.createRoot(container);
|
|
|
|
await act(() => {
|
|
root.render(<select value={undefined} />);
|
|
});
|
|
});
|
|
|
|
it('should be able to safely remove select onChange', async () => {
|
|
const container = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(container);
|
|
async function changeView() {
|
|
await act(() => {
|
|
root.unmount();
|
|
});
|
|
}
|
|
|
|
const stub = (
|
|
<select value="giraffe" onChange={changeView}>
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
<option value="gorilla">A gorilla!</option>
|
|
</select>
|
|
);
|
|
|
|
await act(() => {
|
|
root.render(stub);
|
|
});
|
|
|
|
const node = container.firstChild;
|
|
|
|
await expect(
|
|
act(() => {
|
|
node.dispatchEvent(
|
|
new Event('change', {bubbles: true, cancelable: false}),
|
|
);
|
|
}),
|
|
).resolves.not.toThrow();
|
|
|
|
expect(container.firstChild).toBe(null);
|
|
});
|
|
|
|
it('should select grandchild options nested inside an optgroup', async () => {
|
|
const stub = (
|
|
<select value="b" onChange={noop}>
|
|
<optgroup label="group">
|
|
<option value="a">a</option>
|
|
<option value="b">b</option>
|
|
<option value="c">c</option>
|
|
</optgroup>
|
|
</select>
|
|
);
|
|
const container = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(container);
|
|
|
|
await act(() => {
|
|
root.render(stub);
|
|
});
|
|
|
|
const node = container.firstChild;
|
|
|
|
expect(node.options[0].selected).toBe(false); // a
|
|
expect(node.options[1].selected).toBe(true); // b
|
|
expect(node.options[2].selected).toBe(false); // c
|
|
});
|
|
|
|
// @gate !disableLegacyMode
|
|
it('should allow controlling `value` in a nested legacy render', async () => {
|
|
let selectNode;
|
|
|
|
class Parent extends React.Component {
|
|
state = {
|
|
value: 'giraffe',
|
|
};
|
|
|
|
componentDidMount() {
|
|
this._renderNested();
|
|
}
|
|
|
|
componentDidUpdate() {
|
|
this._renderNested();
|
|
}
|
|
|
|
_handleChange(event) {
|
|
this.setState({value: event.target.value});
|
|
}
|
|
|
|
_renderNested() {
|
|
ReactDOM.render(
|
|
<select
|
|
onChange={this._handleChange.bind(this)}
|
|
ref={n => (selectNode = n)}
|
|
value={this.state.value}>
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
<option value="gorilla">A gorilla!</option>
|
|
</select>,
|
|
this._nestingContainer,
|
|
);
|
|
}
|
|
|
|
render() {
|
|
return <div ref={n => (this._nestingContainer = n)} />;
|
|
}
|
|
}
|
|
|
|
const container = document.createElement('div');
|
|
|
|
document.body.appendChild(container);
|
|
|
|
ReactDOM.render(<Parent />, container);
|
|
|
|
expect(selectNode.value).toBe('giraffe');
|
|
|
|
selectNode.value = 'gorilla';
|
|
|
|
let nativeEvent = document.createEvent('Event');
|
|
nativeEvent.initEvent('input', true, true);
|
|
selectNode.dispatchEvent(nativeEvent);
|
|
|
|
expect(selectNode.value).toEqual('gorilla');
|
|
|
|
nativeEvent = document.createEvent('Event');
|
|
nativeEvent.initEvent('change', true, true);
|
|
selectNode.dispatchEvent(nativeEvent);
|
|
|
|
expect(selectNode.value).toEqual('gorilla');
|
|
|
|
document.body.removeChild(container);
|
|
});
|
|
|
|
it('should not select first option by default when multiple is set and no defaultValue is set', async () => {
|
|
const stub = (
|
|
<select multiple={true} onChange={noop}>
|
|
<option value="a">a</option>
|
|
<option value="b">b</option>
|
|
<option value="c">c</option>
|
|
</select>
|
|
);
|
|
const container = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(container);
|
|
|
|
await act(() => {
|
|
root.render(stub);
|
|
});
|
|
|
|
const node = container.firstChild;
|
|
|
|
expect(node.options[0].selected).toBe(false); // a
|
|
expect(node.options[1].selected).toBe(false); // b
|
|
expect(node.options[2].selected).toBe(false); // c
|
|
});
|
|
|
|
describe('When given a Symbol value', () => {
|
|
it('treats initial Symbol value as missing', async () => {
|
|
const container = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(container);
|
|
await expect(async () => {
|
|
await act(() => {
|
|
root.render(
|
|
<select onChange={noop} value={Symbol('foobar')}>
|
|
<option value={Symbol('foobar')}>A Symbol!</option>
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
</select>,
|
|
);
|
|
});
|
|
}).toErrorDev('Invalid value for prop `value`');
|
|
|
|
const node = container.firstChild;
|
|
expect(node.value).toBe('A Symbol!');
|
|
});
|
|
|
|
it('treats updated Symbol value as missing', async () => {
|
|
const container = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(container);
|
|
await expect(async () => {
|
|
await act(() => {
|
|
root.render(
|
|
<select onChange={noop} value="monkey">
|
|
<option value={Symbol('foobar')}>A Symbol!</option>
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
</select>,
|
|
);
|
|
});
|
|
}).toErrorDev('Invalid value for prop `value`');
|
|
|
|
let node = container.firstChild;
|
|
expect(node.value).toBe('monkey');
|
|
|
|
await act(() => {
|
|
root.render(
|
|
<select onChange={noop} value={Symbol('foobar')}>
|
|
<option value={Symbol('foobar')}>A Symbol!</option>
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
</select>,
|
|
);
|
|
});
|
|
|
|
node = container.firstChild;
|
|
|
|
expect(node.value).toBe('A Symbol!');
|
|
});
|
|
|
|
it('treats initial Symbol defaultValue as an empty string', async () => {
|
|
const container = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(container);
|
|
await expect(async () => {
|
|
await act(() => {
|
|
root.render(
|
|
<select defaultValue={Symbol('foobar')}>
|
|
<option value={Symbol('foobar')}>A Symbol!</option>
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
</select>,
|
|
);
|
|
});
|
|
}).toErrorDev('Invalid value for prop `value`');
|
|
|
|
const node = container.firstChild;
|
|
expect(node.value).toBe('A Symbol!');
|
|
});
|
|
|
|
it('treats updated Symbol defaultValue as an empty string', async () => {
|
|
let container = document.createElement('div');
|
|
let root = ReactDOMClient.createRoot(container);
|
|
await expect(async () => {
|
|
await act(() => {
|
|
root.render(
|
|
<select defaultValue="monkey">
|
|
<option value={Symbol('foobar')}>A Symbol!</option>
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
</select>,
|
|
);
|
|
});
|
|
}).toErrorDev('Invalid value for prop `value`');
|
|
|
|
let node = container.firstChild;
|
|
expect(node.value).toBe('monkey');
|
|
|
|
container = document.createElement('div');
|
|
root = ReactDOMClient.createRoot(container);
|
|
await act(() => {
|
|
root.render(
|
|
<select defaultValue={Symbol('foobar')}>
|
|
<option value={Symbol('foobar')}>A Symbol!</option>
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
</select>,
|
|
);
|
|
});
|
|
|
|
node = container.firstChild;
|
|
expect(node.value).toBe('A Symbol!');
|
|
});
|
|
});
|
|
|
|
describe('When given a function value', () => {
|
|
it('treats initial function value as missing', async () => {
|
|
const container = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(container);
|
|
await expect(async () => {
|
|
await act(() => {
|
|
root.render(
|
|
<select onChange={noop} value={() => {}}>
|
|
<option value={() => {}}>A function!</option>
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
</select>,
|
|
);
|
|
});
|
|
}).toErrorDev('Invalid value for prop `value`');
|
|
|
|
const node = container.firstChild;
|
|
expect(node.value).toBe('A function!');
|
|
});
|
|
|
|
it('treats initial function defaultValue as an empty string', async () => {
|
|
const container = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(container);
|
|
await expect(async () => {
|
|
await act(() => {
|
|
root.render(
|
|
<select defaultValue={() => {}}>
|
|
<option value={() => {}}>A function!</option>
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
</select>,
|
|
);
|
|
});
|
|
}).toErrorDev('Invalid value for prop `value`');
|
|
|
|
const node = container.firstChild;
|
|
expect(node.value).toBe('A function!');
|
|
});
|
|
|
|
it('treats updated function value as an empty string', async () => {
|
|
const container = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(container);
|
|
await expect(async () => {
|
|
await act(() => {
|
|
root.render(
|
|
<select onChange={noop} value="monkey">
|
|
<option value={() => {}}>A function!</option>
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
</select>,
|
|
);
|
|
});
|
|
}).toErrorDev('Invalid value for prop `value`');
|
|
|
|
let node = container.firstChild;
|
|
expect(node.value).toBe('monkey');
|
|
|
|
await act(() => {
|
|
root.render(
|
|
<select onChange={noop} value={() => {}}>
|
|
<option value={() => {}}>A function!</option>
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
</select>,
|
|
);
|
|
});
|
|
|
|
node = container.firstChild;
|
|
expect(node.value).toBe('A function!');
|
|
});
|
|
|
|
it('treats updated function defaultValue as an empty string', async () => {
|
|
let container = document.createElement('div');
|
|
let root = ReactDOMClient.createRoot(container);
|
|
await expect(async () => {
|
|
await act(() => {
|
|
root.render(
|
|
<select defaultValue="monkey">
|
|
<option value={() => {}}>A function!</option>
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
</select>,
|
|
);
|
|
});
|
|
}).toErrorDev('Invalid value for prop `value`');
|
|
|
|
let node = container.firstChild;
|
|
expect(node.value).toBe('monkey');
|
|
|
|
container = document.createElement('div');
|
|
root = ReactDOMClient.createRoot(container);
|
|
await act(() => {
|
|
root.render(
|
|
<select defaultValue={() => {}}>
|
|
<option value={() => {}}>A function!</option>
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
</select>,
|
|
);
|
|
});
|
|
|
|
node = container.firstChild;
|
|
|
|
expect(node.value).toBe('A function!');
|
|
});
|
|
});
|
|
|
|
describe('When given a Temporal.PlainDate-like value', () => {
|
|
class TemporalLike {
|
|
valueOf() {
|
|
// Throwing here is the behavior of ECMAScript "Temporal" date/time API.
|
|
// See https://tc39.es/proposal-temporal/docs/plaindate.html#valueOf
|
|
throw new TypeError('prod message');
|
|
}
|
|
toString() {
|
|
return '2020-01-01';
|
|
}
|
|
}
|
|
|
|
it('throws when given a Temporal.PlainDate-like value (select)', async () => {
|
|
const container = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(container);
|
|
await expect(async () => {
|
|
await expect(
|
|
act(() => {
|
|
root.render(
|
|
<select onChange={noop} value={new TemporalLike()}>
|
|
<option value="2020-01-01">like a Temporal.PlainDate</option>
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
</select>,
|
|
);
|
|
}),
|
|
).rejects.toThrowError(new TypeError('prod message'));
|
|
}).toErrorDev([
|
|
'Form field values (value, checked, defaultValue, or defaultChecked props)' +
|
|
' must be strings, not TemporalLike. ' +
|
|
'This value must be coerced to a string before using it here.',
|
|
'Form field values (value, checked, defaultValue, or defaultChecked props)' +
|
|
' must be strings, not TemporalLike. ' +
|
|
'This value must be coerced to a string before using it here.',
|
|
]);
|
|
});
|
|
|
|
it('throws when given a Temporal.PlainDate-like value (option)', async () => {
|
|
const container = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(container);
|
|
await expect(async () => {
|
|
await expect(
|
|
act(() => {
|
|
root.render(
|
|
<select onChange={noop} value="2020-01-01">
|
|
<option value={new TemporalLike()}>
|
|
like a Temporal.PlainDate
|
|
</option>
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
</select>,
|
|
);
|
|
}),
|
|
).rejects.toThrowError(new TypeError('prod message'));
|
|
}).toErrorDev([
|
|
'The provided `value` attribute is an unsupported type TemporalLike.' +
|
|
' This value must be coerced to a string before using it here.',
|
|
'The provided `value` attribute is an unsupported type TemporalLike.' +
|
|
' This value must be coerced to a string before using it here.',
|
|
]);
|
|
});
|
|
|
|
it('throws when given a Temporal.PlainDate-like value (both)', async () => {
|
|
const container = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(container);
|
|
|
|
await expect(async () => {
|
|
await expect(
|
|
act(() => {
|
|
root.render(
|
|
<select onChange={noop} value={new TemporalLike()}>
|
|
<option value={new TemporalLike()}>
|
|
like a Temporal.PlainDate
|
|
</option>
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
</select>,
|
|
);
|
|
}),
|
|
).rejects.toThrowError(new TypeError('prod message'));
|
|
}).toErrorDev([
|
|
'The provided `value` attribute is an unsupported type TemporalLike.' +
|
|
' This value must be coerced to a string before using it here.',
|
|
'The provided `value` attribute is an unsupported type TemporalLike.' +
|
|
' This value must be coerced to a string before using it here.',
|
|
]);
|
|
});
|
|
|
|
it('throws with updated Temporal.PlainDate-like value (select)', async () => {
|
|
const container = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(container);
|
|
|
|
await act(() => {
|
|
root.render(
|
|
<select onChange={noop} value="monkey">
|
|
<option value="2020-01-01">like a Temporal.PlainDate</option>
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
</select>,
|
|
);
|
|
});
|
|
|
|
await expect(async () => {
|
|
await expect(
|
|
act(() => {
|
|
root.render(
|
|
<select onChange={noop} value={new TemporalLike()}>
|
|
<option value="2020-01-01">like a Temporal.PlainDate</option>
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
</select>,
|
|
);
|
|
}),
|
|
).rejects.toThrowError(new TypeError('prod message'));
|
|
}).toErrorDev(
|
|
'Form field values (value, checked, defaultValue, or defaultChecked props)' +
|
|
' must be strings, not TemporalLike. ' +
|
|
'This value must be coerced to a string before using it here.',
|
|
);
|
|
});
|
|
|
|
it('throws with updated Temporal.PlainDate-like value (option)', async () => {
|
|
const container = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(container);
|
|
|
|
await act(() => {
|
|
root.render(
|
|
<select onChange={noop} value="2020-01-01">
|
|
<option value="donkey">like a Temporal.PlainDate</option>
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
</select>,
|
|
);
|
|
});
|
|
|
|
await expect(async () => {
|
|
await expect(
|
|
act(() => {
|
|
root.render(
|
|
<select onChange={noop} value="2020-01-01">
|
|
<option value={new TemporalLike()}>
|
|
like a Temporal.PlainDate
|
|
</option>
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
</select>,
|
|
);
|
|
}),
|
|
).rejects.toThrowError(new TypeError('prod message'));
|
|
}).toErrorDev(
|
|
'The provided `value` attribute is an unsupported type TemporalLike.' +
|
|
' This value must be coerced to a string before using it here.',
|
|
);
|
|
});
|
|
|
|
it('throws with updated Temporal.PlainDate-like value (both)', async () => {
|
|
const container = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(container);
|
|
|
|
await act(() => {
|
|
root.render(
|
|
<select onChange={noop} value="donkey">
|
|
<option value="donkey">like a Temporal.PlainDate</option>
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
</select>,
|
|
);
|
|
});
|
|
|
|
await expect(async () => {
|
|
await expect(
|
|
act(() => {
|
|
root.render(
|
|
<select onChange={noop} value={new TemporalLike()}>
|
|
<option value={new TemporalLike()}>
|
|
like a Temporal.PlainDate
|
|
</option>
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
</select>,
|
|
);
|
|
}),
|
|
).rejects.toThrowError(
|
|
// eslint-disable-next-line no-undef
|
|
new AggregateError([
|
|
new TypeError('prod message'),
|
|
new TypeError('prod message'),
|
|
]),
|
|
);
|
|
}).toErrorDev([
|
|
'The provided `value` attribute is an unsupported type TemporalLike.' +
|
|
' This value must be coerced to a string before using it here.',
|
|
'Form field values (value, checked, defaultValue, or defaultChecked props)' +
|
|
' must be strings, not TemporalLike. ' +
|
|
'This value must be coerced to a string before using it here.',
|
|
]);
|
|
});
|
|
|
|
it('throws when given a Temporal.PlainDate-like defaultValue (select)', async () => {
|
|
const container = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(container);
|
|
await expect(async () => {
|
|
await expect(
|
|
act(() => {
|
|
root.render(
|
|
<select onChange={noop} defaultValue={new TemporalLike()}>
|
|
<option value="2020-01-01">like a Temporal.PlainDate</option>
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
</select>,
|
|
);
|
|
}),
|
|
).rejects.toThrowError(new TypeError('prod message'));
|
|
}).toErrorDev([
|
|
'Form field values (value, checked, defaultValue, or defaultChecked props)' +
|
|
' must be strings, not TemporalLike. ' +
|
|
'This value must be coerced to a string before using it here.',
|
|
'Form field values (value, checked, defaultValue, or defaultChecked props)' +
|
|
' must be strings, not TemporalLike. ' +
|
|
'This value must be coerced to a string before using it here.',
|
|
]);
|
|
});
|
|
|
|
it('throws when given a Temporal.PlainDate-like defaultValue (option)', async () => {
|
|
const container = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(container);
|
|
|
|
await expect(async () => {
|
|
await expect(
|
|
act(() => {
|
|
root.render(
|
|
<select onChange={noop} defaultValue="2020-01-01">
|
|
<option value={new TemporalLike()}>
|
|
like a Temporal.PlainDate
|
|
</option>
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
</select>,
|
|
);
|
|
}),
|
|
).rejects.toThrowError(new TypeError('prod message'));
|
|
}).toErrorDev([
|
|
'The provided `value` attribute is an unsupported type TemporalLike.' +
|
|
' This value must be coerced to a string before using it here.',
|
|
'The provided `value` attribute is an unsupported type TemporalLike.' +
|
|
' This value must be coerced to a string before using it here.',
|
|
]);
|
|
});
|
|
|
|
it('throws when given a Temporal.PlainDate-like value (both)', async () => {
|
|
const container = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(container);
|
|
await expect(async () => {
|
|
await expect(
|
|
act(() => {
|
|
root.render(
|
|
<select onChange={noop} defaultValue={new TemporalLike()}>
|
|
<option value={new TemporalLike()}>
|
|
like a Temporal.PlainDate
|
|
</option>
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
</select>,
|
|
);
|
|
}),
|
|
).rejects.toThrowError(new TypeError('prod message'));
|
|
}).toErrorDev([
|
|
'The provided `value` attribute is an unsupported type TemporalLike.' +
|
|
' This value must be coerced to a string before using it here.',
|
|
'The provided `value` attribute is an unsupported type TemporalLike.' +
|
|
' This value must be coerced to a string before using it here.',
|
|
]);
|
|
});
|
|
|
|
it('throws with updated Temporal.PlainDate-like defaultValue (select)', async () => {
|
|
let container = document.createElement('div');
|
|
let root = ReactDOMClient.createRoot(container);
|
|
await act(() => {
|
|
root.render(
|
|
<select onChange={noop} defaultValue="monkey">
|
|
<option value="2020-01-01">like a Temporal.PlainDate</option>
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
</select>,
|
|
);
|
|
});
|
|
|
|
container = document.createElement('div');
|
|
root = ReactDOMClient.createRoot(container);
|
|
await expect(async () => {
|
|
await expect(
|
|
act(() => {
|
|
root.render(
|
|
<select onChange={noop} defaultValue={new TemporalLike()}>
|
|
<option value="2020-01-01">like a Temporal.PlainDate</option>
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
</select>,
|
|
);
|
|
}),
|
|
).rejects.toThrowError(new TypeError('prod message'));
|
|
}).toErrorDev([
|
|
'Form field values (value, checked, defaultValue, or defaultChecked props)' +
|
|
' must be strings, not TemporalLike. ' +
|
|
'This value must be coerced to a string before using it here.',
|
|
'Form field values (value, checked, defaultValue, or defaultChecked props)' +
|
|
' must be strings, not TemporalLike. ' +
|
|
'This value must be coerced to a string before using it here.',
|
|
]);
|
|
});
|
|
|
|
it('throws with updated Temporal.PlainDate-like defaultValue (both)', async () => {
|
|
let container = document.createElement('div');
|
|
let root = ReactDOMClient.createRoot(container);
|
|
|
|
await act(() => {
|
|
root.render(
|
|
<select onChange={noop} defaultValue="monkey">
|
|
<option value="donkey">like a Temporal.PlainDate</option>
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
</select>,
|
|
);
|
|
});
|
|
|
|
container = document.createElement('div');
|
|
root = ReactDOMClient.createRoot(container);
|
|
await expect(async () => {
|
|
await expect(
|
|
act(() => {
|
|
root.render(
|
|
<select onChange={noop} value={new TemporalLike()}>
|
|
<option value={new TemporalLike()}>
|
|
like a Temporal.PlainDate
|
|
</option>
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
</select>,
|
|
);
|
|
}),
|
|
).rejects.toThrowError(new TypeError('prod message'));
|
|
}).toErrorDev([
|
|
'The provided `value` attribute is an unsupported type TemporalLike.' +
|
|
' This value must be coerced to a string before using it here.',
|
|
'The provided `value` attribute is an unsupported type TemporalLike.' +
|
|
' This value must be coerced to a string before using it here.',
|
|
]);
|
|
});
|
|
|
|
it('should not warn about missing onChange if value is not set', async () => {
|
|
const container = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(container);
|
|
await expect(
|
|
act(() => {
|
|
root.render(
|
|
<select>
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
<option value="gorilla">A gorilla!</option>
|
|
</select>,
|
|
);
|
|
}),
|
|
).resolves.not.toThrow();
|
|
});
|
|
|
|
it('should not throw an error about missing onChange if value is undefined', async () => {
|
|
const container = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(container);
|
|
await expect(
|
|
act(() => {
|
|
root.render(
|
|
<select value={undefined}>
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
<option value="gorilla">A gorilla!</option>
|
|
</select>,
|
|
);
|
|
}),
|
|
).resolves.not.toThrow();
|
|
});
|
|
|
|
it('should not warn about missing onChange if onChange is set', async () => {
|
|
const change = jest.fn();
|
|
const container = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(container);
|
|
await expect(
|
|
act(() => {
|
|
root.render(
|
|
<select value="monkey" onChange={change}>
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
<option value="gorilla">A gorilla!</option>
|
|
</select>,
|
|
);
|
|
}),
|
|
).resolves.not.toThrow();
|
|
});
|
|
|
|
it('should not warn about missing onChange if disabled is true', async () => {
|
|
const container = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(container);
|
|
await expect(
|
|
act(() => {
|
|
root.render(
|
|
<select value="monkey" disabled={true}>
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
<option value="gorilla">A gorilla!</option>
|
|
</select>,
|
|
);
|
|
}),
|
|
).resolves.not.toThrow();
|
|
});
|
|
|
|
it('should warn about missing onChange if value is false', async () => {
|
|
const container = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(container);
|
|
await expect(async () => {
|
|
await act(() => {
|
|
root.render(
|
|
<select value={false}>
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
<option value="gorilla">A gorilla!</option>
|
|
</select>,
|
|
);
|
|
});
|
|
}).toErrorDev(
|
|
'Warning: You provided a `value` prop to a form ' +
|
|
'field without an `onChange` handler. This will render a read-only ' +
|
|
'field. If the field should be mutable use `defaultValue`. ' +
|
|
'Otherwise, set `onChange`.',
|
|
);
|
|
});
|
|
|
|
it('should warn about missing onChange if value is 0', async () => {
|
|
const container = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(container);
|
|
await expect(async () => {
|
|
await act(() => {
|
|
root.render(
|
|
<select value={0}>
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
<option value="gorilla">A gorilla!</option>
|
|
</select>,
|
|
);
|
|
});
|
|
}).toErrorDev(
|
|
'Warning: You provided a `value` prop to a form ' +
|
|
'field without an `onChange` handler. This will render a read-only ' +
|
|
'field. If the field should be mutable use `defaultValue`. ' +
|
|
'Otherwise, set `onChange`.',
|
|
);
|
|
});
|
|
|
|
it('should warn about missing onChange if value is "0"', async () => {
|
|
const container = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(container);
|
|
await expect(async () => {
|
|
await act(() => {
|
|
root.render(
|
|
<select value="0">
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
<option value="gorilla">A gorilla!</option>
|
|
</select>,
|
|
);
|
|
});
|
|
}).toErrorDev(
|
|
'Warning: You provided a `value` prop to a form ' +
|
|
'field without an `onChange` handler. This will render a read-only ' +
|
|
'field. If the field should be mutable use `defaultValue`. ' +
|
|
'Otherwise, set `onChange`.',
|
|
);
|
|
});
|
|
|
|
it('should warn about missing onChange if value is ""', async () => {
|
|
const container = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(container);
|
|
await expect(async () => {
|
|
await act(() => {
|
|
root.render(
|
|
<select value="">
|
|
<option value="monkey">A monkey!</option>
|
|
<option value="giraffe">A giraffe!</option>
|
|
<option value="gorilla">A gorilla!</option>
|
|
</select>,
|
|
);
|
|
});
|
|
}).toErrorDev(
|
|
'Warning: You provided a `value` prop to a form ' +
|
|
'field without an `onChange` handler. This will render a read-only ' +
|
|
'field. If the field should be mutable use `defaultValue`. ' +
|
|
'Otherwise, set `onChange`.',
|
|
);
|
|
});
|
|
});
|
|
});
|