Repository: vazco/uniforms (ts)

Issue:
JSX labels are ignored in v3
The following code JSX renders a field without label:
```tsx
<TextField label={<span style={{ color: 'red' }}>Fancy Label</span>} />
```

Pull request description:
Fixed non-string labels (fixes #816).
This change fixes a bug reported in #816 as well as adds a broad set of tests for `label` handling. We should do the same for `placeholder`, but this will come separately.

Patch:
diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml
index 4391cb7e..00cee05f 100644
--- a/.github/workflows/CI.yml
+++ b/.github/workflows/CI.yml
@@ -12,9 +12,9 @@ jobs:
         node-version: [10.x, 12.x, 14.x]
     steps:
       - name: Checkout
-        uses: actions/checkout@v2.3.1
+        uses: actions/checkout@v2.3.3
       - name: Use Node.js ${{ matrix.node-version }}
-        uses: actions/setup-node@v2.1.1
+        uses: actions/setup-node@v2.1.2
         with:
           node-version: ${{ matrix.node-version }}
       - name: Install
@@ -24,12 +24,12 @@ jobs:
       - name: Test
         run: npm run coverage -- --no-cache --runInBand
       - name: Report coverage
-        uses: codecov/codecov-action@v1.0.12
+        uses: codecov/codecov-action@v1.0.14
       - name: Build docs
         run: npm --prefix website run build
         if: github.event_name == 'push' && github.ref == 'refs/heads/master' && matrix.node-version == '14.x'
       - name: Deploy docs
-        uses: peaceiris/actions-gh-pages@v3.6.4
+        uses: peaceiris/actions-gh-pages@v3.7.3
         with:
           allow_empty_commit: true
           cname: uniforms.tools
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fb1c503e..0d541e67 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+## Next
+
+- **Fixed:** Regression of accepting non-string labels in `useField` and `connectField`. [\#816](https://github.com/vazco/uniforms/issues/816)
+
 ## [v3.0.0-rc.5](https://github.com/vazco/uniforms/tree/v3.0.0-rc.5) (2020-09-30)
 
 - **Added:** New `disableItem` prop in `SelectField`. [\#736](https://github.com/vazco/uniforms/issues/736)
diff --git a/docs/api-fields.md b/docs/api-fields.md
index 041945e3..94bd8ba1 100644
--- a/docs/api-fields.md
+++ b/docs/api-fields.md
@@ -44,8 +44,6 @@ All additional props will be passed to the result field component.
 | `component` | Field according to the `AutoField` algorithm |       Component to render       |
 |   `name`    |                      -                       | Name of the field in the schema |
 
-**Note:** All `BaseField` props are also accepted.
-
 ##### Props usage:
 
 ```js
@@ -108,8 +106,6 @@ A checkbox.
 |  `wrapClassName`  |                                             Field and sourroundings wrap className. _Some description would be great, huh?_                                             |         bootstrap3, bootstrap4         |
 |   `wrapperCol`    |                                                     Field layout. The layout for input controls. Same as labelCol.                                                      |                  antd                  |
 
-**Note:** All `BaseField` props are also accepted.
-
 ##### Props usage:
 
 ```js
@@ -167,8 +163,6 @@ import { BoolField } from 'uniforms-unstyled';
 |  `wrapClassName`  | Field and sourroundings wrap className. In SemanticUI theme, this class name is used on ui input wrapper, so you can pass classes like small, huge, inverted, transparent etc. |    bootstrap3, bootstrap4, semantic    |
 |   `wrapperCol`    |                                                         Field layout. The layout for input controls. Same as labelCol.                                                         |                  antd                  |
 
-**Note:** All `BaseField` props are also accepted.
-
 ##### Props usage:
 
 ```js
@@ -265,7 +259,6 @@ import { HiddenField } from 'uniforms-unstyled';
 | :-------: | :----------------------------------: | :--------------------: |
 | `addIcon` | Icon. By default, glyphicon is used. | bootstrap3, bootstrap4 |
 
-**Note:** All `BaseField` props are also accepted.<br />
 **Note:** This is one of _internal_ components of `ListField`.
 
 ##### Props usage:
@@ -284,7 +277,6 @@ import { ListAddField } from 'uniforms-unstyled';
 | :----------: | :----------------------------------: | :--------------------: |
 | `removeIcon` | Icon. By default, glyphicon is used. | bootstrap3, bootstrap4 |
 
-**Note:** All `BaseField` props are also accepted.<br />
 **Note:** This is one of _internal_ components of `ListField`.
 
 ##### Props usage:
@@ -307,8 +299,6 @@ import { ListDelField } from 'uniforms-unstyled';
 |   `removeIcon`    | glyphicon |                         Icon. It's passed to the ListDelField.                         |      bootstrap3, bootstrap4      |
 | `showInlineError` |           |              Field inline error. _Some description would be great, huh?_               | bootstrap3, bootstrap4, semantic |
 
-**Note:** All `BaseField` props are also accepted.
-
 ##### Props usage:
 
 ```js
@@ -333,7 +323,6 @@ import { ListField } from 'uniforms-unstyled';
 | :----------: | :-------: | :------------------------------------: | :--------------------: |
 | `removeIcon` | glyphicon | Icon. It's passed to the ListDelField. | bootstrap3, bootstrap4 |
 
-**Note:** All `BaseField` props are also accepted.<br />
 **Note:** This is one of _internal_ components of `ListField`.
 
 ##### Props usage:
@@ -369,8 +358,6 @@ A textarea.
 |   `wrapperCol`    |                                                     Field layout. The layout for input controls. Same as labelCol.                                                      |                  antd                  |
 |  `wrapClassName`  |                                             Field and sourroundings wrap className. _Some description would be great, huh?_                                             |         bootstrap3, bootstrap4         |
 
-**Note:** All `BaseField` props are also accepted.
-
 ##### Props usage:
 
 ```js
@@ -410,8 +397,6 @@ import { LongTextField } from 'uniforms-unstyled';
 |     `grouped`     | Add / remove "grouped" class from the field. In Semantic, fields can be grouped using this class. By default, this class is added to the NestField. |                semantic                |
 | `showInlineError` |                                             Field inline error. _Some description would be great, huh?_                                             | antd, bootstrap3, bootstrap4, semantic |
 
-**Note:** All `BaseField` props are also accepted.
-
 ##### Props usage:
 
 ```js
@@ -449,8 +434,6 @@ A numeric input field.
 |   `wrapperCol`    |                                                     Field layout. The layout for input controls. Same as labelCol.                                                      |                  antd                  |
 |  `wrapClassName`  |                                             Field and sourroundings wrap className. _Some description would be great, huh?_                                             |         bootstrap3, bootstrap4         |
 
-**Note:** All `BaseField` props are also accepted.
-
 ##### Props usage:
 
 ```js
@@ -499,8 +482,6 @@ import { NumField } from 'uniforms-unstyled';
 |    `transform`    |                        Label transform. Allows to transform the each value into a human-readable label                        |                  All                   |
 |   `wrapperCol`    |                                Field layout. The layout for input controls. Same as labelCol.                                 |                  antd                  |
 
-**Note:** All `BaseField` props are also accepted.
-
 ##### Props usage:
 
 ```js
@@ -547,8 +528,6 @@ import { RadioField } from 'uniforms-unstyled';
 |  `wrapClassName`  |                                             Field and surroundings wrap className. _Some description would be great, huh?_                                              |         bootstrap3, bootstrap4         |
 | `textFieldProps`  |                                          Props injected directly to `TextField` ( valid only for non-checkbox `SelectField` ).                                          |                material                |
 
-**Note:** All `BaseField` props are also accepted.
-
 ##### Props usage:
 
 ```js
@@ -616,8 +595,6 @@ import { SubmitField } from 'uniforms-unstyled';
 |   `wrapperCol`    |                                                     Field layout. The layout for input controls. Same as labelCol.                                                      |                  antd                  |
 |  `wrapClassName`  |                                             Field and sourroundings wrap className. _Some description would be great, huh?_                                             |         bootstrap3, bootstrap4         |
 
-**Note:** All `BaseField` props are also accepted.
-
 ##### Props usage:
 
 ```js
diff --git a/docs/api-forms.md b/docs/api-forms.md
index 935a106f..088e7e51 100644
--- a/docs/api-forms.md
+++ b/docs/api-forms.md
@@ -161,8 +161,6 @@ However, `BaseForm` is not self-managed, so you won't be able to type anything u
 |      `model`      | Form model. An object with `{field: value}` structure. It doesn't matter if it has a prototype or not, but keep in mind that in `onSubmit` or in `onChangeModel` you'll receive a plain object. If you treat form as an input, then this is a value. |
 | `modelTransform`  |                                                  Model transform. Function transforming one model into another. It's used in a few situations (modes) described below. Do not mutate a given model!                                                  |
 |    `onChange`     |                                              Field change action. It receives two arguments: key and value, where the key is a dot-separated path to the changed field and value is a requested value.                                               |
-| `onSubmitFailure` |                                                                      Submit failure action. If `onSubmit` returns a Promise, then this will be attached to its `.catch` chain.                                                                       |
-| `onSubmitSuccess` |                                                                       Submit success action. If `onSubmit` returns a Promise, then this will be attached to its `.then` chain.                                                                       |
 |    `onSubmit`     |                                                                  Submit action. When the form is submitted manually or by an HTML5 event, then it's called with the current model.                                                                   |
 |   `placeholder`   |                                                              Default placeholder prop for all fields. By default it's false - set it to true to enable placeholders for the whole form.                                                              |
 |     `schema`      |                                                                               Form schema. It's used for form generation in QuickForm and validation in ValidatedForm.                                                                               |
@@ -204,8 +202,6 @@ import { BaseForm } from 'uniforms'; // Or from the theme package.
     return model;
   }}
   onChange={(key, value) => console.log(key, value)}
-  onSubmitFailure={() => alert('Promise rejected!')}
-  onSubmitSuccess={() => alert('Promise resolved!')}
   onSubmit={model => db.saveThatReturnsPromiseOrNothing(model)}
   placeholder={false}
   schema={myFormSchema}
@@ -223,8 +219,7 @@ import { BaseForm } from 'uniforms'; // Or from the theme package.
     // Submit form.
     //   It's a programmatic equivalent of a submit event. Returns a promise,
     //   which will either resolve with submitted form or reject with
-    //   validation error in ValidatedForm. You can also use onSubmitFailure
-    //   and onSubmitSuccess instead of doing form.submit().then().
+    //   validation error in ValidatedForm.
     form.submit();
   }}
 />;
@@ -306,7 +301,7 @@ Resets a form. It will also reset changed state, model state (only in AutoForm),
 
 #### `submit()`
 
-Submits a form. It's a programmatic equivalent of a submit event. Returns a promise, which will either resolve with a submitted model or reject with validation error in ValidatedForm. You can also use `onSubmitFailure` and `onSubmitSuccess` instead of doing `form.submit().then()`.
+Submits a form. It's a programmatic equivalent of a submit event. Returns a promise, which will either resolve with a submitted model or reject with validation error in ValidatedForm.
 
 #### `validate()`
 
@@ -398,21 +393,6 @@ If you need to transform model before it will be validated, submitted or passed
 />
 ```
 
-### Post-submit handling
-
-It's a good UX practice to tell your users that something failed or succeed. To make it simpler, there are `onSubmitFailure` and `onSubmitSuccess` props.
-
-**Example:**
-
-```js
-<AutoForm
-  schema={schema}
-  onSubmit={doc => db.saveThatReturnsPromise(doc)}
-  onSubmitSuccess={() => alert('Promise resolved!')}
-  onSubmitFailure={() => alert('Promise rejected!')}
-/>
-```
-
 ### Validation options and modes
 
 Any form can be validated in one those three styles:
diff --git a/packages/uniforms-bootstrap3/src/TextField.tsx b/packages/uniforms-bootstrap3/src/TextField.tsx
index 06fd1d98..880f1f44 100644
--- a/packages/uniforms-bootstrap3/src/TextField.tsx
+++ b/packages/uniforms-bootstrap3/src/TextField.tsx
@@ -13,11 +13,12 @@ export type TextFieldProps = HTMLFieldProps<
 
 function Text(props: TextFieldProps) {
   return wrapField(
-    { feedbackable: true, ...omit(props, ['onChange']) },
+    { feedbackable: true, ...omit(props, ['onChange', 'autoComplete']) },
     <input
       className={classnames(props.inputClassName, 'form-control', {
         'form-control-danger': props.error,
       })}
+      autoComplete={props.autoComplete}
       disabled={props.disabled}
       id={props.id}
       name={props.name}
diff --git a/packages/uniforms-bootstrap4/src/TextField.tsx b/packages/uniforms-bootstrap4/src/TextField.tsx
index 2f83e2ca..acbb1aa0 100644
--- a/packages/uniforms-bootstrap4/src/TextField.tsx
+++ b/packages/uniforms-bootstrap4/src/TextField.tsx
@@ -13,11 +13,12 @@ export type TextFieldProps = HTMLFieldProps<
 
 function Text(props: TextFieldProps) {
   return wrapField(
-    omit(props, ['value', 'onChange']),
+    omit(props, ['value', 'onChange', 'autoComplete']),
     <input
       className={classnames(props.inputClassName, 'form-control', {
         'is-invalid': props.error,
       })}
+      autoComplete={props.autoComplete}
       disabled={props.disabled}
       id={props.id}
       name={props.name}
diff --git a/packages/uniforms-semantic/src/TextField.tsx b/packages/uniforms-semantic/src/TextField.tsx
index 1f40b00f..22a3f3af 100644
--- a/packages/uniforms-semantic/src/TextField.tsx
+++ b/packages/uniforms-semantic/src/TextField.tsx
@@ -15,6 +15,7 @@ export type TextFieldProps = HTMLFieldProps<
 >;
 
 function Text({
+  autoComplete,
   className,
   disabled,
   error,
@@ -51,6 +52,7 @@ function Text({
         )}
       >
         <input
+          autoComplete={autoComplete}
           disabled={disabled}
           id={id}
           name={name}
diff --git a/packages/uniforms-unstyled/src/TextField.tsx b/packages/uniforms-unstyled/src/TextField.tsx
index 1e8cfd45..dcd8bcb4 100644
--- a/packages/uniforms-unstyled/src/TextField.tsx
+++ b/packages/uniforms-unstyled/src/TextField.tsx
@@ -8,6 +8,7 @@ export type TextFieldProps = HTMLFieldProps<
 >;
 
 function Text({
+  autoComplete,
   disabled,
   id,
   inputRef,
@@ -24,6 +25,7 @@ function Text({
       {label && <label htmlFor={id}>{label}</label>}
 
       <input
+        autoComplete={autoComplete}
         disabled={disabled}
         id={id}
         name={name}
diff --git a/packages/uniforms/src/BaseForm.tsx b/packages/uniforms/src/BaseForm.tsx
index c28ca7cc..4e9c6c16 100644
--- a/packages/uniforms/src/BaseForm.tsx
+++ b/packages/uniforms/src/BaseForm.tsx
@@ -165,8 +165,6 @@ export class BaseForm<
       'modelTransform',
       'onChange',
       'onSubmit',
-      'onSubmitFailure',
-      'onSubmitSuccess',
       'placeholder',
       'schema',
       'showInlineError',
diff --git a/packages/uniforms/src/useField.tsx b/packages/uniforms/src/useField.tsx
index 8a4ce49a..96d5710f 100644
--- a/packages/uniforms/src/useField.tsx
+++ b/packages/uniforms/src/useField.tsx
@@ -1,30 +1,31 @@
 import get from 'lodash/get';
 import mapValues from 'lodash/mapValues';
-import { useCallback, useEffect, useMemo } from 'react';
+import { ReactNode, useCallback, useEffect, useMemo } from 'react';
 
 import { joinName } from './joinName';
 import { GuaranteedProps } from './types';
 import { useForm } from './useForm';
 
 function propagate(
-  prop: unknown,
-  schema: unknown,
+  prop: ReactNode,
+  schema: ReactNode,
   state: boolean,
-  fallback: string,
-): [string, string] {
-  const schemaDisabled = schema === false || schema === '';
-  const schemaValue =
-    typeof schema === 'string' ? schema : schemaDisabled ? '' : fallback;
-  const resultValue =
-    typeof prop === 'string'
-      ? prop
-      : prop === null ||
-        prop === false ||
-        (prop === undefined && !state) ||
-        schemaDisabled
+  fallback: ReactNode,
+): [ReactNode, ReactNode] {
+  const forcedFallbackInProp = prop === true || prop === undefined;
+  const forcedFallbackInSchema = schema === true || schema === undefined;
+
+  const schemaValue = forcedFallbackInSchema ? fallback : schema;
+  const value =
+    prop === '' ||
+    prop === false ||
+    (forcedFallbackInProp && (forcedFallbackInSchema || !state))
       ? ''
-      : schemaValue;
-  return [resultValue, schemaValue];
+      : forcedFallbackInProp
+      ? schemaValue
+      : prop;
+
+  return [value, schemaValue];
 }
 
 export function useField<
@@ -112,7 +113,8 @@ export function useField<
     ...props,
     label,
     name,
-    placeholder,
+    // TODO: Should we assert `typeof placeholder === 'string'`?
+    placeholder: placeholder as string,
   };
 
   return [fieldProps, context] as [typeof fieldProps, typeof context];


Test patch:
diff --git a/packages/uniforms-antd/__tests__/TextField.tsx b/packages/uniforms-antd/__tests__/TextField.tsx
index 2baac5aa..7f902bc2 100644
--- a/packages/uniforms-antd/__tests__/TextField.tsx
+++ b/packages/uniforms-antd/__tests__/TextField.tsx
@@ -158,3 +158,10 @@ test('<TextField> - renders a input with correct type prop', () => {
 
   expect(wrapper.find(Input).prop('type')).toBe('password');
 });
+
+test('<TextField> - renders a input with autocomplete turned off', () => {
+  const element = <TextField name="x" autoComplete="off" />;
+  const wrapper = mount(element, createContext({ x: { type: String } }));
+
+  expect(wrapper.find(Input).prop('autoComplete')).toBe('off');
+});
diff --git a/packages/uniforms-bootstrap3/__tests__/TextField.tsx b/packages/uniforms-bootstrap3/__tests__/TextField.tsx
index 0650daf8..24f576fe 100644
--- a/packages/uniforms-bootstrap3/__tests__/TextField.tsx
+++ b/packages/uniforms-bootstrap3/__tests__/TextField.tsx
@@ -153,3 +153,11 @@ test('<TextField> - renders a wrapper with unknown props', () => {
   expect(wrapper.find('div').at(0).prop('data-y')).toBe('y');
   expect(wrapper.find('div').at(0).prop('data-z')).toBe('z');
 });
+
+test('<TextField> - renders a input with autocomplete turned off', () => {
+  const element = <TextField name="x" autoComplete="off" />;
+  const wrapper = mount(element, createContext({ x: { type: String } }));
+
+  expect(wrapper.find('div').at(0).prop('autoComplete')).toBeFalsy();
+  expect(wrapper.find('input').prop('autoComplete')).toBe('off');
+});
diff --git a/packages/uniforms-bootstrap4/__tests__/TextField.tsx b/packages/uniforms-bootstrap4/__tests__/TextField.tsx
index e09bde36..a8a7c1a3 100644
--- a/packages/uniforms-bootstrap4/__tests__/TextField.tsx
+++ b/packages/uniforms-bootstrap4/__tests__/TextField.tsx
@@ -153,3 +153,11 @@ test('<TextField> - renders a wrapper with unknown props', () => {
   expect(wrapper.find('div').at(0).prop('data-y')).toBe('y');
   expect(wrapper.find('div').at(0).prop('data-z')).toBe('z');
 });
+
+test('<TextField> - renders a input with autocomplete turned off', () => {
+  const element = <TextField name="x" autoComplete="off" />;
+  const wrapper = mount(element, createContext({ x: { type: String } }));
+
+  expect(wrapper.find('div').at(0).prop('autoComplete')).toBeFalsy();
+  expect(wrapper.find('input').prop('autoComplete')).toBe('off');
+});
diff --git a/packages/uniforms-material/__tests__/TextField.tsx b/packages/uniforms-material/__tests__/TextField.tsx
index 7cebf011..738ad13f 100644
--- a/packages/uniforms-material/__tests__/TextField.tsx
+++ b/packages/uniforms-material/__tests__/TextField.tsx
@@ -159,3 +159,11 @@ test('<TextField> - renders a TextField with correct error text (showInlineError
 
   expect(wrapper.find(TextFieldMaterial).prop('helperText')).toBeUndefined();
 });
+
+test('<TextField> - renders a input with autocomplete off', () => {
+  const element = <TextField name="x" autoComplete="off" />;
+  const wrapper = mount(element, createContext({ x: { type: String } }));
+
+  expect(wrapper.find(TextFieldMaterial)).toHaveLength(1);
+  expect(wrapper.find(TextFieldMaterial).prop('autoComplete')).toBe('off');
+});
diff --git a/packages/uniforms-semantic/__tests__/TextField.tsx b/packages/uniforms-semantic/__tests__/TextField.tsx
index b1dc3e02..8559727c 100644
--- a/packages/uniforms-semantic/__tests__/TextField.tsx
+++ b/packages/uniforms-semantic/__tests__/TextField.tsx
@@ -199,3 +199,10 @@ test('<TextField> - renders with a custom wrapClassName', () => {
 
   expect(wrapper.find('.ui.input.test-class-name')).toHaveLength(1);
 });
+
+test('<TextField> - renders a input with autocomplete turned off', () => {
+  const element = <TextField name="x" autoComplete="off" />;
+  const wrapper = mount(element, createContext({ x: { type: String } }));
+
+  expect(wrapper.find('input').prop('autoComplete')).toBe('off');
+});
diff --git a/packages/uniforms-unstyled/__tests__/TextField.tsx b/packages/uniforms-unstyled/__tests__/TextField.tsx
index 27989b24..808429e7 100644
--- a/packages/uniforms-unstyled/__tests__/TextField.tsx
+++ b/packages/uniforms-unstyled/__tests__/TextField.tsx
@@ -153,3 +153,11 @@ test('<TextField> - renders a wrapper with unknown props', () => {
   expect(wrapper.find('div').at(0).prop('data-y')).toBe('y');
   expect(wrapper.find('div').at(0).prop('data-z')).toBe('z');
 });
+
+test('<TextField> - renders an input with autocomplete turned off', () => {
+  const element = <TextField name="x" autoComplete="off" />;
+  const wrapper = mount(element, createContext({ x: { type: String } }));
+
+  expect(wrapper.find('input')).toHaveLength(1);
+  expect(wrapper.find('input').prop('autoComplete')).toBe('off');
+});
diff --git a/packages/uniforms/__tests__/connectField.tsx b/packages/uniforms/__tests__/connectField.tsx
index aafabd74..beea5dd4 100644
--- a/packages/uniforms/__tests__/connectField.tsx
+++ b/packages/uniforms/__tests__/connectField.tsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { ReactNode } from 'react';
 import { SimpleSchema } from 'simpl-schema';
 import { Context, connectField, randomIds } from 'uniforms';
 import { SimpleSchemaBridge } from 'uniforms-bridge-simple-schema';
@@ -125,4 +125,61 @@ describe('connectField', () => {
       expect(onChange).toBeCalledWith('field', 'initialValueExample');
     });
   });
+
+  // TODO: Write tests for `placeholder`.
+  describe('when rendered with label', () => {
+    const labelA = <span style={{ color: 'red' }}>Error</span>;
+    const labelB = <span style={{ color: 'green' }}>OK</span>;
+
+    it.each([
+      ['Props', '', false, 'Props'],
+      ['Props', '', true, 'Props'],
+      ['Props', 'Schema', false, 'Props'],
+      ['Props', 'Schema', true, 'Props'],
+      ['Props', labelB, false, 'Props'],
+      ['Props', labelB, true, 'Props'],
+      [false, '', false, ''],
+      [false, '', true, ''],
+      [false, 'Schema', false, ''],
+      [false, 'Schema', true, ''],
+      [false, labelB, false, ''],
+      [false, labelB, true, ''],
+      [labelA, '', false, labelA],
+      [labelA, '', true, labelA],
+      [labelA, 'Schema', false, labelA],
+      [labelA, 'Schema', true, labelA],
+      [labelA, labelB, false, labelA],
+      [labelA, labelB, true, labelA],
+      [true, '', false, ''],
+      [true, '', true, ''],
+      [true, 'Schema', false, 'Schema'],
+      [true, 'Schema', true, 'Schema'],
+      [true, labelB, false, labelB],
+      [true, labelB, true, labelB],
+      [undefined, '', false, ''],
+      [undefined, '', true, ''],
+      [undefined, 'Schema', false, ''],
+      [undefined, 'Schema', true, 'Schema'],
+      [undefined, labelB, false, ''],
+      [undefined, labelB, true, labelB],
+    ] as [ReactNode, ReactNode, boolean, ReactNode][])(
+      'resolves it correctly (%#)',
+      (prop, schema, state, result) => {
+        const context: typeof reactContext = {
+          context: {
+            ...reactContext.context,
+            state: { ...reactContext.context.state, label: state },
+          },
+        };
+
+        jest
+          .spyOn(context.context.schema, 'getProps')
+          .mockReturnValueOnce({ label: schema });
+
+        const Field = connectField(Test);
+        const wrapper = mount(<Field name="field" label={prop} />, context);
+        expect(wrapper.find(Test).prop('label')).toBe(result);
+      },
+    );
+  });
 });