diff --git a/CHANGELOG.md b/CHANGELOG.md index 263f735b65..77182a58bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,9 @@ should change the heading of the (upcoming) version to include a major version b - Fixed issue with default value not being prefilled when object with if/then is nested inside another object, fixing [#4222](https://github.com/rjsf-team/react-jsonschema-form/issues/4222) - Fixed issue with schema array with nested dependent fixed-length, fixing [#3754](https://github.com/rjsf-team/react-jsonschema-form/issues/3754) +## Dev / docs / playground + +- Updated unit tests for `@rjsf/core` to convert them to typescript and jest # 6.1.2 diff --git a/package-lock.json b/package-lock.json index cb2046a10a..41c6300aac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12919,6 +12919,13 @@ "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==", "license": "MIT" }, + "node_modules/@types/html": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@types/html/-/html-1.0.4.tgz", + "integrity": "sha512-Wb1ymSAftCLxhc3D6vS0Ike/0xg7W6c+DQxAkerU6pD7C8CMzTYwvrwnlcrTfsVO/nMelB9KOKIT7+N5lOeQUg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/html-minifier-terser": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", @@ -38659,6 +38666,7 @@ "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", + "@types/html": "^1.0.4", "ajv": "^8.17.1", "atob": "^2.1.2", "chai": "^3.5.0", diff --git a/packages/core/jest.config.json b/packages/core/jest.config.json index bb89ed3107..7569ea7b2a 100644 --- a/packages/core/jest.config.json +++ b/packages/core/jest.config.json @@ -1,7 +1,7 @@ { "verbose": true, "testEnvironment": "jsdom", - "setupFilesAfterEnv": ["./test/setup-jest-env.js", "../../testing/testSetup.ts"], + "setupFilesAfterEnv": ["./test/setup-jest-env.ts", "../../testing/testSetup.ts"], "testMatch": ["**/test/**/*.test.[jt]s?(x)"], "transformIgnorePatterns": ["/node_modules/(?!(@x0k/json-schema-merge|deep-freeze-es6))"] } diff --git a/packages/core/package.json b/packages/core/package.json index 0be1ec961a..37361175c5 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -82,6 +82,7 @@ "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", + "@types/html": "^1.0.4", "ajv": "^8.17.1", "atob": "^2.1.2", "chai": "^3.5.0", diff --git a/packages/core/test/ArrayFieldTemplate.test.jsx b/packages/core/test/ArrayFieldTemplate.test.tsx similarity index 80% rename from packages/core/test/ArrayFieldTemplate.test.jsx rename to packages/core/test/ArrayFieldTemplate.test.tsx index c165bbb769..540094c765 100644 --- a/packages/core/test/ArrayFieldTemplate.test.jsx +++ b/packages/core/test/ArrayFieldTemplate.test.tsx @@ -1,25 +1,14 @@ import { PureComponent } from 'react'; -import { expect } from 'chai'; -import { Simulate } from 'react-dom/test-utils'; -import { getUiOptions } from '@rjsf/utils'; +import { ArrayFieldTemplateProps, ArrayFieldItemTemplateProps, RJSFSchema, getUiOptions } from '@rjsf/utils'; +import { fireEvent } from '@testing-library/react'; -import { createFormComponent, createSandbox } from './test_utils'; +import { createFormComponent } from './testUtils'; -describe('ArrayFieldTemplate', () => { - let sandbox; - - const formData = ['one', 'two', 'three']; - - beforeEach(() => { - sandbox = createSandbox(); - }); - - afterEach(() => { - sandbox.restore(); - }); +const formData = ['one', 'two', 'three']; +describe('ArrayFieldTemplate', () => { describe('Custom ArrayFieldTemplate of string array', () => { - function ArrayFieldTemplate(props) { + function ArrayFieldTemplate(props: ArrayFieldTemplateProps) { const { classNames } = getUiOptions(props.uiSchema); return (
@@ -28,7 +17,7 @@ describe('ArrayFieldTemplate', () => {
); } - function ArrayFieldItemTemplate(props) { + function ArrayFieldItemTemplate(props: ArrayFieldItemTemplateProps) { return (
{props.buttonsProps.hasMoveUp &&
@@ -328,7 +319,13 @@ describe('ArrayFieldTemplate', () => { formData, templates: { ArrayFieldTemplate }, }); - Simulate.click(node.querySelector('.rjsf-array-item-add')); + let data = node.querySelectorAll('.test-data'); + expect(data).toHaveLength(formData.length); + const button = node.querySelector('.rjsf-array-item-add'); + expect(button).toBeInTheDocument(); + fireEvent.click(button!); + data = node.querySelectorAll('.test-data'); + expect(data).toHaveLength(formData.length + 1); }); }); }); diff --git a/packages/core/test/BooleanField.test.jsx b/packages/core/test/BooleanField.test.tsx similarity index 69% rename from packages/core/test/BooleanField.test.jsx rename to packages/core/test/BooleanField.test.tsx index 515094ebec..f4fdc237bf 100644 --- a/packages/core/test/BooleanField.test.jsx +++ b/packages/core/test/BooleanField.test.tsx @@ -1,23 +1,11 @@ -import { expect } from 'chai'; import { fireEvent, act } from '@testing-library/react'; -import sinon from 'sinon'; +import { RJSFSchema, WidgetProps } from '@rjsf/utils'; -import { createFormComponent, createSandbox, getSelectedOptionValue, submitForm } from './test_utils'; +import { createFormComponent, getSelectedOptionValue, submitForm } from './testUtils'; -describe('BooleanField', () => { - let sandbox; - - const CustomWidget = () =>
; - - beforeEach(() => { - sandbox = createSandbox(); - sandbox.stub(console, 'warn'); - }); - - afterEach(() => { - sandbox.restore(); - }); +const CustomWidget = () =>
; +describe('BooleanField', () => { it('should render a boolean field', () => { const { node } = createFormComponent({ schema: { @@ -25,7 +13,7 @@ describe('BooleanField', () => { }, }); - expect(node.querySelectorAll('.rjsf-field input[type=checkbox]')).to.have.length.of(1); + expect(node.querySelectorAll('.rjsf-field input[type=checkbox]')).toHaveLength(1); }); it('should render a boolean field with the expected id', () => { @@ -35,7 +23,7 @@ describe('BooleanField', () => { }, }); - expect(node.querySelector('.rjsf-field input[type=checkbox]').id).eql('root'); + expect(node.querySelector('.rjsf-field input[type=checkbox]')).toHaveAttribute('id', 'root'); }); it('should render a boolean field with a label', () => { @@ -46,7 +34,7 @@ describe('BooleanField', () => { }, }); - expect(node.querySelector('.rjsf-field label span').textContent).eql('foo'); + expect(node.querySelector('.rjsf-field label span')).toHaveTextContent('foo'); }); describe('HTML5 required attribute', () => { @@ -63,7 +51,7 @@ describe('BooleanField', () => { }, }); - expect(node.querySelector('input[type=checkbox]').required).eql(false); + expect(node.querySelector('input[type=checkbox]')).not.toHaveAttribute('required'); }); it('should add a required attribute if the schema uses const with a true value', () => { @@ -79,7 +67,7 @@ describe('BooleanField', () => { }, }); - expect(node.querySelector('input[type=checkbox]').required).eql(true); + expect(node.querySelector('input[type=checkbox]')).toHaveAttribute('required', ''); }); it('should add a required attribute if the schema uses an enum with a single value of true', () => { @@ -95,7 +83,7 @@ describe('BooleanField', () => { }, }); - expect(node.querySelector('input[type=checkbox]').required).eql(true); + expect(node.querySelector('input[type=checkbox]')).toHaveAttribute('required', ''); }); it('should add a required attribute if the schema uses an anyOf with a single value of true', () => { @@ -115,7 +103,7 @@ describe('BooleanField', () => { }, }); - expect(node.querySelector('input[type=checkbox]').required).eql(true); + expect(node.querySelector('input[type=checkbox]')).toHaveAttribute('required', ''); }); it('should add a required attribute if the schema uses a oneOf with a single value of true', () => { @@ -135,7 +123,7 @@ describe('BooleanField', () => { }, }); - expect(node.querySelector('input[type=checkbox]').required).eql(true); + expect(node.querySelector('input[type=checkbox]')).toHaveAttribute('required', ''); }); it('should add a required attribute if the schema uses an allOf with a value of true', () => { @@ -155,7 +143,7 @@ describe('BooleanField', () => { }, }); - expect(node.querySelector('input[type=checkbox]').required).eql(true); + expect(node.querySelector('input[type=checkbox]')).toHaveAttribute('required', ''); }); }); @@ -167,7 +155,7 @@ describe('BooleanField', () => { }, }); - expect(node.querySelectorAll('.rjsf-field label')).to.have.length.of(1); + expect(node.querySelectorAll('.rjsf-field label')).toHaveLength(1); }); it('should render a description', () => { @@ -179,12 +167,12 @@ describe('BooleanField', () => { }); const description = node.querySelector('.field-description'); - expect(description.textContent).eql('my description'); + expect(description).toHaveTextContent('my description'); }); it('should pass uiSchema to custom widget', () => { - const CustomCheckboxWidget = ({ uiSchema }) => { - return
{uiSchema.custom_field_key['ui:options'].test}
; + const CustomCheckboxWidget = ({ uiSchema }: WidgetProps) => { + return
{uiSchema?.custom_field_key['ui:options'].test}
; }; const { node } = createFormComponent({ @@ -205,7 +193,7 @@ describe('BooleanField', () => { }, }); - expect(node.querySelector('#custom-ui-option-value').textContent).to.eql('foo'); + expect(node.querySelector('#custom-ui-option-value')).toHaveTextContent('foo'); }); it('should render the description using provided description field', () => { @@ -222,7 +210,7 @@ describe('BooleanField', () => { }); const description = node.querySelector('.field-description'); - expect(description.textContent).eql('my description overridden'); + expect(description).toHaveTextContent('my description overridden'); }); it('should assign a default value', () => { @@ -233,7 +221,7 @@ describe('BooleanField', () => { }, }); - expect(node.querySelector('.rjsf-field input').checked).eql(true); + expect(node.querySelector('.rjsf-field input')).toHaveAttribute('checked', ''); }); it('formData should default to undefined', () => { @@ -242,9 +230,10 @@ describe('BooleanField', () => { noValidate: true, }); submitForm(node); - sinon.assert.calledWithMatch(onSubmit.lastCall, { - formData: undefined, - }); + expect(onSubmit).toHaveBeenLastCalledWith( + expect.objectContaining({ formData: undefined }), + expect.objectContaining({ type: 'submit' }), + ); }); it('should focus on required radio missing data when focusOnFirstField and shows error', () => { @@ -261,22 +250,22 @@ describe('BooleanField', () => { focusOnFirstError: true, uiSchema: { bool: { 'ui:widget': 'radio' } }, }); - const focusSpys = [sinon.spy(), sinon.spy()]; + const focusSpys = [jest.fn(), jest.fn()]; const inputs = node.querySelectorAll('input[id^=root_bool]'); - expect(inputs.length).eql(2); + expect(inputs).toHaveLength(2); let errorInputs = node.querySelectorAll('.form-group.rjsf-field-error input[id^=root_bool]'); - expect(errorInputs).to.have.length.of(0); + expect(errorInputs).toHaveLength(0); // Since programmatically triggering focus does not call onFocus, change the focus method to a spy - inputs[0].focus = focusSpys[0]; - inputs[1].focus = focusSpys[1]; + (inputs[0] as HTMLInputElement).focus = focusSpys[0]; + (inputs[1] as HTMLInputElement).focus = focusSpys[1]; submitForm(node); - sinon.assert.calledWithMatch(onError.lastCall, { - formData: undefined, - }); - sinon.assert.calledOnce(focusSpys[0]); - sinon.assert.notCalled(focusSpys[1]); + expect(onError).toHaveBeenLastCalledWith([ + expect.objectContaining({ message: "must have required property 'bool'" }), + ]); + expect(focusSpys[0]).toHaveBeenCalled(); + expect(focusSpys[1]).not.toHaveBeenCalled(); errorInputs = node.querySelectorAll('.form-group.rjsf-field-error input[id^=root_bool]'); - expect(errorInputs).to.have.length.of(2); + expect(errorInputs).toHaveLength(2); }); it('should focus on required radio missing data when focusOnFirstField and hides error', () => { @@ -293,22 +282,22 @@ describe('BooleanField', () => { focusOnFirstError: true, uiSchema: { bool: { 'ui:widget': 'radio', 'ui:hideError': true } }, }); - const focusSpys = [sinon.spy(), sinon.spy()]; + const focusSpys = [jest.fn(), jest.fn()]; const inputs = node.querySelectorAll('input[id^=root_bool]'); - expect(inputs.length).eql(2); + expect(inputs).toHaveLength(2); let errorInputs = node.querySelectorAll('.form-group.rjsf-field-error input[id^=root_bool]'); - expect(errorInputs).to.have.length.of(0); + expect(errorInputs).toHaveLength(0); // Since programmatically triggering focus does not call onFocus, change the focus method to a spy - inputs[0].focus = focusSpys[0]; - inputs[1].focus = focusSpys[1]; + (inputs[0] as HTMLInputElement).focus = focusSpys[0]; + (inputs[1] as HTMLInputElement).focus = focusSpys[1]; submitForm(node); - sinon.assert.calledWithMatch(onError.lastCall, { - formData: undefined, - }); - sinon.assert.calledOnce(focusSpys[0]); - sinon.assert.notCalled(focusSpys[1]); + expect(onError).toHaveBeenLastCalledWith([ + expect.objectContaining({ message: "must have required property 'bool'" }), + ]); + expect(focusSpys[0]).toHaveBeenCalled(); + expect(focusSpys[1]).not.toHaveBeenCalled(); errorInputs = node.querySelectorAll('.form-group.rjsf-field-error input[id^=root_bool]'); - expect(errorInputs).to.have.length.of(0); + expect(errorInputs).toHaveLength(0); }); it('should handle a change event', () => { @@ -320,10 +309,10 @@ describe('BooleanField', () => { }); act(() => { - fireEvent.click(node.querySelector('input')); + fireEvent.click(node.querySelector('input')!); }); - sinon.assert.calledWithMatch(onChange.lastCall, { formData: true }, 'root'); + expect(onChange).toHaveBeenLastCalledWith(expect.objectContaining({ formData: true }), 'root'); }); it('should fill field with data', () => { @@ -334,7 +323,7 @@ describe('BooleanField', () => { formData: true, }); - expect(node.querySelector('.rjsf-field input').checked).eql(true); + expect(node.querySelector('.rjsf-field input')).toHaveAttribute('checked', ''); }); it('should render radio widgets with the expected id', () => { @@ -345,7 +334,7 @@ describe('BooleanField', () => { uiSchema: { 'ui:widget': 'radio' }, }); - expect(node.querySelector('.field-radio-group').id).eql('root'); + expect(node.querySelector('.field-radio-group')).toHaveAttribute('id', 'root'); }); it('should have default enum option labels for radio widgets', () => { @@ -357,8 +346,11 @@ describe('BooleanField', () => { uiSchema: { 'ui:widget': 'radio' }, }); - const labels = [].map.call(node.querySelectorAll('.field-radio-group label'), (label) => label.textContent); - expect(labels).eql(['Yes', 'No']); + const labels = [].map.call( + node.querySelectorAll('.field-radio-group label'), + (label: Element) => label.textContent, + ); + expect(labels).toEqual(['Yes', 'No']); }); it('should support enum option ordering for radio widgets', () => { @@ -371,8 +363,11 @@ describe('BooleanField', () => { uiSchema: { 'ui:widget': 'radio' }, }); - const labels = [].map.call(node.querySelectorAll('.field-radio-group label'), (label) => label.textContent); - expect(labels).eql(['No', 'Yes']); + const labels = [].map.call( + node.querySelectorAll('.field-radio-group label'), + (label: Element) => label.textContent, + ); + expect(labels).toEqual(['No', 'Yes']); }); it('should support ui:enumNames for radio widgets', () => { @@ -382,8 +377,11 @@ describe('BooleanField', () => { uiSchema: { 'ui:widget': 'radio', 'ui:enumNames': ['Yes', 'No'] }, }); - const labels = [].map.call(node.querySelectorAll('.field-radio-group label'), (label) => label.textContent); - expect(labels).eql(['Yes', 'No']); + const labels = [].map.call( + node.querySelectorAll('.field-radio-group label'), + (label: Element) => label.textContent, + ); + expect(labels).toEqual(['Yes', 'No']); }); it('should support oneOf titles for radio widgets', () => { @@ -405,8 +403,11 @@ describe('BooleanField', () => { uiSchema: { 'ui:widget': 'radio' }, }); - const labels = [].map.call(node.querySelectorAll('.field-radio-group label'), (label) => label.textContent); - expect(labels).eql(['Yes', 'No']); + const labels = [].map.call( + node.querySelectorAll('.field-radio-group label'), + (label: Element) => label.textContent, + ); + expect(labels).toEqual(['Yes', 'No']); }); it('should support oneOf titles for radio widgets, overrides in uiSchema', () => { @@ -428,8 +429,11 @@ describe('BooleanField', () => { uiSchema: { 'ui:widget': 'radio', oneOf: [{ 'ui:title': 'Si!' }, { 'ui:title': 'No!' }] }, }); - const labels = [].map.call(node.querySelectorAll('.field-radio-group label'), (label) => label.textContent); - expect(labels).eql(['Si!', 'No!']); + const labels = [].map.call( + node.querySelectorAll('.field-radio-group label'), + (label: Element) => label.textContent, + ); + expect(labels).toEqual(['Si!', 'No!']); }); it('should preserve oneOf option ordering for radio widgets', () => { @@ -451,8 +455,11 @@ describe('BooleanField', () => { uiSchema: { 'ui:widget': 'radio' }, }); - const labels = [].map.call(node.querySelectorAll('.field-radio-group label'), (label) => label.textContent); - expect(labels).eql(['No', 'Yes']); + const labels = [].map.call( + node.querySelectorAll('.field-radio-group label'), + (label: Element) => label.textContent, + ); + expect(labels).toEqual(['No', 'Yes']); }); it('should support inline radio widgets', () => { @@ -467,11 +474,11 @@ describe('BooleanField', () => { }, }); - expect(node.querySelectorAll('.radio-inline')).to.have.length.of(2); + expect(node.querySelectorAll('.radio-inline')).toHaveLength(2); }); it('should handle a focus event for radio widgets', () => { - const onFocus = sandbox.spy(); + const onFocus = jest.fn(); const { node } = createFormComponent({ schema: { type: 'boolean', @@ -484,16 +491,16 @@ describe('BooleanField', () => { }); const element = node.querySelector('.field-radio-group'); - fireEvent.focus(node.querySelector('input'), { + fireEvent.focus(node.querySelector('input')!, { target: { value: 1, // use index }, }); - expect(onFocus.calledWith(element.id, false)).to.be.true; + expect(onFocus).toHaveBeenLastCalledWith(element?.id, false); }); it('should handle a blur event for radio widgets', () => { - const onBlur = sandbox.spy(); + const onBlur = jest.fn(); const { node } = createFormComponent({ schema: { type: 'boolean', @@ -506,12 +513,12 @@ describe('BooleanField', () => { }); const element = node.querySelector('.field-radio-group'); - fireEvent.blur(node.querySelector('input'), { + fireEvent.blur(node.querySelector('input')!, { target: { value: 1, // use index }, }); - expect(onBlur.calledWith(element.id, false)).to.be.true; + expect(onBlur).toHaveBeenLastCalledWith(element?.id, false); }); it('should support ui:enumNames for select, with overrides in uiSchema', () => { @@ -521,12 +528,12 @@ describe('BooleanField', () => { uiSchema: { 'ui:widget': 'select', 'ui:enumNames': ['Si!', 'No!'] }, }); - const labels = [].map.call(node.querySelectorAll('.rjsf-field option'), (label) => label.textContent); - expect(labels).eql(['', 'Si!', 'No!']); + const labels = [].map.call(node.querySelectorAll('.rjsf-field option'), (label: Element) => label.textContent); + expect(labels).toEqual(['', 'Si!', 'No!']); }); it('should handle a focus event with checkbox', () => { - const onFocus = sandbox.spy(); + const onFocus = jest.fn(); const { node } = createFormComponent({ schema: { type: 'boolean', @@ -539,16 +546,16 @@ describe('BooleanField', () => { }); const element = node.querySelector('select'); - fireEvent.focus(element, { + fireEvent.focus(element!, { target: { value: 1, // use index }, }); - expect(onFocus.calledWith(element.id, false)).to.be.true; + expect(onFocus).toHaveBeenLastCalledWith(element?.id, false); }); it('should handle a blur event with select', () => { - const onBlur = sandbox.spy(); + const onBlur = jest.fn(); const { node } = createFormComponent({ schema: { type: 'boolean', @@ -561,12 +568,12 @@ describe('BooleanField', () => { }); const element = node.querySelector('select'); - fireEvent.blur(element, { + fireEvent.blur(element!, { target: { value: 1, // use index }, }); - expect(onBlur.calledWith(element.id, false)).to.be.true; + expect(onBlur).toHaveBeenLastCalledWith(element?.id, false); }); it('should render the widget with the expected id', () => { @@ -576,7 +583,7 @@ describe('BooleanField', () => { }, }); - expect(node.querySelector('input[type=checkbox]').id).eql('root'); + expect(node.querySelector('input[type=checkbox]')).toHaveAttribute('id', 'root'); }); it('should render customized checkbox', () => { @@ -589,11 +596,11 @@ describe('BooleanField', () => { }, }); - expect(node.querySelector('#custom')).to.exist; + expect(node.querySelector('#custom')).toBeInTheDocument(); }); it('should handle a focus event with checkbox', () => { - const onFocus = sandbox.spy(); + const onFocus = jest.fn(); const { node } = createFormComponent({ schema: { type: 'boolean', @@ -606,16 +613,16 @@ describe('BooleanField', () => { }); const element = node.querySelector('input'); - fireEvent.focus(element, { + fireEvent.focus(element!, { target: { checked: false, }, }); - expect(onFocus.calledWith(element.id, false)).to.be.true; + expect(onFocus).toHaveBeenLastCalledWith(element?.id, false); }); it('should handle a blur event with checkbox', () => { - const onBlur = sandbox.spy(); + const onBlur = jest.fn(); const { node } = createFormComponent({ schema: { type: 'boolean', @@ -628,21 +635,21 @@ describe('BooleanField', () => { }); const element = node.querySelector('input'); - fireEvent.blur(element, { + fireEvent.blur(element!, { target: { checked: false, }, }); - expect(onBlur.calledWith(element.id, false)).to.be.true; + expect(onBlur).toHaveBeenLastCalledWith(element?.id, false); }); describe('Label', () => { - const Widget = (props) =>
; + const Widget = (props: WidgetProps) =>
; const widgets = { Widget }; it('should pass field name to widget if there is no title', () => { - const schema = { + const schema: RJSFSchema = { type: 'object', properties: { boolean: { @@ -657,11 +664,11 @@ describe('BooleanField', () => { }; const { node } = createFormComponent({ schema, widgets, uiSchema }); - expect(node.querySelector('#label-boolean')).to.not.be.null; + expect(node.querySelector('#label-boolean')).not.toBeNull(); }); it('should pass schema title to widget', () => { - const schema = { + const schema: RJSFSchema = { type: 'boolean', title: 'test', }; @@ -670,11 +677,11 @@ describe('BooleanField', () => { }; const { node } = createFormComponent({ schema, widgets, uiSchema }); - expect(node.querySelector('#label-test')).to.not.be.null; + expect(node.querySelector('#label-test')).not.toBeNull(); }); it('should pass empty schema title to widget', () => { - const schema = { + const schema: RJSFSchema = { type: 'boolean', title: '', }; @@ -682,7 +689,7 @@ describe('BooleanField', () => { 'ui:widget': 'Widget', }; const { node } = createFormComponent({ schema, widgets, uiSchema }); - expect(node.querySelector('#label-')).to.not.be.null; + expect(node.querySelector('#label-')).not.toBeNull(); }); }); @@ -694,30 +701,27 @@ describe('BooleanField', () => { }, }); - expect(node.querySelectorAll('.rjsf-field select')).to.have.length.of(1); + expect(node.querySelectorAll('.rjsf-field select')).toHaveLength(1); }); it('should infer the value from an enum on change', () => { - const spy = sinon.spy(); - const { node } = createFormComponent({ + const { node, onChange } = createFormComponent({ schema: { enum: [true, false], }, - onChange: spy, }); - expect(node.querySelectorAll('.rjsf-field select')).to.have.length.of(1); + expect(node.querySelectorAll('.rjsf-field select')).toHaveLength(1); const $select = node.querySelector('.rjsf-field select'); - expect($select.value).eql(''); + expect($select).toHaveValue(''); act(() => { - fireEvent.change($select, { + fireEvent.change($select!, { target: { value: 0 }, // use index }); }); - expect(getSelectedOptionValue($select)).eql('true'); - expect(spy.lastCall.args[0].formData).eql(true); - expect(spy.lastCall.args[1]).eql('root'); + expect(getSelectedOptionValue($select as HTMLSelectElement)).toEqual('true'); + expect(onChange).toHaveBeenLastCalledWith(expect.objectContaining({ formData: true }), 'root'); }); it('should render a string field with a label', () => { @@ -728,7 +732,7 @@ describe('BooleanField', () => { }, }); - expect(node.querySelector('.rjsf-field label').textContent).eql('foo'); + expect(node.querySelector('.rjsf-field label')).toHaveTextContent('foo'); }); it('should assign a default value', () => { @@ -738,9 +742,7 @@ describe('BooleanField', () => { default: true, }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { - formData: true, - }); + expect(onChange).toHaveBeenLastCalledWith(expect.objectContaining({ formData: true })); }); it('should handle a change event', () => { @@ -751,18 +753,12 @@ describe('BooleanField', () => { }); act(() => { - fireEvent.change(node.querySelector('select'), { + fireEvent.change(node.querySelector('select')!, { target: { value: 1 }, // use index }); }); - sinon.assert.calledWithMatch( - onChange.lastCall, - { - formData: false, - }, - 'root', - ); + expect(onChange).toHaveBeenLastCalledWith(expect.objectContaining({ formData: false }), 'root'); }); it('should render the widget with the expected id', () => { @@ -772,7 +768,7 @@ describe('BooleanField', () => { }, }); - expect(node.querySelector('select').id).eql('root'); + expect(node.querySelector('select')).toHaveAttribute('id', 'root'); }); }); }); diff --git a/packages/core/test/DescriptionField.test.jsx b/packages/core/test/DescriptionField.test.jsx deleted file mode 100644 index a7342d0d89..0000000000 --- a/packages/core/test/DescriptionField.test.jsx +++ /dev/null @@ -1,59 +0,0 @@ -import { Component } from 'react'; -import { expect } from 'chai'; - -import DescriptionField from '../src/components/templates/DescriptionField'; -import { createSandbox, createComponent } from './test_utils'; - -describe('DescriptionField', () => { - let sandbox; - - beforeEach(() => { - sandbox = createSandbox(); - }); - - afterEach(() => { - sandbox.restore(); - }); - - // For some reason, stateless components needs to be wrapped into a stateful - // one to be rendered into the document. - class DescriptionFieldWrapper extends Component { - constructor(props) { - super(props); - } - render() { - return ; - } - } - - it('should return a div for a custom component', () => { - const props = { - description: description, - registry: {}, - }; - const { node } = createComponent(DescriptionFieldWrapper, props); - - expect(node.tagName).to.equal('DIV'); - }); - - it('should return a p for a description text', () => { - const props = { - description: 'description', - registry: {}, - }; - const { node } = createComponent(DescriptionFieldWrapper, props); - - expect(node.tagName).to.equal('DIV'); - }); - - it('should have the expected id', () => { - const props = { - description: 'Field description', - id: 'sample_id', - registry: {}, - }; - const { node } = createComponent(DescriptionFieldWrapper, props); - - expect(node.id).to.equal('sample_id'); - }); -}); diff --git a/packages/core/test/DescriptionField.test.tsx b/packages/core/test/DescriptionField.test.tsx new file mode 100644 index 0000000000..c92355e9c5 --- /dev/null +++ b/packages/core/test/DescriptionField.test.tsx @@ -0,0 +1,29 @@ +import { render } from '@testing-library/react'; +import { DescriptionFieldProps } from '@rjsf/utils'; + +import DescriptionField from '../src/components/templates/DescriptionField'; +import { getTestRegistry } from '../src'; + +const registry = getTestRegistry({}); + +describe('DescriptionField', () => { + let node: Element; + let props: DescriptionFieldProps; + beforeAll(() => { + props = { + id: 'sample_id', + description: 'Field description', + schema: {}, + registry, + }; + const { container } = render(); + node = container.firstElementChild!; + }); + it('should return a div for a custom component', () => { + expect(node.tagName).toEqual('DIV'); + }); + + it('should have the expected id', () => { + expect(node).toHaveAttribute('id', 'sample_id'); + }); +}); diff --git a/packages/core/test/FieldTemplate.test.jsx b/packages/core/test/FieldTemplate.test.tsx similarity index 78% rename from packages/core/test/FieldTemplate.test.jsx rename to packages/core/test/FieldTemplate.test.tsx index 22e6b7f38d..51bed7d23b 100644 --- a/packages/core/test/FieldTemplate.test.jsx +++ b/packages/core/test/FieldTemplate.test.tsx @@ -1,21 +1,11 @@ import { Children } from 'react'; +import { FieldTemplateProps, RJSFSchema } from '@rjsf/utils'; -import { expect } from 'chai'; -import { createFormComponent, createSandbox } from './test_utils'; +import { createFormComponent } from './testUtils'; describe('FieldTemplate', () => { - let sandbox; - - beforeEach(() => { - sandbox = createSandbox(); - }); - - afterEach(() => { - sandbox.restore(); - }); - describe('FieldTemplate should only have one child', () => { - function FieldTemplate(props) { + function FieldTemplate(props: FieldTemplateProps) { if (Children.count(props.children) !== 1) { throw 'Got wrong number of children'; } @@ -29,7 +19,7 @@ describe('FieldTemplate', () => { }); describe('Custom FieldTemplate for disabled property', () => { - function FieldTemplate(props) { + function FieldTemplate(props: FieldTemplateProps) { return
; } @@ -40,7 +30,7 @@ describe('FieldTemplate', () => { uiSchema: { 'ui:disabled': true }, templates: { FieldTemplate }, }); - expect(node.querySelectorAll('.disabled')).to.have.length.of(1); + expect(node.querySelectorAll('.disabled')).toHaveLength(1); }); it('should render with disabled when ui:disabled is falsey', () => { @@ -49,7 +39,7 @@ describe('FieldTemplate', () => { uiSchema: { 'ui:disabled': false }, templates: { FieldTemplate }, }); - expect(node.querySelectorAll('.disabled')).to.have.length.of(0); + expect(node.querySelectorAll('.disabled')).toHaveLength(0); }); }); describe('with template configured in ui:FieldTemplate', () => { @@ -58,7 +48,7 @@ describe('FieldTemplate', () => { schema: { type: 'string' }, uiSchema: { 'ui:disabled': true, 'ui:FieldTemplate': FieldTemplate }, }); - expect(node.querySelectorAll('.disabled')).to.have.length.of(1); + expect(node.querySelectorAll('.disabled')).toHaveLength(1); }); it('should render with disabled when ui:disabled is falsey', () => { @@ -66,7 +56,7 @@ describe('FieldTemplate', () => { schema: { type: 'string' }, uiSchema: { 'ui:disabled': false, 'ui:FieldTemplate': FieldTemplate }, }); - expect(node.querySelectorAll('.disabled')).to.have.length.of(0); + expect(node.querySelectorAll('.disabled')).toHaveLength(0); }); }); describe('with template configured globally being overriden in ui:FieldTemplate', () => { @@ -77,7 +67,7 @@ describe('FieldTemplate', () => { // Empty field template to prove that overides work templates: { FieldTemplate: () =>
}, }); - expect(node.querySelectorAll('.disabled')).to.have.length.of(1); + expect(node.querySelectorAll('.disabled')).toHaveLength(1); }); it('should render with disabled when ui:disabled is falsey', () => { @@ -87,13 +77,13 @@ describe('FieldTemplate', () => { // Empty field template to prove that overides work templates: { FieldTemplate: () =>
}, }); - expect(node.querySelectorAll('.disabled')).to.have.length.of(0); + expect(node.querySelectorAll('.disabled')).toHaveLength(0); }); }); }); describe('Custom FieldTemplate should have registry', () => { - function FieldTemplate(props) { + function FieldTemplate(props: FieldTemplateProps) { return (
Root Schema: {JSON.stringify(props.registry.rootSchema)} @@ -102,7 +92,7 @@ describe('FieldTemplate', () => { } it('should allow access to root schema from registry', () => { - const schema = { + const schema: RJSFSchema = { type: 'object', properties: { fooBarBaz: { type: 'string' } }, }; @@ -112,8 +102,8 @@ describe('FieldTemplate', () => { templates: { FieldTemplate }, }); - expect(node.querySelectorAll('#root-schema')).to.have.length.of(1); - expect(node.querySelectorAll('#root-schema')[0].innerHTML).to.equal(JSON.stringify(schema)); + expect(node.querySelectorAll('#root-schema')).toHaveLength(1); + expect(node.querySelectorAll('#root-schema')[0].innerHTML).toEqual(JSON.stringify(schema)); }); }); }); diff --git a/packages/core/test/FormContext.test.jsx b/packages/core/test/FormContext.test.tsx similarity index 66% rename from packages/core/test/FormContext.test.jsx rename to packages/core/test/FormContext.test.tsx index 41ef46b2ac..db0c491207 100644 --- a/packages/core/test/FormContext.test.jsx +++ b/packages/core/test/FormContext.test.tsx @@ -1,64 +1,45 @@ -import { expect } from 'chai'; +import { ArrayFieldTemplateProps, FieldTemplateProps, RJSFSchema } from '@rjsf/utils'; -import { createFormComponent, createSandbox } from './test_utils'; +import { createFormComponent } from './testUtils'; -describe('FormContext', () => { - let sandbox; - - beforeEach(() => { - sandbox = createSandbox(); - }); +const schema: RJSFSchema = { type: 'string' }; - afterEach(() => { - sandbox.restore(); - }); +const formContext = { foo: 'bar' }; - const schema = { type: 'string' }; +const fooId = `#${formContext.foo}`; - const formContext = { foo: 'bar' }; - - const CustomComponent = function (props) { - const { registry } = props; - const { formContext } = registry; - return
; - }; - - it('should be passed to Form', () => { - const { comp } = createFormComponent({ - schema: schema, - formContext, - }); - expect(comp.props.formContext).eq(formContext); - }); +// Use `props: any` to support the variety of uses (widgets, fields, templates) +function CustomComponent(props: any) { + const { registry } = props; + const { formContext } = registry; + return
; +} +describe('FormContext', () => { it('should be passed to custom field', () => { - const fields = { custom: CustomComponent }; - const { node } = createFormComponent({ schema: schema, uiSchema: { 'ui:field': 'custom' }, - fields, + fields: { custom: CustomComponent }, formContext, }); - expect(node.querySelector('#' + formContext.foo)).to.exist; + expect(node.querySelector(fooId)).toBeInTheDocument(); }); it('should be passed to custom widget', () => { - const widgets = { custom: CustomComponent }; - const { node } = createFormComponent({ schema: { type: 'string' }, uiSchema: { 'ui:widget': 'custom' }, - widgets, + widgets: { custom: CustomComponent }, formContext, }); - expect(node.querySelector('#' + formContext.foo)).to.exist; + expect(node.querySelector(fooId)).toBeInTheDocument(); }); it('should be passed to TemplateField', () => { - function CustomTemplateField({ registry: { formContext } }) { + function CustomTemplateField({ registry: { formContext } }: FieldTemplateProps) { return
; } @@ -75,11 +56,11 @@ describe('FormContext', () => { formContext, }); - expect(node.querySelector('#' + formContext.foo)).to.exist; + expect(node.querySelector(fooId)).toBeInTheDocument(); }); it('should be passed to ArrayTemplateField', () => { - function CustomArrayTemplateField({ registry: { formContext } }) { + function CustomArrayTemplateField({ registry: { formContext } }: ArrayFieldTemplateProps) { return
; } @@ -94,7 +75,7 @@ describe('FormContext', () => { formContext, }); - expect(node.querySelector('#' + formContext.foo)).to.exist; + expect(node.querySelector(fooId)).toBeInTheDocument(); }); it('should be passed to custom TitleFieldTemplate', () => { @@ -114,7 +95,7 @@ describe('FormContext', () => { formContext, }); - expect(node.querySelector('#' + formContext.foo)).to.exist; + expect(node.querySelector(fooId)).toBeInTheDocument(); }); it('should be passed to custom DescriptionFieldTemplate', () => { @@ -126,7 +107,7 @@ describe('FormContext', () => { formContext, }); - expect(node.querySelector('#' + formContext.foo)).to.exist; + expect(node.querySelector(fooId)).toBeInTheDocument(); }); it('should be passed to multiselect', () => { @@ -149,7 +130,7 @@ describe('FormContext', () => { formContext, }); - expect(node.querySelector('#' + formContext.foo)).to.exist; + expect(node.querySelector(fooId)).toBeInTheDocument(); }); it('should be passed to files array', () => { @@ -166,6 +147,6 @@ describe('FormContext', () => { formContext, }); - expect(node.querySelector('#' + formContext.foo)).to.exist; + expect(node.querySelector(fooId)).toBeInTheDocument(); }); }); diff --git a/packages/core/test/NullField.test.jsx b/packages/core/test/NullField.test.tsx similarity index 61% rename from packages/core/test/NullField.test.jsx rename to packages/core/test/NullField.test.tsx index d4eb92ea3b..be884abeb4 100644 --- a/packages/core/test/NullField.test.jsx +++ b/packages/core/test/NullField.test.tsx @@ -1,18 +1,6 @@ -import { expect } from 'chai'; -import { createFormComponent, createSandbox, submitForm } from './test_utils'; -import sinon from 'sinon'; +import { createFormComponent, submitForm } from './testUtils'; describe('NullField', () => { - let sandbox; - - beforeEach(() => { - sandbox = createSandbox(); - }); - - afterEach(() => { - sandbox.restore(); - }); - describe('No widget', () => { it('should render a null field', () => { const { node } = createFormComponent({ @@ -21,7 +9,7 @@ describe('NullField', () => { }, }); - expect(node.querySelectorAll('.rjsf-field')).to.have.length.of(1); + expect(node.querySelectorAll('.rjsf-field')).toHaveLength(1); }); it('should render a null field with a label', () => { @@ -32,7 +20,7 @@ describe('NullField', () => { }, }); - expect(node.querySelector('.rjsf-field label').textContent).eql('foo'); + expect(node.querySelector('.rjsf-field label')).toHaveTextContent('foo'); }); it('should assign a default value', () => { @@ -43,7 +31,7 @@ describe('NullField', () => { }, }); - sinon.assert.calledWithMatch(onChange.lastCall, { formData: null }); + expect(onChange).toHaveBeenLastCalledWith(expect.objectContaining({ formData: null })); }); it('should not overwrite existing data', () => { @@ -56,7 +44,10 @@ describe('NullField', () => { }); submitForm(node); - sinon.assert.calledWithMatch(onSubmit.lastCall, { formData: 3 }); + expect(onSubmit).toHaveBeenLastCalledWith( + expect.objectContaining({ formData: 3 }), + expect.objectContaining({ type: 'submit' }), + ); }); }); }); diff --git a/packages/core/test/NumberField.test.jsx b/packages/core/test/NumberField.test.tsx similarity index 58% rename from packages/core/test/NumberField.test.jsx rename to packages/core/test/NumberField.test.tsx index f3b28a7c20..77b0bec719 100644 --- a/packages/core/test/NumberField.test.jsx +++ b/packages/core/test/NumberField.test.tsx @@ -1,21 +1,15 @@ -import * as React from 'react'; -import { expect } from 'chai'; -import { fireEvent, act } from '@testing-library/react'; -import sinon from 'sinon'; +import { createRef } from 'react'; +import { RJSFSchema, UiSchema } from '@rjsf/utils'; +import { act, fireEvent } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import isEmpty from 'lodash/isEmpty'; -import { createFormComponent, createSandbox, getSelectedOptionValue, setProps, submitForm } from './test_utils'; +import Form from '../src'; +import { createFormComponent, getSelectedOptionValue, submitForm } from './testUtils'; -describe('NumberField', () => { - let sandbox; - - beforeEach(() => { - sandbox = createSandbox(); - }); - - afterEach(() => { - sandbox.restore(); - }); +const user = userEvent.setup(); +describe('NumberField', () => { describe('Number widget', () => { it('should use step to represent the multipleOf keyword', () => { const { node } = createFormComponent({ @@ -25,7 +19,7 @@ describe('NumberField', () => { }, }); - expect(node.querySelector('input').step).to.eql('5'); + expect(node.querySelector('input')).toHaveAttribute('step', '5'); }); it('should use min to represent the minimum keyword', () => { @@ -36,7 +30,7 @@ describe('NumberField', () => { }, }); - expect(node.querySelector('input').min).to.eql('0'); + expect(node.querySelector('input')).toHaveAttribute('min', '0'); }); it('should use max to represent the maximum keyword', () => { @@ -47,7 +41,7 @@ describe('NumberField', () => { }, }); - expect(node.querySelector('input').max).to.eql('100'); + expect(node.querySelector('input')).toHaveAttribute('max', '100'); }); it('should use step to represent the multipleOf keyword', () => { @@ -58,7 +52,7 @@ describe('NumberField', () => { }, }); - expect(node.querySelector('input').step).to.eql('5'); + expect(node.querySelector('input')).toHaveAttribute('step', '5'); }); it('should use min to represent the minimum keyword', () => { @@ -69,7 +63,7 @@ describe('NumberField', () => { }, }); - expect(node.querySelector('input').min).to.eql('0'); + expect(node.querySelector('input')).toHaveAttribute('min', '0'); }); it('should use max to represent the maximum keyword', () => { @@ -80,11 +74,11 @@ describe('NumberField', () => { }, }); - expect(node.querySelector('input').max).to.eql('100'); + expect(node.querySelector('input')).toHaveAttribute('max', '100'); }); }); describe('Number and text widget', () => { - let uiSchemas = [ + const uiSchemas: UiSchema[] = [ {}, { 'ui:options': { @@ -92,7 +86,7 @@ describe('NumberField', () => { }, }, ]; - for (let uiSchema of uiSchemas) { + for (const uiSchema of uiSchemas) { it('should render a string field with a label', () => { const { node } = createFormComponent({ schema: { @@ -102,7 +96,7 @@ describe('NumberField', () => { uiSchema, }); - expect(node.querySelector('.rjsf-field label').textContent).eql('foo'); + expect(node.querySelector('.rjsf-field label')).toHaveTextContent('foo'); }); it('should render a string field with a description', () => { @@ -114,7 +108,7 @@ describe('NumberField', () => { uiSchema, }); - expect(node.querySelector('.field-description').textContent).eql('bar'); + expect(node.querySelector('.field-description')).toHaveTextContent('bar'); }); it('formData should default to undefined', () => { @@ -125,9 +119,10 @@ describe('NumberField', () => { }); submitForm(node); - sinon.assert.calledWithMatch(onSubmit.lastCall, { - formData: undefined, - }); + expect(onSubmit).toHaveBeenLastCalledWith( + expect.objectContaining({ formData: undefined }), + expect.objectContaining({ type: 'submit' }), + ); }); it('should assign a default value', () => { @@ -139,10 +134,10 @@ describe('NumberField', () => { uiSchema, }); - expect(node.querySelector('.rjsf-field input').value).eql('2'); + expect(node.querySelector('.rjsf-field input')).toHaveAttribute('value', '2'); }); - it('should handle a change event', () => { + it('should handle a change event', async () => { const { node, onChange } = createFormComponent({ schema: { type: 'number', @@ -150,23 +145,13 @@ describe('NumberField', () => { uiSchema, }); - act(() => { - fireEvent.change(node.querySelector('input'), { - target: { value: '2' }, - }); - }); + await user.type(node.querySelector('input')!, '2'); - sinon.assert.calledWithMatch( - onChange.lastCall, - { - formData: 2, - }, - 'root', - ); + expect(onChange).toHaveBeenLastCalledWith(expect.objectContaining({ formData: 2 }), 'root'); }); it('should handle a blur event', () => { - const onBlur = sandbox.spy(); + const onBlur = jest.fn(); const { node } = createFormComponent({ schema: { type: 'number', @@ -176,15 +161,15 @@ describe('NumberField', () => { }); const input = node.querySelector('input'); - fireEvent.blur(input, { + fireEvent.blur(input!, { target: { value: '2' }, }); - expect(onBlur.calledWith(input.id, 2)); + expect(onBlur).toHaveBeenCalledWith(input?.id, '2'); }); it('should handle a focus event', () => { - const onFocus = sandbox.spy(); + const onFocus = jest.fn(); const { node } = createFormComponent({ schema: { type: 'number', @@ -194,11 +179,11 @@ describe('NumberField', () => { }); const input = node.querySelector('input'); - fireEvent.focus(input, { + fireEvent.focus(input!, { target: { value: '2' }, }); - expect(onFocus.calledWith(input.id, 2)); + expect(onFocus).toHaveBeenCalledWith(input?.id, '2'); }); it('should fill field with data', () => { @@ -210,7 +195,7 @@ describe('NumberField', () => { formData: 2, }); - expect(node.querySelector('.rjsf-field input').value).eql('2'); + expect(node.querySelector('.rjsf-field input')).toHaveAttribute('value', '2'); }); describe('when inputting a number that ends with a dot and/or zero it should normalize it, without changing the input value', () => { @@ -258,7 +243,7 @@ describe('NumberField', () => { ]; tests.forEach((test) => { - it(`should work with an input value of ${test.input}`, () => { + it(`should work with an input value of ${test.input}`, async () => { const { node, onChange } = createFormComponent({ schema: { type: 'number', @@ -268,29 +253,17 @@ describe('NumberField', () => { const $input = node.querySelector('input'); - act(() => { - fireEvent.change($input, { - target: { value: test.input }, - }); - }); + await user.type($input!, test.input); - setTimeout(() => { - sinon.assert.calledWithMatch( - onChange.lastCall, - { - formData: test.output, - }, - 'root', - ); - // "2." is not really a valid number in a input field of type number - // so we need to use getAttribute("value") instead since .value outputs the empty string - expect($input.getAttribute('value')).eql(test.input); - }, 0); + expect(onChange).toHaveBeenLastCalledWith(expect.objectContaining({ formData: test.output }), 'root'); + // "2." is not really a valid number in a input field of type number + // so we need to use getAttribute("value") instead since .value outputs the empty string + expect($input).toHaveValue(isEmpty(uiSchema) ? test.output : test.input); }); }); }); - it('should normalize values beginning with a decimal point', () => { + it('should normalize values beginning with a decimal point', async () => { const { node, onChange } = createFormComponent({ schema: { type: 'number', @@ -300,29 +273,20 @@ describe('NumberField', () => { const $input = node.querySelector('input'); - act(() => { - fireEvent.change($input, { - target: { value: '.00' }, - }); - }); + await user.type($input!, '.00'); - sinon.assert.calledWithMatch( - onChange.lastCall, - { - formData: 0, - }, - 'root', - ); - expect($input.value).eql('.00'); + expect(onChange).toHaveBeenLastCalledWith(expect.objectContaining({ formData: 0 }), 'root'); + const expected = isEmpty(uiSchema) ? 0 : '.00'; + expect($input).toHaveValue(expected); }); it('should update input values correctly when formData prop changes', () => { - const schema = { + const schema: RJSFSchema = { type: 'number', }; - const { comp, node } = createFormComponent({ - ref: React.createRef(), + const { rerender, node } = createFormComponent({ + ref: createRef(), schema, uiSchema, formData: 2.03, @@ -330,48 +294,41 @@ describe('NumberField', () => { const $input = node.querySelector('input'); - expect($input.value).eql('2.03'); + expect($input).toHaveAttribute('value', '2.03'); - setProps(comp, { - schema, - formData: 203, - }); + rerender({ schema, formData: 203 }); - expect($input.value).eql('203'); + expect($input).toHaveAttribute('value', '203'); }); - it('form reset should work for a default value', () => { - const onChangeSpy = sinon.spy(); - const schema = { + it('form reset should work for a default value', async () => { + const schema: RJSFSchema = { type: 'number', default: 1, }; - const ref = React.createRef(); + const ref = createRef
(); - const { node } = createFormComponent({ - ref: ref, + const { node, onChange } = createFormComponent({ + ref, schema, uiSchema, - onChange: onChangeSpy, - }); - - act(() => { - fireEvent.change(node.querySelector('input'), { - target: { value: '231' }, - }); }); const $input = node.querySelector('input'); - expect($input.value).eql('231'); - sinon.assert.calledWithMatch(onChangeSpy.lastCall, { formData: 231 }); + + await user.type($input!, '231', { initialSelectionStart: 0, initialSelectionEnd: 1 }); + + expect($input).toHaveValue(isEmpty(uiSchema) ? 231 : '231'); + expect(onChange).toHaveBeenLastCalledWith(expect.objectContaining({ formData: 231 }), 'root'); act(() => { - ref.current.reset(); + ref.current?.reset(); }); - expect($input.value).eql('1'); - sinon.assert.calledWithMatch(onChangeSpy.lastCall, { formData: 1 }); + expect($input).toHaveValue(isEmpty(uiSchema) ? 1 : '1'); + // No id on programmatic change + expect(onChange).toHaveBeenLastCalledWith(expect.objectContaining({ formData: 1 })); }); it('should render the widget with the expected id', () => { @@ -382,52 +339,50 @@ describe('NumberField', () => { uiSchema, }); - expect(node.querySelector('input').id).eql('root'); + expect(node.querySelector('input')).toHaveAttribute('id', 'root'); }); - it('should render with trailing zeroes', () => { + it('should render with trailing zeroes', async () => { const { node } = createFormComponent({ schema: { type: 'number', }, uiSchema, }); - - act(() => { - fireEvent.change(node.querySelector('input'), { - target: { value: '2.' }, - }); - }); - - // "2." is not really a valid number in a input field of type number - // so we need to use getAttribute("value") instead since .value outputs the empty string - setTimeout(() => { - expect(node.querySelector('.rjsf-field input').getAttribute('value')).eql('2.'); - }); - - act(() => { - fireEvent.change(node.querySelector('input'), { - target: { value: '2.0' }, - }); - }); - expect(node.querySelector('.rjsf-field input').value).eql('2.0'); - - act(() => { - fireEvent.change(node.querySelector('input'), { - target: { value: '2.00' }, - }); - }); - expect(node.querySelector('.rjsf-field input').value).eql('2.00'); - - act(() => { - fireEvent.change(node.querySelector('input'), { - target: { value: '2.000' }, - }); - }); - expect(node.querySelector('.rjsf-field input').value).eql('2.000'); - }); - - it('should allow a zero to be input', () => { + const isNumber = isEmpty(uiSchema); + await user.type(node.querySelector('input')!, '2.'); + + if (isNumber) { + // "2." is not really a valid number in a input field of type number + // so we need to use getAttribute("value") instead since .value outputs the empty string + expect(node.querySelector('.rjsf-field input')).toHaveValue(2); + } else { + expect(node.querySelector('.rjsf-field input')).toHaveValue('2.'); + } + + await user.type(node.querySelector('input')!, '0'); + if (isNumber) { + expect(node.querySelector('.rjsf-field input')).toHaveValue(2.0); + } else { + expect(node.querySelector('.rjsf-field input')).toHaveValue('2.0'); + } + + await user.type(node.querySelector('input')!, '0'); + if (isNumber) { + expect(node.querySelector('.rjsf-field input')).toHaveValue(2.0); + } else { + expect(node.querySelector('.rjsf-field input')).toHaveValue('2.00'); + } + + await user.type(node.querySelector('input')!, '0'); + if (isNumber) { + expect(node.querySelector('.rjsf-field input')).toHaveValue(2.0); + } else { + expect(node.querySelector('.rjsf-field input')).toHaveValue('2.000'); + } + }); + + it('should allow a zero to be input', async () => { const { node } = createFormComponent({ schema: { type: 'number', @@ -435,12 +390,9 @@ describe('NumberField', () => { uiSchema, }); - act(() => { - fireEvent.change(node.querySelector('input'), { - target: { value: '0' }, - }); - }); - expect(node.querySelector('.rjsf-field input').value).eql('0'); + await user.type(node.querySelector('input')!, '0'); + const expected = isEmpty(uiSchema) ? 0 : '0'; + expect(node.querySelector('.rjsf-field input')).toHaveValue(expected); }); it('should render customized StringField', () => { @@ -456,7 +408,7 @@ describe('NumberField', () => { }, }); - expect(node.querySelector('#custom')).to.exist; + expect(node.querySelector('#custom')).toBeInTheDocument(); }); } }); @@ -470,30 +422,27 @@ describe('NumberField', () => { }, }); - expect(node.querySelectorAll('.rjsf-field select')).to.have.length.of(1); + expect(node.querySelectorAll('.rjsf-field select')).toHaveLength(1); }); it('should infer the value from an enum on change', () => { - const spy = sinon.spy(); - const { node } = createFormComponent({ + const { node, onChange } = createFormComponent({ schema: { enum: [1, 2], }, - onChange: spy, }); - expect(node.querySelectorAll('.rjsf-field select')).to.have.length.of(1); + expect(node.querySelectorAll('.rjsf-field select')).toHaveLength(1); const $select = node.querySelector('.rjsf-field select'); - expect($select.value).eql(''); + expect($select).not.toHaveAttribute('value'); act(() => { - fireEvent.change(node.querySelector('.rjsf-field select'), { + fireEvent.change(node.querySelector('.rjsf-field select')!, { target: { value: 0 }, // use index }); }); - expect(getSelectedOptionValue($select)).eql('1'); - expect(spy.lastCall.args[0].formData).eql(1); - expect(spy.lastCall.args[1]).eql('root'); + expect(getSelectedOptionValue($select as HTMLSelectElement)).toEqual('1'); + expect(onChange).toHaveBeenLastCalledWith(expect.objectContaining({ formData: 1 }), 'root'); }); it('should render a string field with a label', () => { @@ -505,7 +454,7 @@ describe('NumberField', () => { }, }); - expect(node.querySelector('.rjsf-field label').textContent).eql('foo'); + expect(node.querySelector('.rjsf-field label')).toHaveTextContent('foo'); }); it('should assign a default value', () => { @@ -518,7 +467,8 @@ describe('NumberField', () => { noValidate: true, }); - sinon.assert.calledWithMatch(onChange.lastCall, { formData: 1 }); + // No id on initial onChange + expect(onChange).toHaveBeenLastCalledWith(expect.objectContaining({ formData: 1 })); }); it('should handle a change event', () => { @@ -530,12 +480,12 @@ describe('NumberField', () => { }); act(() => { - fireEvent.change(node.querySelector('select'), { + fireEvent.change(node.querySelector('select')!, { target: { value: 1 }, // useIndex }); }); - sinon.assert.calledWithMatch(onChange.lastCall, { formData: 2 }, 'root'); + expect(onChange).toHaveBeenLastCalledWith(expect.objectContaining({ formData: 2 }), 'root'); }); it('should fill field with data', () => { @@ -547,7 +497,10 @@ describe('NumberField', () => { formData: 2, }); submitForm(node); - sinon.assert.calledWithMatch(onSubmit.lastCall, { formData: 2 }); + expect(onSubmit).toHaveBeenLastCalledWith( + expect.objectContaining({ formData: 2 }), + expect.objectContaining({ type: 'submit' }), + ); }); it('should render the widget with the expected id', () => { @@ -558,11 +511,11 @@ describe('NumberField', () => { }, }); - expect(node.querySelector('select').id).eql('root'); + expect(node.querySelector('select')).toHaveAttribute('id', 'root'); }); it('should render a select element with a blank option, when default value is not set.', () => { - const schema = { + const schema: RJSFSchema = { type: 'object', properties: { foo: { @@ -577,15 +530,15 @@ describe('NumberField', () => { }); const selects = node.querySelectorAll('select'); - expect(selects[0].value).eql(''); + expect(selects[0]).not.toHaveAttribute('value'); const options = node.querySelectorAll('option'); - expect(options.length).eql(2); - expect(options[0].innerHTML).eql(''); + expect(options.length).toEqual(2); + expect(options[0].innerHTML).toEqual(''); }); it('should render a select element without a blank option, if a default value is set.', () => { - const schema = { + const schema: RJSFSchema = { type: 'object', properties: { foo: { @@ -601,15 +554,15 @@ describe('NumberField', () => { }); const selects = node.querySelectorAll('select'); - expect(getSelectedOptionValue(selects[0])).eql('2'); + expect(getSelectedOptionValue(selects[0])).toEqual('2'); const options = node.querySelectorAll('option'); - expect(options.length).eql(1); - expect(options[0].innerHTML).eql('2'); + expect(options.length).toEqual(1); + expect(options[0].innerHTML).toEqual('2'); }); it('should render a select element without a blank option, if the default value is 0.', () => { - const schema = { + const schema: RJSFSchema = { type: 'object', properties: { foo: { @@ -624,12 +577,13 @@ describe('NumberField', () => { schema, }); + console.log(node.innerHTML); const selects = node.querySelectorAll('select'); - expect(selects[0].value).eql('0'); + expect(selects[0]).not.toHaveAttribute('value'); const options = node.querySelectorAll('option'); - expect(options.length).eql(1); - expect(options[0].innerHTML).eql('0'); + expect(options.length).toEqual(1); + expect(options[0].innerHTML).toEqual('0'); }); }); }); diff --git a/packages/core/test/ObjectFieldTemplate.test.jsx b/packages/core/test/ObjectFieldTemplate.test.tsx similarity index 56% rename from packages/core/test/ObjectFieldTemplate.test.jsx rename to packages/core/test/ObjectFieldTemplate.test.tsx index 39b1d4f7de..07cbee16bc 100644 --- a/packages/core/test/ObjectFieldTemplate.test.jsx +++ b/packages/core/test/ObjectFieldTemplate.test.tsx @@ -1,47 +1,52 @@ import { PureComponent } from 'react'; +import { DescriptionFieldProps, ObjectFieldTemplateProps } from '@rjsf/utils'; -import { expect } from 'chai'; -import { createFormComponent, createSandbox } from './test_utils'; +import { createFormComponent } from './testUtils'; -describe('ObjectFieldTemplate', () => { - let sandbox; - - const formData = { foo: 'bar', bar: 'foo' }; - - beforeEach(() => { - sandbox = createSandbox(); - }); - - afterEach(() => { - sandbox.restore(); - }); - - class ObjectFieldTemplate extends PureComponent { - render() { - const { properties, title, description, registry } = this.props; - const { DescriptionFieldTemplate, TitleFieldTemplate } = registry.templates; - return ( -
- - -
- {properties.map(({ content }, index) => ( -
- {content} -
- ))} -
+const formData = { foo: 'bar', bar: 'foo' }; +class ObjectFieldTemplate extends PureComponent { + render() { + const { properties, title, description = '', registry, schema } = this.props; + const { DescriptionFieldTemplate, TitleFieldTemplate } = registry.templates; + return ( +
+ + +
+ {properties.map(({ content }, index) => ( +
+ {content} +
+ ))}
- ); - } +
+ ); } +} - const TitleFieldTemplate = () =>
; - const DescriptionFieldTemplate = ({ description }) => (description ?
: null); +const TitleFieldTemplate = () =>
; +const DescriptionFieldTemplate = ({ description }: DescriptionFieldProps) => + description ?
: null; + +describe('ObjectFieldTemplate', () => { + function sharedIts() { + it('should render one root element', () => { + expect(node.querySelectorAll('.root')).toHaveLength(1); + }); + it('should render one title', () => { + expect(node.querySelectorAll('.title-field')).toHaveLength(1); + }); + it('should render one description', () => { + expect(node.querySelectorAll('.description-field')).toHaveLength(1); + }); + it('should render two property containers', () => { + expect(node.querySelectorAll('.property')).toHaveLength(2); + }); + } - let node; + let node: Element; describe('with template globally configured', () => { - node = createFormComponent({ + createFormComponent({ schema: { type: 'object', properties: { foo: { type: 'string' }, bar: { type: 'string' } }, @@ -53,7 +58,7 @@ describe('ObjectFieldTemplate', () => { TitleFieldTemplate, DescriptionFieldTemplate, }, - }).node; + }); sharedIts(); }); describe('with template configured in ui:ObjectFieldTemplate', () => { @@ -93,22 +98,4 @@ describe('ObjectFieldTemplate', () => { }).node; sharedIts(); }); - - function sharedIts() { - it('should render one root element', () => { - expect(node.querySelectorAll('.root')).to.have.length.of(1); - }); - - it('should render one title', () => { - expect(node.querySelectorAll('.title-field')).to.have.length.of(1); - }); - - it('should render one description', () => { - expect(node.querySelectorAll('.description-field')).to.have.length.of(1); - }); - - it('should render two property containers', () => { - expect(node.querySelectorAll('.property')).to.have.length.of(2); - }); - } }); diff --git a/packages/core/test/TitleField.test.jsx b/packages/core/test/TitleField.test.jsx deleted file mode 100644 index 448e00ea61..0000000000 --- a/packages/core/test/TitleField.test.jsx +++ /dev/null @@ -1,71 +0,0 @@ -import { Component } from 'react'; -import { expect } from 'chai'; - -import TitleField from '../src/components/templates/TitleField'; -import { createSandbox, createComponent } from './test_utils'; - -describe('TitleField', () => { - let sandbox; - - beforeEach(() => { - sandbox = createSandbox(); - }); - - afterEach(() => { - sandbox.restore(); - }); - - // For some reason, stateless components needs to be wrapped into a stateful - // one to be rendered into the document. - class TitleFieldWrapper extends Component { - constructor(props) { - super(props); - } - render() { - return ; - } - } - - it('should return a legend', () => { - const props = { - title: 'Field title', - required: true, - }; - const { node } = createComponent(TitleFieldWrapper, props); - - expect(node.tagName).to.equal('LEGEND'); - }); - - it('should have the expected id', () => { - const props = { - title: 'Field title', - required: true, - id: 'sample_id', - }; - const { node } = createComponent(TitleFieldWrapper, props); - - expect(node.id).to.equal('sample_id'); - }); - - it('should include only title, when field is not required', () => { - const props = { - title: 'Field title', - required: false, - }; - const { node } = createComponent(TitleFieldWrapper, props); - - expect(node.textContent).to.equal(props.title); - }); - - it('should add an asterisk to the title, when field is required', () => { - const props = { - title: 'Field title', - required: true, - }; - const { node } = createComponent(TitleFieldWrapper, props); - - expect(node.textContent).to.equal(props.title + '*'); - - expect(node.querySelector('span.required').textContent).to.equal('*'); - }); -}); diff --git a/packages/core/test/TitleField.test.tsx b/packages/core/test/TitleField.test.tsx new file mode 100644 index 0000000000..0159e986a9 --- /dev/null +++ b/packages/core/test/TitleField.test.tsx @@ -0,0 +1,40 @@ +import { render } from '@testing-library/react'; +import { TitleFieldProps } from '@rjsf/utils'; + +import TitleField from '../src/components/templates/TitleField'; +import { getTestRegistry } from '../src'; + +const registry = getTestRegistry({}); + +describe('TitleField', () => { + let node: Element; + let props: TitleFieldProps; + beforeAll(() => { + props = { + id: 'sample_id', + title: 'Field title', + required: true, + schema: {}, + registry, + }; + const { container } = render(); + node = container.firstElementChild!; + }); + it('should return a legend', () => { + expect(node.tagName).toEqual('LEGEND'); + }); + + it('should have the expected id', () => { + expect(node).toHaveAttribute('id', 'sample_id'); + }); + + it('should include only title, when field is not required', () => { + expect(node).toHaveTextContent(props.title); + }); + + it('should add an asterisk to the title, when field is required', () => { + expect(node).toHaveTextContent(props.title + '*'); + + expect(node.querySelector('span.required')).toHaveTextContent('*'); + }); +}); diff --git a/packages/core/test/allOf.test.jsx b/packages/core/test/allOf.test.tsx similarity index 69% rename from packages/core/test/allOf.test.jsx rename to packages/core/test/allOf.test.tsx index dbd933edb8..49ce6d4fda 100644 --- a/packages/core/test/allOf.test.jsx +++ b/packages/core/test/allOf.test.tsx @@ -1,21 +1,11 @@ -import { expect } from 'chai'; +import { RJSFSchema, FieldProps, GenericObjectType } from '@rjsf/utils'; -import { createFormComponent, createSandbox } from './test_utils'; +import { createFormComponent } from './testUtils'; import SchemaField from '../src/components/fields/SchemaField'; describe('allOf', () => { - let sandbox; - - beforeEach(() => { - sandbox = createSandbox(); - }); - - afterEach(() => { - sandbox.restore(); - }); - it('should render a regular input element with a single type, when multiple types specified', () => { - const schema = { + const schema: RJSFSchema = { type: 'object', properties: { foo: { @@ -28,11 +18,11 @@ describe('allOf', () => { schema, }); - expect(node.querySelectorAll('input')).to.have.length.of(1); + expect(node.querySelectorAll('input')).toHaveLength(1); }); it('should be able to handle incompatible types and not crash', () => { - const schema = { + const schema: RJSFSchema = { type: 'object', properties: { foo: { @@ -45,10 +35,10 @@ describe('allOf', () => { schema, }); - expect(node.querySelectorAll('input')).to.have.length.of(0); + expect(node.querySelectorAll('input')).toHaveLength(0); }); it('should pass form context to schema field', () => { - const schema = { + const schema: RJSFSchema = { type: 'object', properties: { foo: { @@ -56,8 +46,8 @@ describe('allOf', () => { }, }, }; - const formContext = { root: 'root-id', root_foo: 'foo-id' }; - function CustomSchemaField(props) { + const formContext: GenericObjectType = { root: 'root-id', root_foo: 'foo-id' }; + function CustomSchemaField(props: FieldProps) { const { registry: { formContext }, fieldPathId, @@ -77,9 +67,9 @@ describe('allOf', () => { }); const codeBlocks = node.querySelectorAll('code'); - expect(codeBlocks).to.have.length(2); + expect(codeBlocks).toHaveLength(2); Object.keys(formContext).forEach((key) => { - expect(node.querySelector(`code#${formContext[key]}`)).to.exist; + expect(node.querySelector(`code#${formContext[key]}`)).toBeInTheDocument(); }); }); }); diff --git a/packages/core/test/const.test.js b/packages/core/test/const.test.tsx similarity index 65% rename from packages/core/test/const.test.js rename to packages/core/test/const.test.tsx index 404d899d5d..c71a50da78 100644 --- a/packages/core/test/const.test.js +++ b/packages/core/test/const.test.tsx @@ -1,20 +1,10 @@ -import { expect } from 'chai'; +import { RJSFSchema } from '@rjsf/utils'; -import { createFormComponent, createSandbox } from './test_utils'; +import { createFormComponent } from './testUtils'; describe('const', () => { - let sandbox; - - beforeEach(() => { - sandbox = createSandbox(); - }); - - afterEach(() => { - sandbox.restore(); - }); - it('should render a schema that uses const with a string value', () => { - const schema = { + const schema: RJSFSchema = { type: 'object', properties: { foo: { const: 'bar' }, @@ -25,11 +15,11 @@ describe('const', () => { schema, }); - expect(node.querySelector('input#root_foo')).not.eql(null); + expect(node.querySelector('input#root_foo')).not.toBeNull(); }); it('should render a schema that uses const with a number value', () => { - const schema = { + const schema: RJSFSchema = { type: 'object', properties: { foo: { const: 123 }, @@ -40,11 +30,11 @@ describe('const', () => { schema, }); - expect(node.querySelector('input#root_foo')).not.eql(null); + expect(node.querySelector('input#root_foo')).not.toBeNull(); }); it('should render a schema that uses const with a boolean value', () => { - const schema = { + const schema: RJSFSchema = { type: 'object', properties: { foo: { const: true }, @@ -55,6 +45,6 @@ describe('const', () => { schema, }); - expect(node.querySelector("input#root_foo[type='checkbox']")).not.eql(null); + expect(node.querySelector("input#root_foo[type='checkbox']")).not.toBeNull(); }); }); diff --git a/packages/core/test/ifthenelse.test.js b/packages/core/test/ifthenelse.test.tsx similarity index 65% rename from packages/core/test/ifthenelse.test.js rename to packages/core/test/ifthenelse.test.tsx index 3cb0d09d24..0fbc9dfbee 100644 --- a/packages/core/test/ifthenelse.test.js +++ b/packages/core/test/ifthenelse.test.tsx @@ -1,77 +1,66 @@ -import { expect } from 'chai'; - -import { createFormComponent, createSandbox } from './test_utils'; - -describe('conditional items', () => { - let sandbox; - - beforeEach(() => { - sandbox = createSandbox(); - }); - - afterEach(() => { - sandbox.restore(); - }); - - const schema = { - type: 'object', - properties: { - street_address: { - type: 'string', - }, - country: { - enum: ['United States of America', 'Canada'], - }, - }, - if: { - properties: { country: { const: 'United States of America' } }, +import { RJSFSchema } from '@rjsf/utils'; + +import { createFormComponent } from './testUtils'; +const schema: RJSFSchema = { + type: 'object', + properties: { + street_address: { + type: 'string', }, - then: { - properties: { zipcode: { type: 'string' } }, + country: { + enum: ['United States of America', 'Canada'], }, - else: { - properties: { postal_code: { type: 'string' } }, + }, + if: { + properties: { country: { const: 'United States of America' } }, + }, + then: { + properties: { zipcode: { type: 'string' } }, + }, + else: { + properties: { postal_code: { type: 'string' } }, + }, +}; + +const schemaWithRef: RJSFSchema = { + type: 'object', + properties: { + country: { + enum: ['United States of America', 'Canada'], }, - }; - - const schemaWithRef = { - type: 'object', + }, + if: { properties: { country: { - enum: ['United States of America', 'Canada'], + const: 'United States of America', }, }, - if: { + }, + then: { + $ref: '#/definitions/us', + }, + else: { + $ref: '#/definitions/other', + }, + definitions: { + us: { properties: { - country: { - const: 'United States of America', + zip_code: { + type: 'string', }, }, }, - then: { - $ref: '#/definitions/us', - }, - else: { - $ref: '#/definitions/other', - }, - definitions: { - us: { - properties: { - zip_code: { - type: 'string', - }, - }, - }, - other: { - properties: { - postal_code: { - type: 'string', - }, + other: { + properties: { + postal_code: { + type: 'string', }, }, }, - }; + }, +}; +describe('conditional items', () => { it('should render then when condition is true', () => { const formData = { country: 'United States of America', @@ -82,8 +71,8 @@ describe('conditional items', () => { formData, }); - expect(node.querySelector('input[label=zipcode]')).not.eql(null); - expect(node.querySelector('input[label=postal_code]')).to.eql(null); + expect(node.querySelector('input[label=zipcode]')).not.toBeNull(); + expect(node.querySelector('input[label=postal_code]')).toBeNull(); }); it('should render else when condition is false', () => { @@ -96,8 +85,8 @@ describe('conditional items', () => { formData, }); - expect(node.querySelector('input[label=zipcode]')).to.eql(null); - expect(node.querySelector('input[label=postal_code]')).not.eql(null); + expect(node.querySelector('input[label=zipcode]')).toBeNull(); + expect(node.querySelector('input[label=postal_code]')).not.toBeNull(); }); it('should render control when data has not been filled in', () => { @@ -110,8 +99,8 @@ describe('conditional items', () => { // An empty formData will make the conditional evaluate to true because no properties are required in the if statement // Please see https://github.com/epoberezkin/ajv/issues/913 - expect(node.querySelector('input[label=zipcode]')).not.eql(null); - expect(node.querySelector('input[label=postal_code]')).to.eql(null); + expect(node.querySelector('input[label=zipcode]')).not.toBeNull(); + expect(node.querySelector('input[label=postal_code]')).toBeNull(); }); it('should render then when condition is true with reference', () => { @@ -124,8 +113,8 @@ describe('conditional items', () => { formData, }); - expect(node.querySelector('input[label=zip_code]')).not.eql(null); - expect(node.querySelector('input[label=postal_code]')).to.eql(null); + expect(node.querySelector('input[label=zip_code]')).not.toBeNull(); + expect(node.querySelector('input[label=postal_code]')).toBeNull(); }); it('should render else when condition is false with reference', () => { @@ -138,12 +127,12 @@ describe('conditional items', () => { formData, }); - expect(node.querySelector('input[label=zip_code]')).to.eql(null); - expect(node.querySelector('input[label=postal_code]')).not.eql(null); + expect(node.querySelector('input[label=zip_code]')).toBeNull(); + expect(node.querySelector('input[label=postal_code]')).not.toBeNull(); }); describe('allOf if then else', () => { - const schemaWithAllOf = { + const schemaWithAllOf: RJSFSchema = { type: 'object', properties: { street_address: { @@ -191,7 +180,7 @@ describe('conditional items', () => { formData, }); - expect(node.querySelector('input[label=zipcode]')).not.eql(null); + expect(node.querySelector('input[label=zipcode]')).not.toBeNull(); }); it('should render correctly when condition is false in allOf (1)', () => { @@ -204,7 +193,7 @@ describe('conditional items', () => { formData, }); - expect(node.querySelector('input[label=zipcode]')).to.eql(null); + expect(node.querySelector('input[label=zipcode]')).toBeNull(); }); it('should render correctly when condition is true in allof (2)', () => { @@ -217,9 +206,9 @@ describe('conditional items', () => { formData, }); - expect(node.querySelector('input[label=postcode]')).not.eql(null); - expect(node.querySelector('input[label=zipcode]')).to.eql(null); - expect(node.querySelector('input[label=telephone]')).to.eql(null); + expect(node.querySelector('input[label=postcode]')).not.toBeNull(); + expect(node.querySelector('input[label=zipcode]')).toBeNull(); + expect(node.querySelector('input[label=telephone]')).toBeNull(); }); it('should render correctly when condition is true in allof (3)', () => { @@ -232,12 +221,12 @@ describe('conditional items', () => { formData, }); - expect(node.querySelector('input[label=postcode]')).to.eql(null); - expect(node.querySelector('input[label=zipcode]')).to.eql(null); - expect(node.querySelector('input[label=telephone]')).not.eql(null); + expect(node.querySelector('input[label=postcode]')).toBeNull(); + expect(node.querySelector('input[label=zipcode]')).toBeNull(); + expect(node.querySelector('input[label=telephone]')).not.toBeNull(); }); - const schemaWithAllOfRef = { + const schemaWithAllOfRef: RJSFSchema = { type: 'object', properties: { street_address: { @@ -274,7 +263,7 @@ describe('conditional items', () => { formData, }); - expect(node.querySelector('input[label=postcode]')).not.eql(null); + expect(node.querySelector('input[label=postcode]')).not.toBeNull(); }); }); @@ -298,10 +287,10 @@ describe('conditional items', () => { }); // The zipcode field exists, but not as an additional property - expect(node.querySelector('input[label=zipcode]')).not.eql(null); - expect(node.querySelector('div.form-additional input[label=zipcode]')).to.eql(null); + expect(node.querySelector('input[label=zipcode]')).not.toBeNull(); + expect(node.querySelector('div.form-additional input[label=zipcode]')).toBeNull(); // The "otherKey" field exists as an additional property - expect(node.querySelector('div.form-additional input[label=otherKey]')).not.eql(null); + expect(node.querySelector('div.form-additional input[label=otherKey]')).not.toBeNull(); }); }); diff --git a/packages/core/test/setup-jest-env.js b/packages/core/test/setup-jest-env.ts similarity index 70% rename from packages/core/test/setup-jest-env.js rename to packages/core/test/setup-jest-env.ts index f94f1cc010..cca0103775 100644 --- a/packages/core/test/setup-jest-env.js +++ b/packages/core/test/setup-jest-env.ts @@ -5,7 +5,8 @@ import { setImmediate } from 'timers'; global.atob = require('atob'); // HTML debugging helper -global.d = function d(node) { +// @ts-expect-error TS7017 because we are avoiding an implicit any +global.d = function d(node: any) { console.log(html.prettyPrint(node.outerHTML, { indent_size: 2 })); }; diff --git a/packages/core/test/testUtils.tsx b/packages/core/test/testUtils.tsx new file mode 100644 index 0000000000..12f56a15d9 --- /dev/null +++ b/packages/core/test/testUtils.tsx @@ -0,0 +1,80 @@ +import type { ComponentType } from 'react'; +import { render, fireEvent, act } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { GenericObjectType, ValidatorType } from '@rjsf/utils'; +import validator from '@rjsf/validator-ajv8'; +import has from 'lodash/has'; + +import Form, { FormProps } from '../src'; + +export function renderNode(Component: ComponentType, props: GenericObjectType) { + const { container } = render(); + const node = container.firstElementChild; + return { node }; +} + +export function createComponent(Component: ComponentType, theProps: FormProps) { + const onChange = jest.fn(); + const onError = jest.fn(); + const onSubmit = jest.fn(); + const { container, rerender } = render( + , + ); + + const rerenderFunction = (newProps: Omit) => { + // For Form components, ensure validator is always passed + const propsWithValidator: FormProps = + Component === Form && !has(newProps, 'validator') ? { ...newProps, validator } : (newProps as FormProps); + return rerender(); + }; + const node = container.firstElementChild; + if (!node) { + throw new Error('node is not defined'); + } + + return { container, node, onChange, onError, onSubmit, rerender: rerenderFunction }; +} + +export function createFormComponent(props: Omit, v: ValidatorType = validator) { + return createComponent(Form, { validator: v, ...props }); +} + +// eslint-disable-next-line no-unused-vars +type CreatorFn = (creatorFn: typeof createFormComponent) => void; + +/* Run a group of tests with different combinations of omitExtraData and liveOmit as form props. + */ +export function describeRepeated(title: string, fn: CreatorFn) { + const formExtraPropsList: { omitExtraData: FormProps['omitExtraData']; liveOmit?: FormProps['liveOmit'] }[] = [ + { omitExtraData: false }, + { omitExtraData: true }, + { omitExtraData: true, liveOmit: true }, + { omitExtraData: true, liveOmit: 'onBlur' }, + ]; + for (const formExtraProps of formExtraPropsList) { + const createFormComponentFn = (props: Omit) => + createFormComponent({ ...props, ...formExtraProps }); + describe(`${title} ${JSON.stringify(formExtraProps)}`, () => fn(createFormComponentFn)); + } +} + +export function submitForm(node: Element) { + act(() => { + fireEvent.submit(node); + }); +} + +export function getSelectedOptionValue(selectNode: HTMLSelectElement) { + if (selectNode.type !== 'select-one') { + throw new Error(`invalid node provided, expected select got ${selectNode.type}`); + } + const value = selectNode.value; + const options = [...selectNode.options]; + const selectedOptions = options + .filter((option) => (Array.isArray(value) ? value.includes(option.value) : value === option.value)) + .map((option) => option.text); + if (!Array.isArray(value)) { + return selectedOptions[0]; + } + return selectedOptions; +} diff --git a/packages/core/test/validate.test.js b/packages/core/test/validate.test.tsx similarity index 69% rename from packages/core/test/validate.test.js rename to packages/core/test/validate.test.tsx index a5e9afd380..e65e4305fa 100644 --- a/packages/core/test/validate.test.js +++ b/packages/core/test/validate.test.tsx @@ -1,25 +1,15 @@ -import { expect } from 'chai'; -import sinon from 'sinon'; import { fireEvent, act } from '@testing-library/react'; - -import { createFormComponent, submitForm } from './test_utils'; +import { ErrorListProps, FormValidation, GenericObjectType, RJSFSchema } from '@rjsf/utils'; import { customizeValidator as customizeV8Validator } from '@rjsf/validator-ajv8'; +import { FormProps } from '../src'; +import { createFormComponent, submitForm } from './testUtils'; + describe('Validation', () => { describe('Form integration, v8 validator', () => { - let sandbox; - - beforeEach(() => { - sandbox = sinon.createSandbox(); - }); - - afterEach(() => { - sandbox.restore(); - }); - describe('JSONSchema validation', () => { describe('Required fields', () => { - const schema = { + const schema: RJSFSchema = { type: 'object', required: ['foo'], properties: { @@ -28,7 +18,8 @@ describe('Validation', () => { }, }; - let onError, node; + let onError: jest.Mock; + let node: Element; beforeEach(() => { const compInfo = createFormComponent({ schema, @@ -42,7 +33,7 @@ describe('Validation', () => { }); it('should trigger onError call', () => { - sinon.assert.calledWithMatch(onError.lastCall, [ + expect(onError).toHaveBeenLastCalledWith([ { message: "must have required property 'foo'", name: 'required', @@ -56,13 +47,13 @@ describe('Validation', () => { }); it('should render errors', () => { - expect(node.querySelectorAll('.errors li')).to.have.length.of(1); - expect(node.querySelector('.errors li').textContent).eql("must have required property 'foo'"); + expect(node.querySelectorAll('.errors li')).toHaveLength(1); + expect(node.querySelector('.errors li')).toHaveTextContent("must have required property 'foo'"); }); }); describe('Min length', () => { - const schema = { + const schema: RJSFSchema = { type: 'object', required: ['foo'], properties: { @@ -73,29 +64,29 @@ describe('Validation', () => { }, }; - let node, onError; + let onError: jest.Mock; + let node: Element; beforeEach(() => { - onError = sandbox.spy(); const compInfo = createFormComponent({ schema, formData: { foo: '123456789', }, - onError, }); node = compInfo.node; + onError = compInfo.onError; submitForm(node); }); it('should render errors', () => { - expect(node.querySelectorAll('.errors li')).to.have.length.of(1); - expect(node.querySelector('.errors li').textContent).eql('.foo must NOT have fewer than 10 characters'); + expect(node.querySelectorAll('.errors li')).toHaveLength(1); + expect(node.querySelector('.errors li')).toHaveTextContent('.foo must NOT have fewer than 10 characters'); }); it('should trigger the onError handler', () => { - sinon.assert.calledWithMatch(onError.lastCall, [ + expect(onError).toHaveBeenLastCalledWith([ { message: 'must NOT have fewer than 10 characters', name: 'minLength', @@ -112,10 +103,10 @@ describe('Validation', () => { describe('Custom Form validation', () => { it('should validate a simple string value', () => { - const schema = { type: 'string' }; + const schema: RJSFSchema = { type: 'string' }; const formData = 'a'; - function customValidate(formData, errors) { + function customValidate(formData: FormProps['formData'], errors: FormValidation) { if (formData !== 'hello') { errors.addError('Invalid'); } @@ -129,14 +120,14 @@ describe('Validation', () => { }); submitForm(node); - sinon.assert.calledWithMatch(onError.lastCall, [{ property: '.', message: 'Invalid', stack: '. Invalid' }]); + expect(onError).toHaveBeenLastCalledWith([{ property: '.', message: 'Invalid', stack: '. Invalid' }]); }); it('should live validate a simple string value when liveValidate is set to true', () => { - const schema = { type: 'string' }; + const schema: RJSFSchema = { type: 'string' }; const formData = 'a'; - function customValidate(formData, errors) { + function customValidate(formData: FormProps['formData'], errors: FormValidation) { if (formData !== 'hello') { errors.addError('Invalid'); } @@ -151,28 +142,27 @@ describe('Validation', () => { }); act(() => { - fireEvent.change(node.querySelector('input'), { + fireEvent.change(node.querySelector('input')!, { target: { value: '1234' }, }); }); - sinon.assert.calledWithMatch( - onChange.lastCall, - { + expect(onChange).toHaveBeenLastCalledWith( + expect.objectContaining({ errorSchema: { __errors: ['Invalid'] }, errors: [{ property: '.', message: 'Invalid', stack: '. Invalid' }], formData: '1234', - }, + }), 'root', ); }); it('should submit form on valid data', () => { - const schema = { type: 'string' }; + const schema: RJSFSchema = { type: 'string' }; const formData = 'hello'; - const onSubmit = sandbox.spy(); + const onSubmit = jest.fn(); - function customValidate(formData, errors) { + function customValidate(formData: FormProps['formData'], errors: FormValidation) { if (formData !== 'hello') { errors.addError('Invalid'); } @@ -188,16 +178,16 @@ describe('Validation', () => { submitForm(node); - sinon.assert.called(onSubmit); + expect(onSubmit).toHaveBeenCalled(); }); it('should prevent form submission on invalid data', () => { - const schema = { type: 'string' }; + const schema: RJSFSchema = { type: 'string' }; const formData = 'a'; - const onSubmit = sandbox.spy(); - const onError = sandbox.spy(); + const onSubmit = jest.fn(); + const onError = jest.fn(); - function customValidate(formData, errors) { + function customValidate(formData: FormProps['formData'], errors: FormValidation) { if (formData !== 'hello') { errors.addError('Invalid'); } @@ -214,12 +204,12 @@ describe('Validation', () => { submitForm(node); - sinon.assert.notCalled(onSubmit); - sinon.assert.called(onError); + expect(onSubmit).not.toHaveBeenCalled(); + expect(onError).toHaveBeenCalled(); }); it('should validate a simple object', () => { - const schema = { + const schema: RJSFSchema = { type: 'object', properties: { pass1: { type: 'string', minLength: 3 }, @@ -229,10 +219,10 @@ describe('Validation', () => { const formData = { pass1: 'aaa', pass2: 'b' }; - function customValidate(formData, errors) { + function customValidate(formData: FormProps['formData'], errors: FormValidation) { const { pass1, pass2 } = formData; if (pass1 !== pass2) { - errors.pass2.addError("Passwords don't match"); + (errors.pass2 as FormValidation).addError("Passwords don't match"); } return errors; } @@ -243,7 +233,7 @@ describe('Validation', () => { formData, }); submitForm(node); - sinon.assert.calledWithMatch(onError.lastCall, [ + expect(onError).toHaveBeenLastCalledWith([ { message: 'must NOT have fewer than 3 characters', name: 'minLength', @@ -262,7 +252,7 @@ describe('Validation', () => { }); it('should validate an array of object', () => { - const schema = { + const schema: RJSFSchema = { type: 'array', items: { type: 'object', @@ -278,10 +268,11 @@ describe('Validation', () => { { pass1: 'a', pass2: 'a' }, ]; - function customValidate(formData, errors) { - formData.forEach(({ pass1, pass2 }, i) => { + function customValidate(formData: FormProps['formData'], errors: FormValidation) { + formData.forEach(({ pass1, pass2 }: GenericObjectType, i: number) => { + console.log(pass1, pass2, errors); if (pass1 !== pass2) { - errors[i].pass2.addError("Passwords don't match"); + (errors as GenericObjectType)[i].pass2.addError("Passwords don't match"); } }); return errors; @@ -294,7 +285,7 @@ describe('Validation', () => { }); submitForm(node); - sinon.assert.calledWithMatch(onError.lastCall, [ + expect(onError).toHaveBeenLastCalledWith([ { property: '.0.pass2', message: "Passwords don't match", @@ -304,7 +295,7 @@ describe('Validation', () => { }); it('should validate a simple array', () => { - const schema = { + const schema: RJSFSchema = { type: 'array', items: { type: 'string', @@ -313,7 +304,7 @@ describe('Validation', () => { const formData = ['aaa', 'bbb', 'ccc']; - function customValidate(formData, errors) { + function customValidate(formData: FormProps['formData'], errors: FormValidation) { if (formData.indexOf('bbb') !== -1) { errors.addError('Forbidden value: bbb'); } @@ -326,7 +317,7 @@ describe('Validation', () => { formData, }); submitForm(node); - sinon.assert.calledWithMatch(onError.lastCall, [ + expect(onError).toHaveBeenLastCalledWith([ { property: '.', message: 'Forbidden value: bbb', @@ -338,7 +329,7 @@ describe('Validation', () => { describe('showErrorList prop validation', () => { describe('Required fields', () => { - const schema = { + const schema: RJSFSchema = { type: 'object', required: ['foo'], properties: { @@ -347,7 +338,8 @@ describe('Validation', () => { }, }; - let node, onError; + let onError: jest.Mock; + let node: Element; beforeEach(() => { const compInfo = createFormComponent({ schema, @@ -363,11 +355,11 @@ describe('Validation', () => { }); it('should not render error list if showErrorList prop true', () => { - expect(node.querySelectorAll('.errors li')).to.have.length.of(0); + expect(node.querySelectorAll('.errors li')).toHaveLength(0); }); it('should trigger onError call', () => { - sinon.assert.calledWithMatch(onError.lastCall, [ + expect(onError).toHaveBeenLastCalledWith([ { message: "must have required property 'foo'", name: 'required', @@ -383,7 +375,7 @@ describe('Validation', () => { }); describe('Custom ErrorList', () => { - const schema = { + const schema: RJSFSchema = { type: 'string', minLength: 1, }; @@ -402,12 +394,12 @@ describe('Validation', () => { registry: { formContext: { className }, }, - }) => ( + }: ErrorListProps) => (
{errors.length} custom
-
{errorSchema.__errors[0]}
+
{errorSchema.__errors?.[0]}
{schema.type}
-
{uiSchema.foo}
+
{uiSchema?.foo}
); @@ -424,24 +416,25 @@ describe('Validation', () => { // trigger the errors by submitting the form since initial render no longer shows them submitForm(node); - expect(node.querySelectorAll('.CustomErrorList')).to.have.length.of(1); - expect(node.querySelector('.CustomErrorList').textContent).eql('1 custom'); - expect(node.querySelectorAll('.ErrorSchema')).to.have.length.of(1); - expect(node.querySelector('.ErrorSchema').textContent).eql('must be string'); - expect(node.querySelectorAll('.Schema')).to.have.length.of(1); - expect(node.querySelector('.Schema').textContent).eql('string'); - expect(node.querySelectorAll('.UiSchema')).to.have.length.of(1); - expect(node.querySelector('.UiSchema').textContent).eql('bar'); - expect(node.querySelectorAll('.foo')).to.have.length.of(1); + expect(node.querySelectorAll('.CustomErrorList')).toHaveLength(1); + expect(node.querySelector('.CustomErrorList')).toHaveTextContent('1 custom'); + expect(node.querySelectorAll('.ErrorSchema')).toHaveLength(1); + expect(node.querySelector('.ErrorSchema')).toHaveTextContent('must be string'); + expect(node.querySelectorAll('.Schema')).toHaveLength(1); + expect(node.querySelector('.Schema')).toHaveTextContent('string'); + expect(node.querySelectorAll('.UiSchema')).toHaveLength(1); + expect(node.querySelector('.UiSchema')).toHaveTextContent('bar'); + expect(node.querySelectorAll('.foo')).toHaveLength(1); }); }); describe('Custom meta schema', () => { - let onError, node; + let onError: jest.Mock; + let node: Element; const formData = { datasetId: 'no err', }; - const schema = { + const schema: RJSFSchema = { $ref: '#/definitions/Dataset', $schema: 'http://json-schema.org/draft-06/schema#', definitions: { @@ -462,19 +455,21 @@ describe('Validation', () => { const validator = customizeV8Validator({ additionalMetaSchemas: [require('ajv/lib/refs/json-schema-draft-06.json')], }); - const withMetaSchema = createFormComponent({ - schema, - formData, - liveValidate: true, + const withMetaSchema = createFormComponent( + { + schema, + formData, + liveValidate: true, + }, validator, - }); + ); node = withMetaSchema.node; onError = withMetaSchema.onError; submitForm(node); }); it('should be used to validate schema', () => { - expect(node.querySelectorAll('.errors li')).to.have.length.of(1); - sinon.assert.calledWithMatch(onError.lastCall, [ + expect(node.querySelectorAll('.errors li')).toHaveLength(1); + expect(onError).toHaveBeenLastCalledWith([ { message: 'must match pattern "\\d+"', name: 'pattern', @@ -485,15 +480,15 @@ describe('Validation', () => { title: '', }, ]); - onError.resetHistory(); + onError.mockClear(); act(() => { - fireEvent.change(node.querySelector('input'), { + fireEvent.change(node.querySelector('input')!, { target: { value: '1234' }, }); }); - expect(node.querySelectorAll('.errors li')).to.have.length.of(0); - sinon.assert.notCalled(onError); + expect(node.querySelectorAll('.errors li')).toHaveLength(0); + expect(onError).not.toHaveBeenCalled(); }); }); }); diff --git a/packages/core/test/withTheme.test.jsx b/packages/core/test/withTheme.test.tsx similarity index 69% rename from packages/core/test/withTheme.test.jsx rename to packages/core/test/withTheme.test.tsx index fa79d3b8f3..e848815222 100644 --- a/packages/core/test/withTheme.test.jsx +++ b/packages/core/test/withTheme.test.tsx @@ -1,30 +1,20 @@ -import { expect } from 'chai'; import { Component, createRef } from 'react'; +import { RJSFSchema } from '@rjsf/utils'; import validator from '@rjsf/validator-ajv8'; -import { withTheme } from '../src'; -import { createComponent, createSandbox } from './test_utils'; +import Form, { FormProps, ThemeProps, withTheme } from '../src'; +import { createComponent } from './testUtils'; -const WrapperClassComponent = (...args) => { - return class extends Component { +function WrapperClassComponent(props: ThemeProps) { + return class extends Component { render() { - const Cmp = withTheme(...args); + const Cmp = withTheme(props); return ; } }; -}; +} describe('withTheme', () => { - let sandbox; - - beforeEach(() => { - sandbox = createSandbox(); - }); - - afterEach(() => { - sandbox.restore(); - }); - describe('With fields', () => { it('should use the withTheme field', () => { const fields = { @@ -32,7 +22,7 @@ describe('withTheme', () => { return
; }, }; - const schema = { + const schema: RJSFSchema = { type: 'object', properties: { fieldA: { @@ -44,12 +34,12 @@ describe('withTheme', () => { }, }; const uiSchema = {}; - let { node } = createComponent(WrapperClassComponent({ fields }), { + const { node } = createComponent(WrapperClassComponent({ fields }), { schema, uiSchema, validator, }); - expect(node.querySelectorAll('.string-field')).to.have.length.of(2); + expect(node.querySelectorAll('.string-field')).toHaveLength(2); }); it('should use withTheme field and the user defined field', () => { @@ -63,7 +53,7 @@ describe('withTheme', () => { return
; }, }; - const schema = { + const schema: RJSFSchema = { type: 'object', properties: { fieldA: { @@ -75,14 +65,14 @@ describe('withTheme', () => { }, }; const uiSchema = {}; - let { node } = createComponent(WrapperClassComponent({ fields: themeFields }), { + const { node } = createComponent(WrapperClassComponent({ fields: themeFields }), { schema, uiSchema, fields: userFields, validator, }); - expect(node.querySelectorAll('.string-field')).to.have.length.of(1); - expect(node.querySelectorAll('.number-field')).to.have.length.of(1); + expect(node.querySelectorAll('.string-field')).toHaveLength(1); + expect(node.querySelectorAll('.number-field')).toHaveLength(1); }); it('should use only the user defined field', () => { @@ -96,7 +86,7 @@ describe('withTheme', () => { return
; }, }; - const schema = { + const schema: RJSFSchema = { type: 'object', properties: { fieldA: { @@ -108,14 +98,14 @@ describe('withTheme', () => { }, }; const uiSchema = {}; - let { node } = createComponent(WrapperClassComponent({ fields: themeFields }), { + const { node } = createComponent(WrapperClassComponent({ fields: themeFields }), { schema, uiSchema, fields: userFields, validator, }); - expect(node.querySelectorAll('.string-field')).to.have.length.of(0); - expect(node.querySelectorAll('.form-control')).to.have.length.of(2); + expect(node.querySelectorAll('.string-field')).toHaveLength(0); + expect(node.querySelectorAll('.form-control')).toHaveLength(2); }); }); @@ -124,16 +114,16 @@ describe('withTheme', () => { const widgets = { TextWidget: () =>
, }; - const schema = { + const schema: RJSFSchema = { type: 'string', }; const uiSchema = {}; - let { node } = createComponent(WrapperClassComponent({ widgets }), { + const { node } = createComponent(WrapperClassComponent({ widgets }), { schema, uiSchema, validator, }); - expect(node.querySelectorAll('#test')).to.have.length.of(1); + expect(node.querySelectorAll('#test')).toHaveLength(1); }); it('should use the withTheme widget as well as user defined widget', () => { @@ -143,7 +133,7 @@ describe('withTheme', () => { const userWidgets = { DateWidget: () =>
, }; - const schema = { + const schema: RJSFSchema = { type: 'object', properties: { fieldA: { @@ -156,14 +146,14 @@ describe('withTheme', () => { }, }; const uiSchema = {}; - let { node } = createComponent(WrapperClassComponent({ widgets: themeWidgets }), { + const { node } = createComponent(WrapperClassComponent({ widgets: themeWidgets }), { schema, uiSchema, widgets: userWidgets, validator, }); - expect(node.querySelectorAll('#test-theme-widget')).to.have.length.of(1); - expect(node.querySelectorAll('#test-user-widget')).to.have.length.of(1); + expect(node.querySelectorAll('#test-theme-widget')).toHaveLength(1); + expect(node.querySelectorAll('#test-user-widget')).toHaveLength(1); }); it('should use only the user defined widget', () => { @@ -173,7 +163,7 @@ describe('withTheme', () => { const userWidgets = { TextWidget: () =>
, }; - const schema = { + const schema: RJSFSchema = { type: 'object', properties: { fieldA: { @@ -182,14 +172,14 @@ describe('withTheme', () => { }, }; const uiSchema = {}; - let { node } = createComponent(WrapperClassComponent({ widgets: themeWidgets }), { + const { node } = createComponent(WrapperClassComponent({ widgets: themeWidgets }), { schema, uiSchema, widgets: userWidgets, validator, }); - expect(node.querySelectorAll('#test-theme-widget')).to.have.length.of(0); - expect(node.querySelectorAll('#test-user-widget')).to.have.length.of(1); + expect(node.querySelectorAll('#test-theme-widget')).toHaveLength(0); + expect(node.querySelectorAll('#test-user-widget')).toHaveLength(1); }); }); @@ -200,7 +190,7 @@ describe('withTheme', () => { return
; }, }; - const schema = { + const schema: RJSFSchema = { type: 'object', properties: { fieldA: { @@ -212,12 +202,12 @@ describe('withTheme', () => { }, }; const uiSchema = {}; - let { node } = createComponent(WrapperClassComponent({ templates: themeTemplates }), { + const { node } = createComponent(WrapperClassComponent({ templates: themeTemplates }), { schema, uiSchema, validator, }); - expect(node.querySelectorAll('.with-theme-field-template')).to.have.length.of(1); + expect(node.querySelectorAll('.with-theme-field-template')).toHaveLength(1); }); it('should use only the user defined template', () => { @@ -232,17 +222,17 @@ describe('withTheme', () => { }, }; - const schema = { + const schema: RJSFSchema = { type: 'object', properties: { foo: { type: 'string' }, bar: { type: 'string' } }, }; - let { node } = createComponent(WrapperClassComponent({ templates: themeTemplates }), { + const { node } = createComponent(WrapperClassComponent({ templates: themeTemplates }), { schema, templates: userTemplates, validator, }); - expect(node.querySelectorAll('.with-theme-field-template')).to.have.length.of(0); - expect(node.querySelectorAll('.user-field-template')).to.have.length.of(1); + expect(node.querySelectorAll('.with-theme-field-template')).toHaveLength(0); + expect(node.querySelectorAll('.user-field-template')).toHaveLength(1); }); it('should use the withTheme submit button template', () => { @@ -253,7 +243,7 @@ describe('withTheme', () => { }, }, }; - const schema = { + const schema: RJSFSchema = { type: 'object', properties: { fieldA: { @@ -265,12 +255,12 @@ describe('withTheme', () => { }, }; const uiSchema = {}; - let { node } = createComponent(WrapperClassComponent({ templates: themeTemplates }), { + const { node } = createComponent(WrapperClassComponent({ templates: themeTemplates }), { schema, uiSchema, validator, }); - expect(node.querySelectorAll('.with-theme-button-template')).to.have.length.of(1); + expect(node.querySelectorAll('.with-theme-button-template')).toHaveLength(1); }); it('should use only the user defined submit button', () => { @@ -289,23 +279,23 @@ describe('withTheme', () => { }, }; - const schema = { + const schema: RJSFSchema = { type: 'object', properties: { foo: { type: 'string' }, bar: { type: 'string' } }, }; - let { node } = createComponent(WrapperClassComponent({ templates: themeTemplates }), { + const { node } = createComponent(WrapperClassComponent({ templates: themeTemplates }), { schema, templates: userTemplates, validator, }); - expect(node.querySelectorAll('.with-theme-button-template')).to.have.length.of(0); - expect(node.querySelectorAll('.user-button-template')).to.have.length.of(1); + expect(node.querySelectorAll('.with-theme-button-template')).toHaveLength(0); + expect(node.querySelectorAll('.user-button-template')).toHaveLength(1); }); }); it('should forward the ref', () => { - const ref = createRef(); - const schema = {}; + const ref = createRef(); + const schema: RJSFSchema = {}; const uiSchema = {}; createComponent(withTheme({}), { @@ -315,6 +305,6 @@ describe('withTheme', () => { ref, }); - expect(ref.current.submit).not.eql(undefined); + expect(ref.current?.submit).not.toBeUndefined(); }); });