Skip to content

Commit

Permalink
test: add unit test coverage for adaptive-form (#3371)
Browse files Browse the repository at this point in the history
* ignore default schemas

* remove unused code

* add test for plugin config hook

* refactor test-utils exports

* convert to typescript project
* export react test utils from lib/react
* export hooks test utils from lib/hooks

* add test coverage for utils

* do not throw error if all fields are ordered

* fix outdated types for react testing library

* add unit test for error message

* add unit tests for field label

* ignore link in coverage

* add unit test for FormRow

* add unit tests for schema field

* add unit tests for ObjectItem

* add snippet for react component test scaffold

* add OpenObjectField unit tests

* revert not exporting react test utils

* add unit tests for OneOfField

* add unit tests for StringField

* add unit tests for UnsupportedField

* add unit tests for BooleanField

* remove coverage ignore statement

* allow non-null assertion in test

* add unit test for NumberField

* add unit test for SelectField

* add unit tests for ArrayField

* add unit test for ArrayFieldItem

* render string field with a transparent border

* add unit test for IntentField

remove recognizerType function, use isSelected instead

* add unit test for JsonField

* do not include wildcard in ordered properties

* add unit test for ObjectArrayField

* add unit test for ObjectField

* add unit test for RecognizerField

* add unit test for RegexIntentField

* fix linting error

* update hooks imports
  • Loading branch information
a-b-r-o-w-n authored Jun 10, 2020
1 parent 50bfc96 commit b37a88b
Show file tree
Hide file tree
Showing 84 changed files with 2,079 additions and 433 deletions.
26 changes: 26 additions & 0 deletions .vscode/snippets.json.code-snippets
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
// Place your BotFramework-Composer workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and
// description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope
// is left empty or omitted, the snippet gets applied to all languages. The prefix is what is
// used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders.
// Placeholders with the same ids are connected.
"React component test scaffolding": {
"prefix": "rct",
"body": [
"import React from 'react';",
"import { render } from '@bfc/test-utils';",
"import assign from 'lodash/assign';\n",
"import { $1 } from '$2';\n",
"const defaultProps = {\n $3\n};\n",
"function renderSubject(overrides = {}) {",
" const props = assign({}, defaultProps, overrides);",
" return render(<$1 {...props} />);",
"}\n",
"describe('<$1 />', () => {",
" it.todo('$0');",
"});\n"
],
"description": "React component test scaffolding"
}
}
1 change: 1 addition & 0 deletions Composer/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ module.exports = {
},
rules: {
'@typescript-eslint/ban-ts-ignore': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-object-literal-type-assertion': 'off',
'@typescript-eslint/unbound-method': 'off',

Expand Down
3 changes: 2 additions & 1 deletion Composer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
"scripts": {
"build": "node scripts/begin.js && yarn build:prod && yarn build:plugins",
"build:prod": "yarn build:dev && yarn build:server && yarn build:client && yarn build:electron",
"build:dev": "yarn build:lib && yarn build:tools && yarn build:extensions && yarn build:plugins",
"build:dev": "yarn build:test && yarn build:lib && yarn build:tools && yarn build:extensions && yarn build:plugins",
"build:test": "yarn workspace @bfc/test-utils build",
"build:lib": "yarn workspace @bfc/libs build:all",
"build:electron": "yarn workspace @bfc/electron-server build",
"build:extensions": "wsrun -lt -p @bfc/plugin-loader @bfc/extension @bfc/adaptive-form @bfc/adaptive-flow @bfc/ui-plugin-* -c build",
Expand Down
32 changes: 15 additions & 17 deletions Composer/packages/client/__tests__/hooks/useForm.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { hooks } from '@bfc/test-utils';
import { renderHook, act } from '@bfc/test-utils/lib/hooks';

import { useForm, FieldConfig } from '../../src/hooks/useForm';

Expand All @@ -11,8 +11,6 @@ interface TestFormData {
asyncValidationField: string;
}

afterEach(hooks.cleanup);

describe('useForm', () => {
const custValidate = jest.fn();
const asyncValidate = jest.fn();
Expand All @@ -34,7 +32,7 @@ describe('useForm', () => {

describe('formData', () => {
it('applies default values', () => {
const { result } = hooks.renderHook(() => useForm(fields));
const { result } = renderHook(() => useForm(fields));

expect(result.current.formData).toEqual({
requiredField: 'foo',
Expand All @@ -44,9 +42,9 @@ describe('useForm', () => {
});

it('can update single fields', async () => {
const { result, waitForNextUpdate } = hooks.renderHook(() => useForm(fields));
const { result, waitForNextUpdate } = renderHook(() => useForm(fields));

await hooks.act(async () => {
await act(async () => {
result.current.updateField('requiredField', 'new value');
await waitForNextUpdate();
});
Expand All @@ -55,9 +53,9 @@ describe('useForm', () => {
});

it('can update the whole object', async () => {
const { result, waitForNextUpdate } = hooks.renderHook(() => useForm(fields));
const { result, waitForNextUpdate } = renderHook(() => useForm(fields));

await hooks.act(async () => {
await act(async () => {
result.current.updateForm({
requiredField: 'new',
customValidationField: 'form',
Expand All @@ -78,7 +76,7 @@ describe('useForm', () => {
it('can validate when mounting', async () => {
custValidate.mockReturnValue('custom');
asyncValidate.mockResolvedValue('async');
const { result, waitForNextUpdate } = hooks.renderHook(() => useForm(fields, { validateOnMount: true }));
const { result, waitForNextUpdate } = renderHook(() => useForm(fields, { validateOnMount: true }));
await waitForNextUpdate();

expect(result.current.formErrors).toMatchObject({
Expand All @@ -88,9 +86,9 @@ describe('useForm', () => {
});

it('validates required fields', async () => {
const { result, waitForNextUpdate } = hooks.renderHook(() => useForm(fields));
const { result, waitForNextUpdate } = renderHook(() => useForm(fields));

await hooks.act(async () => {
await act(async () => {
result.current.updateField('requiredField', '');
await waitForNextUpdate();
});
Expand All @@ -100,9 +98,9 @@ describe('useForm', () => {

it('validates using a custom validator', async () => {
custValidate.mockReturnValue('my custom validation');
const { result, waitForNextUpdate } = hooks.renderHook(() => useForm(fields));
const { result, waitForNextUpdate } = renderHook(() => useForm(fields));

await hooks.act(async () => {
await act(async () => {
result.current.updateField('customValidationField', 'foo');
await waitForNextUpdate();
});
Expand All @@ -112,9 +110,9 @@ describe('useForm', () => {

it('validates using an asyn validator', async () => {
asyncValidate.mockResolvedValue('my async validation');
const { result, waitForNextUpdate } = hooks.renderHook(() => useForm(fields));
const { result, waitForNextUpdate } = renderHook(() => useForm(fields));

await hooks.act(async () => {
await act(async () => {
result.current.updateField('asyncValidationField', 'foo');
await waitForNextUpdate();
});
Expand All @@ -125,11 +123,11 @@ describe('useForm', () => {

describe('hasErrors', () => {
it('returns true when there are errors', async () => {
const { result, waitForNextUpdate } = hooks.renderHook(() => useForm(fields));
const { result, waitForNextUpdate } = renderHook(() => useForm(fields));

expect(result.current.hasErrors).toBe(false);

await hooks.act(async () => {
await act(async () => {
result.current.updateField('requiredField', '');
await waitForNextUpdate();
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { hooks } from '@bfc/test-utils';
import { renderHook } from '@bfc/test-utils/lib/hooks';

import { useStoreContext } from '../../../src/hooks/useStoreContext';
import useNotifications from '../../../src/pages/notifications/useNotifications';

const { renderHook } = hooks;

jest.mock('../../../src/hooks/useStoreContext', () => ({
useStoreContext: jest.fn(),
}));
Expand Down
4 changes: 1 addition & 3 deletions Composer/packages/client/__tests__/shell/lgApi.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { hooks } from '@bfc/test-utils';
import { renderHook } from '@bfc/test-utils/lib/hooks';

import { useLgApi } from '../../src/shell/lgApi';
import { useStoreContext } from '../../src/hooks/useStoreContext';
Expand All @@ -10,8 +10,6 @@ jest.mock('../../src/hooks/useStoreContext', () => ({
useStoreContext: jest.fn(),
}));

const { renderHook } = hooks;

const state = {
lgFiles: [
{
Expand Down
4 changes: 1 addition & 3 deletions Composer/packages/client/__tests__/shell/luApi.test.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { hooks } from '@bfc/test-utils';
import { renderHook } from '@bfc/test-utils/lib/hooks';

import { useLuApi } from '../../src/shell/luApi';
import { useStoreContext } from '../../src/hooks/useStoreContext';

const { renderHook } = hooks;

jest.mock('../../src/hooks/useStoreContext', () => ({
useStoreContext: jest.fn(),
}));
Expand Down
1 change: 0 additions & 1 deletion Composer/packages/extensions/adaptive-flow/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@
},
"devDependencies": {
"@bfc/test-utils": "*",
"@testing-library/react": "^10.0.2",
"@types/lodash": "^4.14.146",
"@types/react": "16.9.23",
"format-message": "^6.2.3",
Expand Down
4 changes: 3 additions & 1 deletion Composer/packages/extensions/adaptive-form/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@

const { createConfig } = require('@bfc/test-utils');

module.exports = createConfig('adaptive-form', 'react');
module.exports = createConfig('adaptive-form', 'react', {
coveragePathIgnorePatterns: ['defaultRecognizers.ts', 'defaultRoleSchema.ts', 'defaultUiSchema.ts'],
});
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const ErrorMessage: React.FC<ErrorMessageProps> = (props) => {
>
{[label, error].filter(Boolean).join(' ')}
{helpLink && (
<Link key="a" href={helpLink} rel="noopener noreferrer" target="_blank">
<Link key="a" data-testid="ErrorMessageHelpLink" href={helpLink} rel="noopener noreferrer" target="_blank">
{formatMessage('Refer to the syntax documentation here.')}
</Link>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ const DescriptionCallout: React.FC<DescriptionCalloutProps> = function Descripti
),
}}
>
<div css={focusBorder} tabIndex={0}>
<div css={focusBorder} data-testid="FieldLabelDescriptionIcon" tabIndex={0}>
<Icon
aria-label={title + '; ' + description}
iconName={'Unknown'}
Expand Down
110 changes: 44 additions & 66 deletions Composer/packages/extensions/adaptive-form/src/components/FormRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,52 @@ import { FieldProps, UIOptions } from '@bfc/extension';

import { resolvePropSchema } from '../utils';

import SchemaField from './SchemaField';
import { SchemaField } from './SchemaField';

interface FormRowProps extends Omit<FieldProps, 'onChange'> {
export interface FormRowProps extends Omit<FieldProps, 'onChange'> {
onChange: (field: string) => (data: any) => void;
row: string | [string, string];
}

export const _getRowProps = (rowProps: FormRowProps, field: string) => {
const {
id,
depth,
schema,
definitions,
value,
uiOptions,
transparentBorder,
className,
label,
rawErrors,
onBlur,
onFocus,
onChange,
} = rowProps;

const { required = [] } = schema;
const fieldSchema = resolvePropSchema(schema, field, definitions);

return {
id: `${id}.${field}`,
schema: fieldSchema ?? {},
label: (label === false ? false : undefined) as false | undefined,
name: field,
rawErrors: rawErrors?.[field],
required: required.includes(field),
uiOptions: (uiOptions.properties?.[field] as UIOptions) ?? {},
value: value && value[field],
onChange: onChange(field),
depth,
definitions,
transparentBorder,
className,
onBlur,
onFocus,
};
};

const formRow = {
row: css`
display: flex;
Expand All @@ -40,79 +79,18 @@ const formRow = {
};

const FormRow: React.FC<FormRowProps> = (props) => {
const {
id,
depth,
schema,
row,
definitions,
value,
uiOptions,
transparentBorder,
className,
label,
rawErrors,
onBlur,
onFocus,
onChange,
} = props;

const { required = [] } = schema;
const { id, row } = props;

if (Array.isArray(row)) {
return (
<div css={formRow.row}>
{row.map((property) => (
<SchemaField
key={`${id}.${property}`}
className={className}
css={formRow.property}
definitions={definitions}
depth={depth}
id={`${id}.${property}`}
label={label === false ? false : undefined}
name={property}
rawErrors={rawErrors?.[property]}
required={required.includes(property)}
schema={resolvePropSchema(schema, property, definitions) || {}}
transparentBorder={transparentBorder}
uiOptions={(uiOptions.properties?.[property] as UIOptions) ?? {}}
value={value && value[property]}
onBlur={onBlur}
onChange={onChange(property)}
onFocus={onFocus}
/>
<SchemaField key={`${id}.${property}`} css={formRow.property} {..._getRowProps(props, property)} />
))}
</div>
);
}
const propSchema = resolvePropSchema(schema, row, definitions);

if (propSchema) {
return (
<SchemaField
key={`${id}.${row}`}
className={className}
css={formRow.full}
definitions={definitions}
depth={depth}
id={`${id}.${row}`}
label={label === false ? false : undefined}
name={row}
rawErrors={rawErrors?.[row]}
required={required.includes(row)}
schema={propSchema}
transparentBorder={transparentBorder}
uiOptions={(uiOptions.properties?.[row] as UIOptions) ?? {}}
value={value && value[row]}
onBlur={onBlur}
onChange={onChange(row)}
onFocus={onFocus}
/>
);
}

return null;
return <SchemaField key={`${id}.${row}`} css={formRow.full} {..._getRowProps(props, row)} />;
};

export { FormRow };
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
/* istanbul ignore file */

/** @jsx jsx */
import { jsx } from '@emotion/core';
Expand Down
Loading

0 comments on commit b37a88b

Please sign in to comment.