Repository: mozilla-services/react-jsonschema-form
Pull request #381 (2016-11-08 13:42:33)

Don't pass consumed error to child schema
### Reasons for making this change

This is a bug fix.

I ended with an error being rendered twice. This happens when you have an `errorSchema` containing an error for the whole object, not a property of the object. `SchemaField` consumes `errorSchema.__errors` by creating an `ErrorList` from it and passing it to the `FieldTemplate`. Since `errorSchema.__errors` is consumed already this way, it shouldn't be passed to the child schema component.

### Checklist

* [ ] **I'm updating documentation**
  - [ ] I've checked the rendering of the Markdown text I've added
  - [ ] If I'm adding a new section, I've updated the Table of Content
* [ ] **I'm adding or updating code**
  - [ ] I've added and/or updated tests
  - [ ] I've updated docs if needed
* [ ] **I'm adding a new feature**
  - [ ] I've updated the playground with an example use of the feature



Patch:
diff --git a/README.md b/README.md
index ecc15ab47..d7380e772 100644
--- a/README.md
+++ b/README.md
@@ -550,11 +550,9 @@ By default, checkboxes are stacked but if you prefer them inline:
 
 ```js
 const uiSchema = {
-  "ui:widget": {
-    component: "checkboxes",
-    options: {
-      inline: true
-    }
+  "ui:widget": "checkboxes",
+  "ui:options": {
+    inline: true
   }
 };
 ```
@@ -785,9 +783,11 @@ render((
 
 This is useful if you expose the `uiSchema` as pure JSON, which can't carry functions.
 
+> Note: Until 0.40.0 it was possible to register a widget as object with shape `{ component: MyCustomWidget, options: {...} }`. This undocumented API has been removed. Instead, you can register a custom widget with a React `defaultProps` property. `defaultProps.options` can be an object containing your custom options.
+
 #### Custom widget options
 
-If you need to pass options to your custom widget, change your `ui:widget` value to be an object having the following structure:
+If you need to pass options to your custom widget, you can add a `ui:options` object containing those properties. If the widget has `defaultProps`, the options will be merged with the (optional) options object from `defaultProps`:
 
 ```jsx
 const schema = {
@@ -796,18 +796,24 @@ const schema = {
 
 function MyCustomWidget(props) {
   const {options} = props;
-  return <input style={{options.backgroundColor}} />;
+  const {color, backgroundColor} = options;
+  return <input style={{color, backgroundColor}} />;
 }
 
+MyCustomWidget.defaultProps = {
+  options: {
+    color: "red"
+  }
+};
+
 const uiSchema = {
-  "ui:widget": {
-    options: {
-      backgroundColor: "yellow",
-    },
-    component: MyCustomWidget
+  "ui:widget": MyCustomWidget,
+  "ui:options": {
+    backgroundColor: "yellow"
   }
 };
 
+// renders red on yellow input
 render((
   <Form schema={schema}
         uiSchema={uiSchema} />
@@ -816,6 +822,8 @@ render((
 
 > Note: This also applies to [registered custom components](#custom-component-registration).
 
+> Note: Since v0.41.0, the `ui:widget` object API, where a widget and options were specified with `"ui:widget": {component, options}` shape, is deprecated. It will be removed in a future release.
+
 ### Custom field components
 
 You can provide your own field components to a uiSchema for basically any json schema data type, by specifying a `ui:field` property.
diff --git a/playground/samples/numbers.js b/playground/samples/numbers.js
index 02a3d21b1..c67fd7c54 100644
--- a/playground/samples/numbers.js
+++ b/playground/samples/numbers.js
@@ -41,11 +41,9 @@ module.exports = {
       "ui:widget": "updown"
     },
     numberEnumRadio: {
-      "ui:widget": {
-        component: "radio",
-        options: {
-          inline: true
-        }
+      "ui:widget": "radio",
+      "ui:options": {
+        inline: true
       }
     },
     integerRange: {
diff --git a/playground/samples/widgets.js b/playground/samples/widgets.js
index 9482a9157..a91820598 100644
--- a/playground/samples/widgets.js
+++ b/playground/samples/widgets.js
@@ -111,39 +111,35 @@ module.exports = {
       "ui:readonly": true
     },
     widgetOptions: {
-      "ui:widget": {
-        component: ({value, onChange, options}) => {
-          const {backgroundColor} = options;
-          return (
-            <input className="form-control"
-              onChange={(event) => onChange(event.target.value)}
-              style={{backgroundColor}}
-              value={value} />
-          );
-        },
-        options: {
-          backgroundColor: "yellow",
-        }
+      "ui:widget": ({value, onChange, options}) => {
+        const {backgroundColor} = options;
+        return (
+          <input className="form-control"
+            onChange={(event) => onChange(event.target.value)}
+            style={{backgroundColor}}
+            value={value} />
+        );
+      },
+      "ui:options": {
+        backgroundColor: "yellow"
       }
     },
     selectWidgetOptions: {
-      "ui:widget": {
-        component: ({value, onChange, options}) => {
-          const {enumOptions, backgroundColor} = options;
-          return (
-            <select className="form-control"
-              style={{backgroundColor}}
-              value={value}
-              onChange={(event) => onChange(event.target.value)}>{
-              enumOptions.map(({label, value}, i) => {
-                return <option key={i} value={value}>{label}</option>;
-              })
-            }</select>
-          );
-        },
-        options: {
-          backgroundColor: "pink",
-        }
+      "ui:widget": ({value, onChange, options}) => {
+        const {enumOptions, backgroundColor} = options;
+        return (
+          <select className="form-control"
+            style={{backgroundColor}}
+            value={value}
+            onChange={(event) => onChange(event.target.value)}>{
+            enumOptions.map(({label, value}, i) => {
+              return <option key={i} value={value}>{label}</option>;
+            })
+          }</select>
+        );
+      },
+      "ui:options": {
+        backgroundColor: "pink"
       }
     },
   },
diff --git a/src/components/fields/ArrayField.js b/src/components/fields/ArrayField.js
index 2052218d0..1bb10fe0c 100644
--- a/src/components/fields/ArrayField.js
+++ b/src/components/fields/ArrayField.js
@@ -1,8 +1,9 @@
 import React, {Component, PropTypes} from "react";
 
 import {
-  getAlternativeWidget,
+  getWidget,
   getDefaultFormState,
+  getUiOptions,
   isMultiSelect,
   isFilesArray,
   isFixedItems,
@@ -178,10 +179,7 @@ class ArrayField extends Component {
     const {definitions, fields} = this.props.registry;
     const {TitleField, DescriptionField} = fields;
     const itemsSchema = retrieveSchema(schema.items, definitions);
-    const {addable} = {
-      addable: true,
-      ...uiSchema["ui:options"]
-    };
+    const {addable=true} = getUiOptions(uiSchema);
 
     return (
       <fieldset
@@ -226,17 +224,15 @@ class ArrayField extends Component {
     const {items} = this.state;
     const {widgets, definitions} = this.props.registry;
     const itemsSchema = retrieveSchema(schema.items, definitions);
-
-    const Widget = getAlternativeWidget(schema, uiSchema["ui:widget"] || "select", widgets);
+    const enumOptions = optionsList(itemsSchema);
+    const {widget="select", ...options} = {...getUiOptions(uiSchema), enumOptions};
+    const Widget = getWidget(schema, widget, widgets);
     return (
       <Widget
         id={idSchema && idSchema.$id}
         multiple
         onChange={this.onSelectChange}
-        options={{
-          ...Widget.defaultProps.options,
-          enumOptions: optionsList(itemsSchema),
-        }}
+        options={options}
         schema={schema}
         value={items}
         disabled={disabled}
@@ -246,12 +242,15 @@ class ArrayField extends Component {
   }
 
   renderFiles() {
-    const {schema, idSchema, name, disabled, readonly, autofocus} = this.props;
+    const {schema, uiSchema, idSchema, name, disabled, readonly, autofocus} = this.props;
     const title = schema.title || name;
     const {items} = this.state;
-    const {FileWidget} = this.props.registry.widgets;
+    const {widgets} = this.props.registry;
+    const {widget="files", ...options} = getUiOptions(uiSchema);
+    const Widget = getWidget(schema, widget, widgets);
     return (
-      <FileWidget
+      <Widget
+        options={options}
         id={idSchema && idSchema.$id}
         multiple
         onChange={this.onSelectChange}
@@ -284,10 +283,7 @@ class ArrayField extends Component {
       retrieveSchema(item, definitions));
     const additionalSchema = allowAdditionalItems(schema) ?
       retrieveSchema(schema.additionalItems, definitions) : null;
-    const {addable} = {
-      addable: true,
-      ...uiSchema["ui:options"]
-    };
+    const {addable=true} = getUiOptions(uiSchema);
     const canAdd = addable && additionalSchema;
 
     if (!items || items.length < itemSchemas.length) {
@@ -435,7 +431,9 @@ if (process.env.NODE_ENV !== "production") {
     schema: PropTypes.object.isRequired,
     uiSchema: PropTypes.shape({
       "ui:options": PropTypes.shape({
-        orderable: PropTypes.bool
+        addable: PropTypes.bool,
+        orderable: PropTypes.bool,
+        removable: PropTypes.bool
       })
     }),
     idSchema: PropTypes.object,
diff --git a/src/components/fields/BooleanField.js b/src/components/fields/BooleanField.js
index 1f6afb1a3..57ee2e82f 100644
--- a/src/components/fields/BooleanField.js
+++ b/src/components/fields/BooleanField.js
@@ -2,27 +2,12 @@ import React, {PropTypes} from "react";
 
 import {
   defaultFieldValue,
-  getAlternativeWidget,
+  getWidget,
+  getUiOptions,
   optionsList,
-  getDefaultRegistry,
-  isObject,
+  getDefaultRegistry
 } from "../../utils";
 
-
-function buildOptions(schema, uiWidget) {
-  // Note: uiWidget can be undefined, a string or an object; we only deal with
-  // the inline option when we're provided a definition object.
-  return {
-    inline: isObject(uiWidget) &&
-            isObject(uiWidget.options) &&
-            uiWidget.options.inline,
-    enumOptions: optionsList(Object.assign({
-      enumNames: ["true", "false"],
-      enum: [true, false]
-    }, {enumNames: schema.enumNames}))
-  };
-}
-
 function BooleanField(props) {
   const {
     schema,
@@ -39,26 +24,25 @@ function BooleanField(props) {
   } = props;
   const {title} = schema;
   const {widgets, formContext} = registry;
-  const widget = uiSchema["ui:widget"];
-  const commonProps = {
-    schema,
-    id: idSchema && idSchema.$id,
-    onChange,
-    label: (title === undefined) ? name : title,
-    value: defaultFieldValue(formData, schema),
-    required,
-    disabled,
-    readonly,
-    registry,
-    formContext,
-    autofocus,
-  };
-  if (widget) {
-    const Widget = getAlternativeWidget(schema, widget, widgets);
-    return <Widget options={buildOptions(schema, uiSchema["ui:widget"])} {...commonProps}/>;
-  }
-  const {CheckboxWidget} = registry.widgets;
-  return <CheckboxWidget {...commonProps}/>;
+  const {widget="checkbox", ...options} = getUiOptions(uiSchema);
+  const Widget = getWidget(schema, widget, widgets);
+  const enumOptions = optionsList({
+    enum: [true, false],
+    enumNames: schema.enumNames || ["yes", "no"]
+  });
+  return <Widget
+    options={{...options, enumOptions}}
+    schema={schema}
+    id={idSchema && idSchema.$id}
+    onChange={onChange}
+    label={title === undefined ? name : title}
+    value={defaultFieldValue(formData, schema)}
+    required={required}
+    disabled={disabled}
+    readonly={readonly}
+    registry={registry}
+    formContext={formContext}
+    autofocus={autofocus}/>;
 }
 
 if (process.env.NODE_ENV !== "production") {
diff --git a/src/components/fields/SchemaField.js b/src/components/fields/SchemaField.js
index 165e519dc..45ce1000f 100644
--- a/src/components/fields/SchemaField.js
+++ b/src/components/fields/SchemaField.js
@@ -157,12 +157,15 @@ function SchemaField(props) {
     displayLabel = false;
   }
 
+  const {__errors, ...fieldErrorSchema} = errorSchema;
+
   const field = (
     <FieldComponent {...props}
       schema={schema}
       disabled={disabled}
       readonly={readonly}
       autofocus={autofocus}
+      errorSchema={fieldErrorSchema}
       formContext={formContext}/>
   );
 
@@ -170,7 +173,7 @@ function SchemaField(props) {
   const id = idSchema.$id;
   const label = props.schema.title || schema.title || name;
   const description = props.schema.description || schema.description;
-  const errors = errorSchema.__errors;
+  const errors = __errors;
   const help = uiSchema["ui:help"];
   const hidden = uiSchema["ui:widget"] === "hidden";
   const classNames = [
diff --git a/src/components/fields/StringField.js b/src/components/fields/StringField.js
index c777228dc..f1ed3eb0f 100644
--- a/src/components/fields/StringField.js
+++ b/src/components/fields/StringField.js
@@ -2,7 +2,8 @@ import React, {PropTypes} from "react";
 
 import {
   defaultFieldValue,
-  getAlternativeWidget,
+  getWidget,
+  getUiOptions,
   optionsList,
   getDefaultRegistry
 } from "../../utils";
@@ -15,46 +16,34 @@ function StringField(props) {
     uiSchema,
     idSchema,
     formData,
-    registry,
     required,
     disabled,
     readonly,
     autofocus,
+    registry,
     onChange
   } = props;
-  const {title} = schema;
+  const {title, format} = schema;
   const {widgets, formContext} = registry;
-  const widget = uiSchema["ui:widget"] || schema.format;
-  const placeholder = uiSchema["ui:placeholder"] || "";
-  const commonProps = {
-    schema,
-    id: idSchema && idSchema.$id,
-    label: (title === undefined) ? name : title,
-    value: defaultFieldValue(formData, schema),
-    onChange,
-    required,
-    disabled,
-    readonly,
-    formContext,
-    autofocus,
-    registry,
-  };
-
-  const {TextWidget, SelectWidget} = widgets;
+  const enumOptions = Array.isArray(schema.enum) && optionsList(schema);
+  const defaultWidget = format || (enumOptions ? "select" : "text");
+  const {widget=defaultWidget, placeholder="", ...options} = getUiOptions(uiSchema);
+  const Widget = getWidget(schema, widget, widgets);
 
-  if (Array.isArray(schema.enum)) {
-    const enumOptions = optionsList(schema);
-    if (widget) {
-      const Widget = getAlternativeWidget(schema, widget, widgets, {enumOptions});
-      return <Widget {...commonProps}/>;
-    }
-    return <SelectWidget options={{enumOptions}} {...commonProps}/>;
-  }
-  if (widget) {
-    const Widget = getAlternativeWidget(schema, widget, widgets);
-    return <Widget {...commonProps} placeholder={placeholder}/>;
-  }
-  return <TextWidget {...commonProps} placeholder={placeholder}/>;
+  return <Widget
+    options={{...options, enumOptions}}
+    schema={schema}
+    id={idSchema && idSchema.$id}
+    label={title === undefined ? name : title}
+    value={defaultFieldValue(formData, schema)}
+    onChange={onChange}
+    required={required}
+    disabled={disabled}
+    readonly={readonly}
+    formContext={formContext}
+    autofocus={autofocus}
+    registry={registry}
+    placeholder={placeholder}/>;
 }
 
 if (process.env.NODE_ENV !== "production") {
diff --git a/src/utils.js b/src/utils.js
index 9973f68d6..2b0bfa29f 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -1,32 +1,47 @@
+import React from "react";
+
 import "setimmediate";
 
 
-const altWidgetMap = {
+const widgetMap = {
   boolean: {
+    checkbox: "CheckboxWidget",
     radio: "RadioWidget",
     select: "SelectWidget",
     hidden: "HiddenWidget",
   },
   string: {
+    text: "TextWidget",
     password: "PasswordWidget",
+    email: "EmailWidget",
+    hostname: "TextWidget",
+    ipv4: "TextWidget",
+    ipv6: "TextWidget",
+    uri: "URLWidget",
+    "data-url": "FileWidget",
     radio: "RadioWidget",
     select: "SelectWidget",
     textarea: "TextareaWidget",
     hidden: "HiddenWidget",
     date: "DateWidget",
     datetime: "DateTimeWidget",
+    "date-time": "DateTimeWidget",
     "alt-date": "AltDateWidget",
     "alt-datetime": "AltDateTimeWidget",
     color: "ColorWidget",
     file: "FileWidget",
   },
   number: {
+    text: "TextWidget",
+    select: "SelectWidget",
     updown: "UpDownWidget",
     range: "RangeWidget",
     radio: "RadioWidget",
     hidden: "HiddenWidget",
   },
   integer: {
+    text: "TextWidget",
+    select: "SelectWidget",
     updown: "UpDownWidget",
     range: "RangeWidget",
     radio: "RadioWidget",
@@ -35,20 +50,10 @@ const altWidgetMap = {
   array: {
     select: "SelectWidget",
     checkboxes: "CheckboxesWidget",
+    files: "FileWidget"
   }
 };
 
-const stringFormatWidgets = {
-  "date-time": "DateTimeWidget",
-  "date": "DateWidget",
-  "email": "EmailWidget",
-  "hostname": "TextWidget",
-  "ipv4": "TextWidget",
-  "ipv6": "TextWidget",
-  "uri": "URLWidget",
-  "data-url": "FileWidget",
-};
-
 export function getDefaultRegistry() {
   const load = (prefix, arr) => arr.reduce((obj, comp) => {
     obj[comp] = require(`./components/${prefix}/${comp}`).default;
@@ -99,34 +104,21 @@ export function defaultFieldValue(formData, schema) {
   return typeof formData === "undefined" ? schema.default : formData;
 }
 
-export function getAlternativeWidget(
-  schema,
-  widget,
-  registeredWidgets={},
-  widgetOptions={}
-) {
-  const {type, format} = schema;
-
-  function setDefaultOptions(widget) {
-    const {defaultProps={}} = widget;
-    widget.defaultProps = {
-      ...defaultProps,
-      options: {
-        ...defaultProps.options,
-        ...widgetOptions
-      }
-    };
-    return widget;
-  }
+export function getWidget(schema, widget, registeredWidgets={}) {
+  const {type} = schema;
 
-  if (typeof widget === "function") {
-    return setDefaultOptions(widget);
+  function mergeOptions(Widget) {
+    // cache return value as property of widget for proper react reconciliation
+    if (!Widget.MergedWidget) {
+      const defaultOptions = Widget.defaultProps && Widget.defaultProps.options || {};
+      Widget.MergedWidget = ({options={}, ...props}) =>
+        <Widget options={{...defaultOptions, ...options}} {...props}/>;
+    }
+    return Widget.MergedWidget;
   }
 
-  if (isObject(widget)) {
-    const {component, options} = widget;
-    const mergedOptions = {...options, ...widgetOptions};
-    return getAlternativeWidget(schema, component, registeredWidgets, mergedOptions);
+  if (typeof widget === "function") {
+    return mergeOptions(widget);
   }
 
   if (typeof widget !== "string") {
@@ -135,25 +127,19 @@ export function getAlternativeWidget(
 
   if (registeredWidgets.hasOwnProperty(widget)) {
     const registeredWidget = registeredWidgets[widget];
-    return getAlternativeWidget(schema, registeredWidget, registeredWidgets, widgetOptions);
-  }
-
-  if (!altWidgetMap.hasOwnProperty(type)) {
-    throw new Error(`No alternative widget for type ${type}`);
+    return getWidget(schema, registeredWidget, registeredWidgets);
   }
 
-  if (altWidgetMap[type].hasOwnProperty(widget)) {
-    const altWidget = registeredWidgets[altWidgetMap[type][widget]];
-    return getAlternativeWidget(schema, altWidget, registeredWidgets, widgetOptions);
+  if (!widgetMap.hasOwnProperty(type)) {
+    throw new Error(`No widget for type "${type}"`);
   }
 
-  if (type === "string" && stringFormatWidgets.hasOwnProperty(format)) {
-    const stringFormatWidget = registeredWidgets[stringFormatWidgets[format]];
-    return getAlternativeWidget(schema, stringFormatWidget, registeredWidgets, widgetOptions);
+  if (widgetMap[type].hasOwnProperty(widget)) {
+    const registeredWidget = registeredWidgets[widgetMap[type][widget]];
+    return getWidget(schema, registeredWidget, registeredWidgets);
   }
 
-  const info = type === "string" && format ? `/${format}` : "";
-  throw new Error(`No alternative widget "${widget}" for type ${type}${info}`);
+  throw new Error(`No widget "${widget}" for type "${type}"`);
 }
 
 function computeDefaults(schema, parentDefaults, definitions={}) {
@@ -208,6 +194,22 @@ export function getDefaultFormState(_schema, formData, definitions={}) {
   return formData || defaults;
 }
 
+export function getUiOptions(uiSchema) {
+  // get all passed options from ui:widget, ui:options, and ui:<optionName>
+  return Object.keys(uiSchema).filter(key => key.indexOf("ui:") === 0).reduce((options, key) => {
+    const value = uiSchema[key];
+
+    if (key === "ui:widget" && isObject(value)) {
+      console.warn("Setting options via ui:widget object is deprecated, use ui:options instead");
+      return {...options, ...(value.options || {}), widget: value.component};
+    }
+    if (key === "ui:options" && isObject(value)) {
+      return {...options, ...value};
+    }
+    return {...options, [key.substring(3)]: value};
+  }, {});
+}
+
 export function isObject(thing) {
   return typeof thing === "object" && thing !== null && !Array.isArray(thing);
 }


Test patch:
diff --git a/test/SchemaField_test.js b/test/SchemaField_test.js
index 701809cd9..70924f81f 100644
--- a/test/SchemaField_test.js
+++ b/test/SchemaField_test.js
@@ -1,5 +1,6 @@
 import React from "react";
 import {expect} from "chai";
+import {Simulate} from "react-addons-test-utils";
 
 import SchemaField from "../src/components/fields/SchemaField";
 import TitleField from "../src/components/fields/TitleField";
@@ -195,4 +196,52 @@ describe("SchemaField", () => {
         .to.eql("A Foo field");
     });
   });
+
+  describe("errors", () => {
+    const schema = {
+      type: "object",
+      properties: {
+        foo: {type: "string"}
+      }
+    };
+
+    const uiSchema = {
+      "ui:field": (props) => {
+        const {uiSchema, ...fieldProps} = props; //eslint-disable-line
+        return <SchemaField {...fieldProps}/>;
+      }
+    };
+
+    function validate(formData, errors) {
+      errors.addError("container");
+      errors.foo.addError("test");
+      return errors;
+    }
+
+    function submit(node) {
+      try {
+        Simulate.submit(node);
+      } catch (e) {
+        // Silencing error thrown as failure is expected here
+      }
+    }
+
+    it("should render it's own errors", () => {
+      const {node} = createFormComponent({schema, uiSchema, validate});
+      submit(node);
+
+      const matches = node.querySelectorAll("form > .form-group > div > .error-detail .text-danger");
+      expect(matches).to.have.length.of(1);
+      expect(matches[0].textContent).to.eql("container");
+    });
+
+    it("should pass errors to child component", () => {
+      const {node} = createFormComponent({schema, uiSchema, validate});
+      submit(node);
+
+      const matches = node.querySelectorAll("form .form-group .form-group .text-danger");
+      expect(matches).to.have.length.of(1);
+      expect(matches[0].textContent).to.contain("test");
+    });
+  });
 });
diff --git a/test/uiSchema_test.js b/test/uiSchema_test.js
index eea810b03..40f7b875b 100644
--- a/test/uiSchema_test.js
+++ b/test/uiSchema_test.js
@@ -67,6 +67,125 @@ describe("uiSchema", () => {
       });
     });
 
+    describe("custom options", () => {
+      let widget, widgets, schema, uiSchema;
+
+      beforeEach(() => {
+        sandbox.stub(console, "warn");
+
+        widget = ({label, options}) => <div id={label} style={options}></div>;
+        widget.defaultProps = {options: {background: "yellow", color: "green"}};
+
+        widgets = {widget};
+
+        // all fields in one schema to catch errors where options passed to one
+        // instance of a widget are persistent across all instances
+        schema = {
+          type: "object",
+          properties: {
+            funcAll: {type: "string"},
+            funcNone: {type: "string"},
+            stringAll: {type: "string"},
+            stringNone: {type: "string"}
+          }
+        };
+
+        uiSchema = {
+          // pass widget as function
+          funcAll: {
+            "ui:widget": {
+              component: widget,
+              options: {
+                background: "purple"
+              }
+            },
+            "ui:options": {
+              margin: "7px"
+            },
+            "ui:padding": "42px"
+          },
+          funcNone: {
+            "ui:widget": widget
+          },
+
+          // pass widget as string
+          stringAll: {
+            "ui:widget": {
+              component: "widget",
+              options: {
+                background: "blue"
+              }
+            },
+            "ui:options": {
+              margin: "19px"
+            },
+            "ui:padding": "41px"
+          },
+          stringNone: {
+            "ui:widget": "widget"
+          }
+        };
+      });
+
+      it("should log warning when deprecated ui:widget: {component, options} api is used", () => {
+        createFormComponent({
+          schema: {type:"string"},
+          uiSchema: {"ui:widget": {component: "widget"}},
+          widgets
+        });
+        expect(console.warn.calledWithMatch(/ui:widget object is deprecated/)).to.be.true;
+      });
+
+      it("should cache MergedWidget instance", () => {
+        expect(widget.MergedWidget).not.to.be.ok;
+        createFormComponent({schema: {type:"string"}, uiSchema: {"ui:widget": "widget"}, widgets});
+        const cached = widget.MergedWidget;
+        expect(cached).to.be.ok;
+        createFormComponent({schema: {type:"string"}, uiSchema: {"ui:widget": "widget"}, widgets});
+        expect(widget.MergedWidget).to.equal(cached);
+      });
+
+      it("should render merged ui:widget options for widget referenced as function", () => {
+        const {node} = createFormComponent({schema, uiSchema, widgets});
+        const widget = node.querySelector("#funcAll");
+
+        expect(widget.style.background).to.equal("purple");
+        expect(widget.style.color).to.equal("green");
+        expect(widget.style.margin).to.equal("7px");
+        expect(widget.style.padding).to.equal("42px");
+      });
+
+      it("should render ui:widget default options for widget referenced as function", () => {
+        const {node} = createFormComponent({schema, uiSchema, widgets});
+        const widget = node.querySelector("#funcNone");
+
+        expect(widget.style.background).to.equal("yellow");
+        expect(widget.style.color).to.equal("green");
+        expect(widget.style.margin).to.equal("");
+        expect(widget.style.padding).to.equal("");
+      });
+
+      it("should render merged ui:widget options for widget referenced as string", () => {
+        const {node} = createFormComponent({schema, uiSchema, widgets});
+        const widget = node.querySelector("#stringAll");
+
+        expect(widget.style.background).to.equal("blue");
+        expect(widget.style.color).to.equal("green");
+        expect(widget.style.margin).to.equal("19px");
+        expect(widget.style.padding).to.equal("41px");
+      });
+
+      it("should render ui:widget default options for widget referenced as string", () => {
+        const {node} = createFormComponent({schema, uiSchema, widgets});
+        const widget = node.querySelector("#stringNone");
+
+        expect(widget.style.background).to.equal("yellow");
+        expect(widget.style.color).to.equal("green");
+        expect(widget.style.margin).to.equal("");
+        expect(widget.style.padding).to.equal("");
+      });
+    });
+
     describe("nested widget", () => {
       const schema = {
         "type": "object",
@@ -163,29 +282,6 @@ describe("uiSchema", () => {
           expect(node.querySelectorAll(".custom")).to.have.length.of(1);
         });
       });
-
-      describe("referenced descriptor", () => {
-        const uiSchema = {
-          "field": {
-            "ui:widget": "custom"
-          }
-        };
-
-        const widgets = {
-          custom: {
-            component: CustomWidget,
-            options: {
-              className: "custom"
-            }
-          }
-        };
-
-        it("should render a custom widget with options", () => {
-          const {node} = createFormComponent({schema, uiSchema, widgets});
-
-          expect(node.querySelectorAll(".custom")).to.have.length.of(1);
-        });
-      });
     });
 
     describe("enum fields native options", () => {
@@ -1020,7 +1116,7 @@ describe("uiSchema", () => {
           node => node.textContent);
 
         expect(labels)
-          .eql(["true", "false"]);
+          .eql(["yes", "no"]);
       });
 
       it("should support formData", () => {
@@ -1075,9 +1171,9 @@ describe("uiSchema", () => {
         const {node} = createFormComponent({schema, uiSchema});
 
         expect(node.querySelectorAll("option")[0].textContent)
-          .eql("true");
+          .eql("yes");
         expect(node.querySelectorAll("option")[1].textContent)
-          .eql("false");
+          .eql("no");
       });
 
       it("should update state when true is selected", () => {