Repository: mui/material-ui (TypeScript)
Date: 2020-03-22 15:20:27+00:00
Task type: Bug Fix

Issue:
[Autocomplete] Warn when value and option doesn't match
Hi,
I am trying to change list options and default value from state. List option is ok but default value is never update.

I have add an exemple on this code box :
https://codesandbox.io/s/test-material-ui-autocomplete-700nh?fontsize=14&hidenavigation=1&theme=dark

Thank you for yours ideas...



Issue discussion:
👋 Thanks for using Material-UI!

We use GitHub issues exclusively as a bug and feature requests tracker, however,
this issue appears to be a support request.

For support, please check out https://material-ui.com/getting-started/support/. Thanks!

If you have a question on StackOverflow, you are welcome to link to it here, it might help others.
If your issue is subsequently confirmed as a bug,  and the report follows the issue template, it can be reopened.
Control the component, we don't aim to support `defaultValue` prop updates (even if a native input do). 
Thank you for your reply. I am newbie andI think dificulty come from here... but when I add state variable on autocomplete value, items are show on textarea but not selected on dropdown list (and I can selected them second time).
Do you have a sample of controlled used of autocomplete ?
You can find one controlled demo in https://material-ui.com/components/autocomplete/#playground.
I have see this but when I try to manage values directely, dropdownlist is not updated with selected values passed by state as you can see on my exemple :
https://codesandbox.io/s/test-material-ui-autocomplete-o3uic?fontsize=14&hidenavigation=1&theme=dark

Push button change list and add item preselected but in dropdown list this item is not selected and we can add it another time.

@guimex22 The value needs to be referentially equal to the option to consider it selected. We have #18443 to customize this.

However, I think that we have an opportunity to warn about this case. What do you think of this diff?
```diff
diff --git a/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js b/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js
index f833a0c0c..5cd34fa78 100644
--- a/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js
+++ b/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js
@@ -247,6 +247,35 @@ export default function useAutocomplete(props) {

   popupOpen = freeSolo && filteredOptions.length === 0 ? false : popupOpen;

+  if (process.env.NODE_ENV !== 'production') {
+    if (value !== null && !freeSolo && options.length > 0) {
+      const missingValue = (multiple ? value : [value]).filter(
+        value2 => !options.some(option => getOptionSelected(option, value2)),
+      );
+
+      if (missingValue.length > 0) {
+        // eslint-disable-next-line react-hooks/rules-of-hooks
+        console.warn(
+          [
+            `Material-UI: you have provided an out-of-range value${
+              missingValue.length > 1 ? 's' : ''
+            } \`${
+              missingValue.length > 1
+                ? JSON.stringify(missingValue)
+                : JSON.stringify(missingValue[0])
+            }\` for the autocomplete ${id ? `(id="${id}") ` : ''}component.`,
+            '',
+            'Consider providing a value that matches one of the available options.',
+            'You can use the `getOptionSelected` prop to customize the equality test.',
+          ].join('\n'),
+        );
+      }
+    }
+  }
+
   const focusTag = useEventCallback(tagToFocus => {
     if (tagToFocus === -1) {
       inputRef.current.focus();
```

Do you want to work on a pull request? :)
@oliviertassinari , I'd like to work on this if the op doesn't have the time. Can I go ahead?
@blueyedgeek Awesome, feel free to go ahead 👌 
@oliviertassinari I'd like to jump on this if no one is currently active.  
@hefedev Great :) we will also need a test case. 
@hefedev, @oliviertassinari I had meant to work on this but some personal issues came up and I couldn't find the time to do it. Glad someone else was able to put in the required effort 👏🏾
I won't be able to work on this anymore. but for the new first timers please check below and make sure you need a test case for it. 

https://github.com/mui-org/material-ui/blob/575776f3004c6ac655b128fbdb30bd4b35115ab7/packages/material-ui-lab/src/Autocomplete/Autocomplete.test.js#L553 
@oliviertassinari  Can I take this issue?

Solution patch:
diff --git a/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js b/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js
--- a/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js
+++ b/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js
@@ -200,10 +200,9 @@ export default function useAutocomplete(props) {
           const erroneousReturn =
             optionLabel === undefined ? 'undefined' : `${typeof optionLabel} (${optionLabel})`;
           console.error(
-            [
-              `Material-UI: the \`getOptionLabel\` method of ${componentName} returned ${erroneousReturn} instead of a string for`,
-              JSON.stringify(newValue),
-            ].join('\n'),
+            `Material-UI: the \`getOptionLabel\` method of ${componentName} returned ${erroneousReturn} instead of a string for ${JSON.stringify(
+              newValue,
+            )}.`,
           );
         }
       }
@@ -256,6 +255,28 @@ export default function useAutocomplete(props) {
 
   popupOpen = freeSolo && filteredOptions.length === 0 ? false : popupOpen;
 
+  if (process.env.NODE_ENV !== 'production') {
+    if (value !== null && !freeSolo && options.length > 0) {
+      const missingValue = (multiple ? value : [value]).filter(
+        (value2) => !options.some((option) => getOptionSelected(option, value2)),
+      );
+
+      if (missingValue.length > 0) {
+        console.warn(
+          [
+            `Material-UI: the value provided to ${componentName} is invalid.`,
+            `None of the options match with \`${
+              missingValue.length > 1
+                ? JSON.stringify(missingValue)
+                : JSON.stringify(missingValue[0])
+            }\`.`,
+            'You can use the `getOptionSelected` prop to customize the equality test.',
+          ].join('\n'),
+        );
+      }
+    }
+  }
+
   const focusTag = useEventCallback((tagToFocus) => {
     if (tagToFocus === -1) {
       inputRef.current.focus();
diff --git a/packages/material-ui/src/Tabs/Tabs.js b/packages/material-ui/src/Tabs/Tabs.js
--- a/packages/material-ui/src/Tabs/Tabs.js
+++ b/packages/material-ui/src/Tabs/Tabs.js
@@ -153,8 +153,8 @@ const Tabs = React.forwardRef(function Tabs(props, ref) {
           if (!tab) {
             console.error(
               [
-                `Material-UI: the value provided \`${value}\` to the Tabs component is invalid.`,
-                'None of the Tabs children have this value.',
+                `Material-UI: the value provided to the Tabs component is invalid.`,
+                `None of the Tabs' children match with \`${value}\`.`,
                 valueToIndex.keys
                   ? `You can provide one of the following values: ${Array.from(
                       valueToIndex.keys(),


Test patch:
diff --git a/packages/material-ui-lab/src/Autocomplete/Autocomplete.test.js b/packages/material-ui-lab/src/Autocomplete/Autocomplete.test.js
--- a/packages/material-ui-lab/src/Autocomplete/Autocomplete.test.js
+++ b/packages/material-ui-lab/src/Autocomplete/Autocomplete.test.js
@@ -2,7 +2,7 @@ import * as React from 'react';
 import { expect } from 'chai';
 import { createMount, getClasses } from '@material-ui/core/test-utils';
 import describeConformance from '@material-ui/core/test-utils/describeConformance';
-import consoleErrorMock from 'test/utils/consoleErrorMock';
+import consoleErrorMock, { consoleWarnMock } from 'test/utils/consoleErrorMock';
 import { spy } from 'sinon';
 import { createClientRender, fireEvent } from 'test/utils/createClientRender';
 import { createFilterOptions } from '../useAutocomplete/useAutocomplete';
@@ -777,10 +777,12 @@ describe('<Autocomplete />', () => {
   describe('warnings', () => {
     beforeEach(() => {
       consoleErrorMock.spy();
+      consoleWarnMock.spy();
     });
 
     afterEach(() => {
       consoleErrorMock.reset();
+      consoleWarnMock.reset();
     });
 
     it('warn if getOptionLabel do not return a string', () => {
@@ -836,6 +838,25 @@ describe('<Autocomplete />', () => {
         'The component expects a single value to match a given option but found 2 matches.',
       );
     });
+
+    it('warn if value does not exist in options list', () => {
+      const value = 'not a good value';
+      const options = ['first option', 'second option'];
+
+      render(
+        <Autocomplete
+          {...defaultProps}
+          value={value}
+          options={options}
+          renderInput={(params) => <TextField {...params} />}
+        />,
+      );
+
+      expect(consoleWarnMock.callCount()).to.equal(4);
+      expect(consoleWarnMock.messages()[0]).to.include(
+        'None of the options match with `"not a good value"`',
+      );
+    });
   });
 
   describe('prop: options', () => {
diff --git a/test/utils/consoleErrorMock.js b/test/utils/consoleErrorMock.js
--- a/test/utils/consoleErrorMock.js
+++ b/test/utils/consoleErrorMock.js
@@ -1,3 +1,4 @@
+/* eslint-disable no-console */
 import utilFormat from 'format-util';
 import { spy } from 'sinon';
 
@@ -14,22 +15,26 @@ import { spy } from 'sinon';
  *   warning.restore();
  * });
  */
-class ConsoleErrorMock {
+export class ConsoleMock {
   consoleErrorContainer;
 
+  constructor(methodName) {
+    this.methodName = methodName;
+  }
+
   spy = () => {
-    this.consoleErrorContainer = console.error;
-    console.error = spy();
+    this.consoleErrorContainer = console[this.methodName];
+    console[this.methodName] = spy();
   };
 
   reset = () => {
-    console.error = this.consoleErrorContainer;
+    console[this.methodName] = this.consoleErrorContainer;
     delete this.consoleErrorContainer;
   };
 
   callCount = () => {
     if (this.consoleErrorContainer) {
-      return console.error.callCount;
+      return console[this.methodName].callCount;
     }
 
     throw new Error('Requested call count before spy() was called');
@@ -45,7 +50,7 @@ class ConsoleErrorMock {
   /**
    * returns the formatted message for each call
    *
-   * you could call console.error("type %s", "foo") which would log
+   * you could call console[this.methodName]("type %s", "foo") which would log
    * "type foo". If you  want to assert on the actual message use messages() instead
    */
   messages = () => {
@@ -56,11 +61,13 @@ class ConsoleErrorMock {
     /**
      * @type {import('sinon').SinonSpy}
      */
-    const consoleSpy = console.error;
+    const consoleSpy = console[this.methodName];
     return consoleSpy.args.map((loggerArgs) => {
       return utilFormat(...loggerArgs);
     });
   };
 }
 
-export default new ConsoleErrorMock();
+export const consoleWarnMock = new ConsoleMock('warn');
+
+export default new ConsoleMock('error');