From 7b22c286fc8b99c097e8ba43a9b6f8bfcfb7ec53 Mon Sep 17 00:00:00 2001 From: Heath Chiavettone Date: Mon, 17 Oct 2022 18:13:46 -0700 Subject: [PATCH] fix: #3189 by allowing the ajv 8 validator to alternate schema Ajv classes Fix #3189 by extending the `@rjsf/validator-ajv8` to support the `Ajv2019` and `Ajv2020` classes - Updated the `CustomValidatorOptionsType` to add a new `AjvClass` option - Updated the `createAjvInstance()` function to use the `AjvClass` option is provided to create the `Ajv` class instance, falling back to the default `Ajv` - Updated the `AJV8Validator` to extract the `AjvClass` from the options and pass it to the `createAjvInstance()` function - Updated the tests to also test the `Ajv2019` and `Ajv2020` class instances - Updated the `validation.md` file to document the new `AjvClass` options, switching the examples over to `tsx` - Updated the `CHANGELOG.md` file accordingly --- CHANGELOG.md | 2 + docs/usage/validation.md | 109 +- .../validator-ajv8/src/createAjvInstance.ts | 5 +- packages/validator-ajv8/src/types.ts | 4 +- packages/validator-ajv8/src/validator.ts | 4 +- .../test/createAjvInstance.test.ts | 47 + .../test/utilsTests/schema.test.ts | 28 + .../validator-ajv8/test/validator.test.ts | 1088 ++++++++++++++++- 8 files changed, 1249 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac0549d27b..6648f09a1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,9 +34,11 @@ should change the heading of the (upcoming) version to include a major version b ## @rjsf/validator-ajv8 - Updated the typing to add the new `S extends StrictRJSFSchema = RJSFSchema` generic and fixed up type casts +- Added the `AjvClass` prop to the `CustomValidatorOptionsType` to support using the `Ajv2019` or `Ajv2020` class implementation instead of the default `Ajv` class; fixing [#3189](https://github.com/rjsf-team/react-jsonschema-form/issues/3189) ## Dev / docs / playground - Updated the `5.x upgrade guide` to document the new `StrictRJSFSchema` and `S` generic +- Updated the `validation` guide to document the new `AjvClass` prop on `CustomValidatorOptionsType` # 5.0.0-beta.11 diff --git a/docs/usage/validation.md b/docs/usage/validation.md index f5e9bbad6e..3c235c415d 100644 --- a/docs/usage/validation.md +++ b/docs/usage/validation.md @@ -19,10 +19,11 @@ You can enable live form data validation by passing a `liveValidate` prop to the Be warned that this is an expensive strategy, with possibly strong impact on performances. -```jsx +```tsx +import { RJSFSchema } from "@rjsf/utils"; import validator from "@rjsf/validator-ajv6"; -const schema = { +const schema: StrictRJSFSchema = { type: ["string"], const: "test" }; @@ -41,19 +42,20 @@ Add a `ref` to your `Form` component and call the `validateForm()` method to val The `validateForm()` method returns true if the form is valid, false otherwise. If you have provided an `onError` callback it will be called with the list of errors when the `validatorForm()` method returns false. -```jsx +```tsx import { createRef } from "react" +import { RJSFSchema } from "@rjsf/utils"; import validator from "@rjsf/validator-ajv6"; const formRef = createRef(); const onError = (errors) => alert(errors); -const schema = { +const schema: RJSFSchema = { type: "string" }; render(( -
+ ), document.getElementById("app")); if (formRef.current.validateForm()) { @@ -65,10 +67,11 @@ if (formRef.current.validateForm()) { By default, the form uses HTML5 validation. This may cause unintuitive results because the HTML5 validation errors (such as when a field is `required`) may be displayed before the form is submitted, and thus these errors will display differently from the react-jsonschema-form validation errors. You can turn off HTML validation by setting the `noHtml5Validate` to `true`. -```jsx +```tsx +import { RJSFSchema } from "@rjsf/utils"; import validator from "@rjsf/validator-ajv6"; -const schema = { +const schema: RJSFSchema = { type: "object", properties: { name: { @@ -90,7 +93,8 @@ Form data is always validated against the JSON schema. But it is possible to define your own custom validation rules that will run in addition to (and after) the `validator` implementation. This is especially useful when the validation depends on several interdependent fields. -```jsx +```tsx +import { RJSFSchema } from "@rjsf/utils"; import validator from "@rjsf/validator-ajv6"; function customValidate(formData, errors) { @@ -100,7 +104,7 @@ function customValidate(formData, errors) { return errors; } -const schema = { +const schema: RJSFSchema = { type: "object", properties: { pass1: {type: "string", minLength: 3}, @@ -123,7 +127,8 @@ render(( Validation error messages are provided by the JSON Schema validation by default. If you need to change these messages or make any other modifications to the errors from the JSON Schema validation, you can define a transform function that receives the list of JSON Schema errors and returns a new list. -```jsx +```tsx +import { RJSFSchema } from "@rjsf/utils"; import validator from "@rjsf/validator-ajv6"; function transformErrors(errors) { @@ -135,7 +140,7 @@ function transformErrors(errors) { }); } -const schema = { +const schema: RJSFSchema = { type: "object", properties: { onlyNumbersString: {type: "string", pattern: "^\\d*$"}, @@ -168,6 +173,7 @@ This list is the form global error list that appears at the top of your forms. An error list template is basically a React stateless component being passed errors as props, so you can render them as you like: ```tsx +import { RJSFSchema, ErrorListProps } from "@rjsf/utils"; import validator from "@rjsf/validator-ajv6"; function ErrorListTemplate(props: ErrorListProps) { @@ -186,7 +192,7 @@ function ErrorListTemplate(props: ErrorListProps) { ); } -const schema = { +const schema: RJSFSchema = { type: "string", const: "test" }; @@ -242,18 +248,19 @@ To support additional meta schemas, you can create and pass to the `Form` compon The `additionalMetaSchemas` prop allows you to validate the form data against one (or more than one) JSON Schema meta schema, for example, JSON Schema draft-04. You can import a meta schema as follows: -```jsx +```tsx const metaSchemaDraft04 = require("ajv/lib/refs/json-schema-draft-04.json"); ``` In this example `schema` passed as props to `Form` component can be validated against draft-07 (default) and by draft-04 (added), depending on the value of `$schema` attribute. -```jsx +```tsx +import { RJSFSchema } from "@rjsf/utils"; import { customizeValidator } from '@rjsf/validator-ajv6'; const validator = customizeValidator({ additionalMetaSchemas: [metaSchemaDraft04] }); -const schema = { +const schema: RJSFSchema = { "$schema": "http://json-schema.org/draft-04/schema#", type: "string" }; @@ -267,10 +274,11 @@ return (); react-jsonschema-form adds two formats, `color` and `data-url`, to support certain [alternative widgets](../usage/widgets.md). To add formats of your own, you can create and pass to the `Form` component a customized `@rjsf/validator-ajv6`: -```jsx +```tsx +import { RJSFSchema } from "@rjsf/utils"; import { customizeValidator } from '@rjsf/validator-ajv6'; -const schema = { +const schema: RJSFSchema = { type: 'string', format: 'phone-us' }; @@ -294,10 +302,11 @@ Handling async errors is an important part of many applications. Support for thi For example, a request could be made to some backend when the user submits the form. If that request fails, the errors returned by the backend should be formatted like in the following example. -```jsx +```tsx +import { RJSFSchema, ErrorSchema } from "@rjsf/utils"; import validator from "@rjsf/validator-ajv6"; -const schema = { +const schema: RJSFSchema = { type: "object", properties: { foo: { @@ -314,7 +323,7 @@ const schema = { } }; -const extraErrors = { +const extraErrors: ErrorSchema = { foo: { __errors: ["some error that got added as a prop"], }, @@ -337,10 +346,11 @@ An important note is that these errors are "display only" and will not block the In version 5, with the advent of the decoupling of the validation implementation from the `Form`, it is now possible to provide additional options to the `ajv6` instance used within `@rjsf/validator-ajv6`. For instance, if you need more information from `ajv` about errors via the `verbose` option, now you can turn it on! -```jsx +```tsx +import { RJSFSchema } from "@rjsf/utils"; import { customizeValidator } from '@rjsf/validator-ajv6'; -const schema = { +const schema: RJSFSchema = { type: 'string', format: 'phone-us' }; @@ -367,16 +377,18 @@ So if your schema is using an older format, you have to either upgrade it or sti The `ajvOptionsOverrides` for the Ajv 8 validator are the ones supported by that version and not the Ajv 6 validator. Second, the data formats previously provided in Ajv 6 now need to be added explicitly using the `ajv-formats` package. A new `ajvFormatOptions` option is available on the `customizeValidator()` API to be able to configure this. +Additionally, a new `AjvClass` option is available on the `customizeValidator()` API to support using one of the other [JSON schema versions](https://ajv.js.org/json-schema.html#json-schema-versions) provided by Ajv 8 besides the `draft-07` default. Finally, the Ajv 8 validator supports the localization of error messages. ### ajvFormatOptions By default, ALL formats are being added to the default `@rjsf/validator-ajv8` that you get when you import it. -```jsx +```tsx +import { RJSFSchema } from "@rjsf/utils"; import validator from '@rjsf/validator-ajv8'; -const schema = { +const schema: RJSFSchema = { type: 'string', format: 'email' }; @@ -388,10 +400,11 @@ render(( If you don't actually need any of the [ajv-formats](https://github.com/ajv-validator/ajv-formats#formats) and want to reduce the memory footprint, then you can turn it off as follows: -```jsx +```tsx +import { RJSFSchema } from "@rjsf/utils"; import { customizeValidator } from '@rjsf/validator-ajv8'; -const schema = { +const schema: RJSFSchema = { type: 'string', }; @@ -404,10 +417,11 @@ render(( If you only need some of them, you can pass any of the [options](https://github.com/ajv-validator/ajv-formats#options) to the formatter: -```jsx +```tsx +import { RJSFSchema } from "@rjsf/utils"; import { customizeValidator } from '@rjsf/validator-ajv8'; -const schema = { +const schema: RJSFSchema = { type: 'string', format: 'date' }; @@ -421,6 +435,30 @@ render(( ), document.getElementById("app")); ``` +### AjvClass +By default, the `@rjsf/validator-ajv8` uses the `draft-07` schema version. +It is possible to use one of the other version it supports, like `draft-2019-09` or `draft-2020-12`. +NOTE: `draft-2020-12` has breaking changes and hasn't been fully tested with `@rjsf`. + +```tsx +import { RJSFSchema } from "@rjsf/utils"; +import { customizeValidator } from '@rjsf/validator-ajv8'; +import Ajv2019 from "ajv/dist/2019"; + +const schema: RJSFSchema = { + type: 'string', + format: 'date' +}; + +const validator = customizeValidator({ AjvClass: Ajv2019 }); +// or +// const validator = customizeValidator({ AjvClass: Ajv2020 }); + +render(( + +), document.getElementById("app")); +``` + ### Localization (L12n) support The Ajv 8 validator supports the localization of error messages using [ajv-i18n](https://github.com/ajv-validator/ajv-i18n). @@ -439,11 +477,12 @@ NOTE: The `ajv-i18n` validators implement the `Localizer` interface. Using a specific locale while including all of `ajv-i18n`: - ```jsx + ```tsx +import { RJSFSchema } from "@rjsf/utils"; import { customizeValidator } from '@rjsf/validator-ajv8'; import localizer from "ajv-i18n"; -const schema = { +const schema: RJSFSchema = { type: 'string', }; @@ -456,11 +495,12 @@ render(( Using a specific locale minimizing the bundle size - ```jsx + ```tsx +import { RJSFSchema } from "@rjsf/utils"; import { customizeValidator } from '@rjsf/validator-ajv8'; import spanishLocalizer from "ajv-i18n/localize/es"; -const schema = { +const schema: RJSFSchema = { type: 'string', }; @@ -474,6 +514,7 @@ render(( An example of a custom `Localizer` implementation: ```tsx +import { RJSFSchema } from "@rjsf/utils"; import { customizeValidator } from '@rjsf/validator-ajv8'; import { ErrorObject } from "ajv"; @@ -499,6 +540,10 @@ function localize_ru(errors: null | ErrorObject[] = []) { }) } +const schema: RJSFSchema = { + type: 'string', +}; + const validator = customizeValidator({}, localize_ru); render(( diff --git a/packages/validator-ajv8/src/createAjvInstance.ts b/packages/validator-ajv8/src/createAjvInstance.ts index 9445fdcc9b..e5f0cc40c3 100644 --- a/packages/validator-ajv8/src/createAjvInstance.ts +++ b/packages/validator-ajv8/src/createAjvInstance.ts @@ -31,9 +31,10 @@ export default function createAjvInstance( additionalMetaSchemas?: CustomValidatorOptionsType["additionalMetaSchemas"], customFormats?: CustomValidatorOptionsType["customFormats"], ajvOptionsOverrides: CustomValidatorOptionsType["ajvOptionsOverrides"] = {}, - ajvFormatOptions?: FormatsPluginOptions | false + ajvFormatOptions?: FormatsPluginOptions | false, + AjvClass: typeof Ajv = Ajv ) { - const ajv = new Ajv({ ...AJV_CONFIG, ...ajvOptionsOverrides }); + const ajv = new AjvClass({ ...AJV_CONFIG, ...ajvOptionsOverrides }); if (typeof ajvFormatOptions !== "boolean") { addFormats(ajv, ajvFormatOptions); } diff --git a/packages/validator-ajv8/src/types.ts b/packages/validator-ajv8/src/types.ts index 4f024591b1..633252b261 100644 --- a/packages/validator-ajv8/src/types.ts +++ b/packages/validator-ajv8/src/types.ts @@ -1,4 +1,4 @@ -import { Options, ErrorObject } from "ajv"; +import Ajv, { Options, ErrorObject } from "ajv"; import { FormatsPluginOptions } from "ajv-formats"; /** The type describing how to customize the AJV6 validator @@ -14,6 +14,8 @@ export interface CustomValidatorOptionsType { ajvOptionsOverrides?: Options; /** The `ajv-format` options to use when adding formats to `ajv`; pass `false` to disable it */ ajvFormatOptions?: FormatsPluginOptions | false; + /** The AJV class to construct */ + AjvClass?: typeof Ajv; } /** The type describing a function that takes a list of Ajv `ErrorObject`s and localizes them diff --git a/packages/validator-ajv8/src/validator.ts b/packages/validator-ajv8/src/validator.ts index f559ac657c..c601d0a6ac 100644 --- a/packages/validator-ajv8/src/validator.ts +++ b/packages/validator-ajv8/src/validator.ts @@ -55,12 +55,14 @@ export default class AJV8Validator< customFormats, ajvOptionsOverrides, ajvFormatOptions, + AjvClass, } = options; this.ajv = createAjvInstance( additionalMetaSchemas, customFormats, ajvOptionsOverrides, - ajvFormatOptions + ajvFormatOptions, + AjvClass ); this.localizer = localizer; } diff --git a/packages/validator-ajv8/test/createAjvInstance.test.ts b/packages/validator-ajv8/test/createAjvInstance.test.ts index f726888bd4..e503d98517 100644 --- a/packages/validator-ajv8/test/createAjvInstance.test.ts +++ b/packages/validator-ajv8/test/createAjvInstance.test.ts @@ -1,4 +1,5 @@ import Ajv from "ajv"; +import Ajv2019 from "ajv/dist/2019"; import addFormats from "ajv-formats"; import createAjvInstance, { @@ -9,6 +10,7 @@ import createAjvInstance, { import { CustomValidatorOptionsType } from "../src"; jest.mock("ajv"); +jest.mock("ajv/dist/2019"); jest.mock("ajv-formats"); export const CUSTOM_OPTIONS: CustomValidatorOptionsType = { @@ -63,6 +65,51 @@ describe("createAjvInstance()", () => { expect(ajv.addMetaSchema).not.toHaveBeenCalled(); }); }); + describe("all defaults except uses the Ajv2019 class", () => { + let ajv: Ajv; + beforeAll(() => { + ajv = createAjvInstance( + undefined, + undefined, + undefined, + undefined, + Ajv2019 + ); + }); + afterAll(() => { + (Ajv as unknown as jest.Mock).mockClear(); + (addFormats as unknown as jest.Mock).mockClear(); + }); + it("expect a new Ajv2019 to be constructed with the AJV_CONFIG", () => { + expect(Ajv2019).toHaveBeenCalledWith(AJV_CONFIG); + }); + it("expect the default Ajv constructor was not called", () => { + expect(Ajv).not.toHaveBeenCalled(); + }); + it("expect addFormats to be called with the new ajv instance and undefined", () => { + expect(addFormats).toHaveBeenCalledWith(ajv, undefined); + }); + it("addFormat() was called twice", () => { + expect(ajv.addFormat).toHaveBeenCalledTimes(2); + }); + it("the first addFormat() was for data-url", () => { + expect(ajv.addFormat).toHaveBeenNthCalledWith( + 1, + "data-url", + DATA_URL_FORMAT_REGEX + ); + }); + it("the second addFormat() was for color", () => { + expect(ajv.addFormat).toHaveBeenNthCalledWith( + 2, + "color", + COLOR_FORMAT_REGEX + ); + }); + it("addMetaSchema was not called", () => { + expect(ajv.addMetaSchema).not.toHaveBeenCalled(); + }); + }); describe("has additional meta schemas, custom formats, ajv options override and ajv format options", () => { let ajv: Ajv; beforeAll(() => { diff --git a/packages/validator-ajv8/test/utilsTests/schema.test.ts b/packages/validator-ajv8/test/utilsTests/schema.test.ts index 1b99a1dc68..da36b92508 100644 --- a/packages/validator-ajv8/test/utilsTests/schema.test.ts +++ b/packages/validator-ajv8/test/utilsTests/schema.test.ts @@ -1,4 +1,6 @@ // With Lerna active, the test world has access to the test suite via the symlink +import Ajv2019 from "ajv/dist/2019"; +import Ajv2020 from "ajv/dist/2020"; import { getDefaultFormStateTest, getDisplayLabelTest, @@ -25,3 +27,29 @@ mergeValidationDataTest(testValidator); retrieveSchemaTest(testValidator); toIdSchemaTest(testValidator); toPathSchemaTest(testValidator); + +const testValidator2019 = getTestValidator({ AjvClass: Ajv2019 }); + +getDefaultFormStateTest(testValidator2019); +getDisplayLabelTest(testValidator2019); +getMatchingOptionTest(testValidator2019); +isFilesArrayTest(testValidator2019); +isMultiSelectTest(testValidator2019); +isSelectTest(testValidator2019); +mergeValidationDataTest(testValidator2019); +retrieveSchemaTest(testValidator2019); +toIdSchemaTest(testValidator2019); +toPathSchemaTest(testValidator2019); + +const testValidator2020 = getTestValidator({ AjvClass: Ajv2020 }); + +getDefaultFormStateTest(testValidator2020); +getDisplayLabelTest(testValidator2020); +getMatchingOptionTest(testValidator2020); +isFilesArrayTest(testValidator2020); +isMultiSelectTest(testValidator2020); +isSelectTest(testValidator2020); +mergeValidationDataTest(testValidator2020); +retrieveSchemaTest(testValidator2020); +toIdSchemaTest(testValidator2020); +toPathSchemaTest(testValidator2020); diff --git a/packages/validator-ajv8/test/validator.test.ts b/packages/validator-ajv8/test/validator.test.ts index aeb4120dee..fe80d6d58e 100644 --- a/packages/validator-ajv8/test/validator.test.ts +++ b/packages/validator-ajv8/test/validator.test.ts @@ -1,3 +1,5 @@ +import Ajv2019 from "ajv/dist/2019"; +import Ajv2020 from "ajv/dist/2020"; import { ErrorSchema, FormValidation, @@ -394,13 +396,925 @@ describe("AJV8Validator", () => { }); }); }); + describe("default options, with Ajv2019", () => { + // Use the TestValidator to access the `withIdRefPrefix` function + let validator: TestValidator; + beforeAll(() => { + validator = new TestValidator({ AjvClass: Ajv2019 }); + }); + describe("validator.isValid()", () => { + it("should return true if the data is valid against the schema", () => { + const schema: RJSFSchema = { + type: "object", + properties: { + foo: { type: "string" }, + }, + }; + + expect(validator.isValid(schema, { foo: "bar" }, schema)).toBe(true); + }); + it("should return false if the data is not valid against the schema", () => { + const schema: RJSFSchema = { + type: "object", + properties: { + foo: { type: "string" }, + }, + }; + + expect(validator.isValid(schema, { foo: 12345 }, schema)).toBe(false); + }); + it("should return false if the schema is invalid", () => { + const schema: RJSFSchema = "foobarbaz" as unknown as RJSFSchema; + + expect(validator.isValid(schema, { foo: "bar" }, schema)).toBe(false); + }); + it("should return true if the data is valid against the schema including refs to rootSchema", () => { + const schema: RJSFSchema = { + anyOf: [{ $ref: "#/definitions/foo" }], + }; + const rootSchema: RJSFSchema = { + definitions: { + foo: { + properties: { + name: { type: "string" }, + }, + }, + }, + }; + const formData = { + name: "John Doe", + }; + + expect(validator.isValid(schema, formData, rootSchema)).toBe(true); + }); + }); + describe("validator.withIdRefPrefix()", () => { + it("should recursively add id prefix to all refs", () => { + const schema: RJSFSchema = { + anyOf: [{ $ref: "#/defs/foo" }], + }; + const expected = { + anyOf: [{ $ref: "__rjsf_rootSchema#/defs/foo" }], + }; + + expect(validator.withIdRefPrefix(schema)).toEqual(expected); + }); + it("shouldn`t mutate the schema", () => { + const schema: RJSFSchema = { + anyOf: [{ $ref: "#/defs/foo" }], + }; + + validator.withIdRefPrefix(schema); + + expect(schema).toEqual({ + anyOf: [{ $ref: "#/defs/foo" }], + }); + }); + it("should not change a property named `$ref`", () => { + const schema: RJSFSchema = { + title: "A registration form", + description: "A simple form example.", + type: "object", + properties: { + $ref: { type: "string", title: "First name", default: "Chuck" }, + }, + }; + + expect(validator.withIdRefPrefix(schema)).toEqual(schema); + }); + }); + describe("validator.toErrorList()", () => { + it("should return empty list for unspecified errorSchema", () => { + expect(validator.toErrorList()).toEqual([]); + }); + it("should convert an errorSchema into a flat list", () => { + const errorSchema: ErrorSchema = { + __errors: ["err1", "err2"], + a: { + b: { + __errors: ["err3", "err4"], + } as ErrorSchema, + }, + c: { + __errors: ["err5"], + } as ErrorSchema, + } as unknown as ErrorSchema; + expect(validator.toErrorList(errorSchema)).toEqual([ + { property: ".", message: "err1", stack: ". err1" }, + { property: ".", message: "err2", stack: ". err2" }, + { property: ".a.b", message: "err3", stack: ".a.b err3" }, + { property: ".a.b", message: "err4", stack: ".a.b err4" }, + { property: ".c", message: "err5", stack: ".c err5" }, + ]); + }); + }); + describe("validator.validateFormData()", () => { + describe("No custom validate function, single value", () => { + let errors: RJSFValidationError[]; + let errorSchema: ErrorSchema; + + beforeAll(() => { + const schema: RJSFSchema = { + type: "object", + properties: { + foo: { type: "string" }, + [illFormedKey]: { type: "string" }, + }, + }; + const result = validator.validateFormData( + { foo: 42, [illFormedKey]: 41 }, + schema + ); + errors = result.errors; + errorSchema = result.errorSchema; + }); + + it("should return an error list", () => { + expect(errors).toHaveLength(2); + expect(errors[0].message).toEqual("must be string"); + expect(errors[1].message).toEqual("must be string"); + }); + it("should return an errorSchema", () => { + expect(errorSchema.foo!.__errors).toHaveLength(1); + expect(errorSchema.foo!.__errors![0]).toEqual("must be string"); + expect(errorSchema[illFormedKey]!.__errors).toHaveLength(1); + expect(errorSchema[illFormedKey]!.__errors![0]).toEqual( + "must be string" + ); + }); + }); + describe("Validating multipleOf with a float", () => { + let errors: RJSFValidationError[]; + beforeAll(() => { + const schema: RJSFSchema = { + type: "object", + properties: { + price: { + title: "Price per task ($)", + type: "number", + multipleOf: 0.01, + minimum: 0, + }, + }, + }; + const result = validator.validateFormData({ price: 0.14 }, schema); + errors = result.errors; + }); + it("should not return an error", () => { + expect(errors).toHaveLength(0); + }); + }); + describe("Validating multipleOf with a float, with multiple errors", () => { + let errors: RJSFValidationError[]; + let errorSchema: ErrorSchema; + beforeAll(() => { + const schema: RJSFSchema = { + type: "object", + properties: { + price: { + title: "Price per task ($)", + type: "number", + multipleOf: 0.03, + minimum: 1, + }, + }, + }; + const result = validator.validateFormData({ price: 0.14 }, schema); + errors = result.errors; + errorSchema = result.errorSchema; + }); + it("should have 2 errors", () => { + expect(errors).toHaveLength(2); + }); + it("first error is for minimum", () => { + expect(errors[0].message).toEqual("must be >= 1"); + }); + it("first error is for multipleOf", () => { + expect(errors[1].message).toEqual("must be multiple of 0.03"); + }); + it("should return an errorSchema", () => { + expect(errorSchema.price!.__errors).toHaveLength(2); + expect(errorSchema.price!.__errors).toEqual([ + "must be >= 1", + "must be multiple of 0.03", + ]); + }); + }); + describe("TransformErrors", () => { + let errors: RJSFValidationError[]; + let newErrorMessage: string; + beforeAll(() => { + const schema: RJSFSchema = { + type: "object", + properties: { + foo: { type: "string" }, + [illFormedKey]: { type: "string" }, + }, + }; + newErrorMessage = "Better error message"; + const transformErrors = (errors: RJSFValidationError[]) => { + return [Object.assign({}, errors[0], { message: newErrorMessage })]; + }; + const result = validator.validateFormData( + { foo: 42, [illFormedKey]: 41 }, + schema, + undefined, + transformErrors + ); + errors = result.errors; + }); + + it("should use transformErrors function", () => { + expect(errors).not.toHaveLength(0); + expect(errors[0].message).toEqual(newErrorMessage); + }); + }); + describe("Custom validate function", () => { + let errors: RJSFValidationError[]; + let errorSchema: ErrorSchema; + describe("formData is provided", () => { + beforeAll(() => { + const schema: RJSFSchema = { + type: "object", + required: ["pass1", "pass2"], + properties: { + pass1: { type: "string" }, + pass2: { type: "string" }, + foo: { type: "array", items: { type: "string" } }, // Adding an array for test coverage + }, + }; + + const validate = (formData: any, errors: FormValidation) => { + if (formData.pass1 !== formData.pass2) { + errors.pass2!.addError("passwords don`t match."); + } + return errors; + }; + const formData = { pass1: "a", pass2: "b", foo: ["a"] }; + const result = validator.validateFormData( + formData, + schema, + validate + ); + errors = result.errors; + errorSchema = result.errorSchema; + }); + it("should return an error list", () => { + expect(errors).toHaveLength(1); + expect(errors[0].stack).toEqual(".pass2 passwords don`t match."); + }); + it("should return an errorSchema", () => { + expect(errorSchema.pass2!.__errors).toHaveLength(1); + expect(errorSchema.pass2!.__errors![0]).toEqual( + "passwords don`t match." + ); + }); + }); + describe("formData is missing data", () => { + beforeAll(() => { + const schema: RJSFSchema = { + type: "object", + properties: { + pass1: { type: "string" }, + pass2: { type: "string" }, + }, + }; + const validate = (formData: any, errors: FormValidation) => { + if (formData.pass1 !== formData.pass2) { + errors.pass2!.addError("passwords don`t match."); + } + return errors; + }; + const formData = { pass1: "a" }; + const result = validator.validateFormData( + formData, + schema, + validate + ); + errors = result.errors; + errorSchema = result.errorSchema; + }); + it("should return an error list", () => { + expect(errors).toHaveLength(1); + expect(errors[0].stack).toEqual(".pass2 passwords don`t match."); + }); + it("should return an errorSchema", () => { + expect(errorSchema.pass2!.__errors).toHaveLength(1); + expect(errorSchema.pass2!.__errors![0]).toEqual( + "passwords don`t match." + ); + }); + }); + }); + describe("Data-Url validation", () => { + let schema: RJSFSchema; + beforeAll(() => { + schema = { + type: "object", + properties: { + dataUrlWithName: { type: "string", format: "data-url" }, + dataUrlWithoutName: { type: "string", format: "data-url" }, + }, + }; + }); + it("Data-Url with name is accepted", () => { + const formData = { + dataUrlWithName: "data:text/plain;name=file1.txt;base64,x=", + }; + const result = validator.validateFormData(formData, schema); + expect(result.errors).toHaveLength(0); + }); + it("Data-Url without name is accepted", () => { + const formData = { + dataUrlWithoutName: "data:text/plain;base64,x=", + }; + const result = validator.validateFormData(formData, schema); + expect(result.errors).toHaveLength(0); + }); + }); + describe("Invalid schema", () => { + let errors: RJSFValidationError[]; + let errorSchema: ErrorSchema; + + beforeAll(() => { + const schema: RJSFSchema = { + type: "object", + properties: { + foo: { + type: "string", + required: "invalid_type_non_array" as unknown as string[], + }, + }, + }; + const result = validator.validateFormData({ foo: 42 }, schema); + errors = result.errors; + errorSchema = result.errorSchema; + }); + it("should return an error list", () => { + expect(errors).toHaveLength(1); + expect(errors[0].name).toEqual("type"); + expect(errors[0].property).toEqual(".properties.foo.required"); + // Ajv2019 uses $defs rather than definitions + expect(errors[0].schemaPath).toEqual("#/$defs/stringArray/type"); + expect(errors[0].message).toEqual("must be array"); + }); + it("should return an errorSchema", () => { + expect(errorSchema.properties!.foo!.required!.__errors).toHaveLength( + 1 + ); + expect(errorSchema.properties!.foo!.required!.__errors![0]).toEqual( + "must be array" + ); + }); + }); + }); + }); + describe("default options, with Ajv2020", () => { + // Use the TestValidator to access the `withIdRefPrefix` function + let validator: TestValidator; + beforeAll(() => { + validator = new TestValidator({ AjvClass: Ajv2020 }); + }); + describe("validator.isValid()", () => { + it("should return true if the data is valid against the schema", () => { + const schema: RJSFSchema = { + type: "object", + properties: { + foo: { type: "string" }, + }, + }; + + expect(validator.isValid(schema, { foo: "bar" }, schema)).toBe(true); + }); + it("should return false if the data is not valid against the schema", () => { + const schema: RJSFSchema = { + type: "object", + properties: { + foo: { type: "string" }, + }, + }; + + expect(validator.isValid(schema, { foo: 12345 }, schema)).toBe(false); + }); + it("should return false if the schema is invalid", () => { + const schema: RJSFSchema = "foobarbaz" as unknown as RJSFSchema; + + expect(validator.isValid(schema, { foo: "bar" }, schema)).toBe(false); + }); + it("should return true if the data is valid against the schema including refs to rootSchema", () => { + const schema: RJSFSchema = { + anyOf: [{ $ref: "#/definitions/foo" }], + }; + const rootSchema: RJSFSchema = { + definitions: { + foo: { + properties: { + name: { type: "string" }, + }, + }, + }, + }; + const formData = { + name: "John Doe", + }; + + expect(validator.isValid(schema, formData, rootSchema)).toBe(true); + }); + }); + describe("validator.withIdRefPrefix()", () => { + it("should recursively add id prefix to all refs", () => { + const schema: RJSFSchema = { + anyOf: [{ $ref: "#/defs/foo" }], + }; + const expected = { + anyOf: [{ $ref: "__rjsf_rootSchema#/defs/foo" }], + }; + + expect(validator.withIdRefPrefix(schema)).toEqual(expected); + }); + it("shouldn`t mutate the schema", () => { + const schema: RJSFSchema = { + anyOf: [{ $ref: "#/defs/foo" }], + }; + + validator.withIdRefPrefix(schema); + + expect(schema).toEqual({ + anyOf: [{ $ref: "#/defs/foo" }], + }); + }); + it("should not change a property named `$ref`", () => { + const schema: RJSFSchema = { + title: "A registration form", + description: "A simple form example.", + type: "object", + properties: { + $ref: { type: "string", title: "First name", default: "Chuck" }, + }, + }; + + expect(validator.withIdRefPrefix(schema)).toEqual(schema); + }); + }); + describe("validator.toErrorList()", () => { + it("should return empty list for unspecified errorSchema", () => { + expect(validator.toErrorList()).toEqual([]); + }); + it("should convert an errorSchema into a flat list", () => { + const errorSchema: ErrorSchema = { + __errors: ["err1", "err2"], + a: { + b: { + __errors: ["err3", "err4"], + } as ErrorSchema, + }, + c: { + __errors: ["err5"], + } as ErrorSchema, + } as unknown as ErrorSchema; + expect(validator.toErrorList(errorSchema)).toEqual([ + { property: ".", message: "err1", stack: ". err1" }, + { property: ".", message: "err2", stack: ". err2" }, + { property: ".a.b", message: "err3", stack: ".a.b err3" }, + { property: ".a.b", message: "err4", stack: ".a.b err4" }, + { property: ".c", message: "err5", stack: ".c err5" }, + ]); + }); + }); + describe("validator.validateFormData()", () => { + describe("No custom validate function, single value", () => { + let errors: RJSFValidationError[]; + let errorSchema: ErrorSchema; + + beforeAll(() => { + const schema: RJSFSchema = { + type: "object", + properties: { + foo: { type: "string" }, + [illFormedKey]: { type: "string" }, + }, + }; + const result = validator.validateFormData( + { foo: 42, [illFormedKey]: 41 }, + schema + ); + errors = result.errors; + errorSchema = result.errorSchema; + }); + + it("should return an error list", () => { + expect(errors).toHaveLength(2); + expect(errors[0].message).toEqual("must be string"); + expect(errors[1].message).toEqual("must be string"); + }); + it("should return an errorSchema", () => { + expect(errorSchema.foo!.__errors).toHaveLength(1); + expect(errorSchema.foo!.__errors![0]).toEqual("must be string"); + expect(errorSchema[illFormedKey]!.__errors).toHaveLength(1); + expect(errorSchema[illFormedKey]!.__errors![0]).toEqual( + "must be string" + ); + }); + }); + describe("Validating multipleOf with a float", () => { + let errors: RJSFValidationError[]; + beforeAll(() => { + const schema: RJSFSchema = { + type: "object", + properties: { + price: { + title: "Price per task ($)", + type: "number", + multipleOf: 0.01, + minimum: 0, + }, + }, + }; + const result = validator.validateFormData({ price: 0.14 }, schema); + errors = result.errors; + }); + it("should not return an error", () => { + expect(errors).toHaveLength(0); + }); + }); + describe("Validating multipleOf with a float, with multiple errors", () => { + let errors: RJSFValidationError[]; + let errorSchema: ErrorSchema; + beforeAll(() => { + const schema: RJSFSchema = { + type: "object", + properties: { + price: { + title: "Price per task ($)", + type: "number", + multipleOf: 0.03, + minimum: 1, + }, + }, + }; + const result = validator.validateFormData({ price: 0.14 }, schema); + errors = result.errors; + errorSchema = result.errorSchema; + }); + it("should have 2 errors", () => { + expect(errors).toHaveLength(2); + }); + it("first error is for minimum", () => { + expect(errors[0].message).toEqual("must be >= 1"); + }); + it("first error is for multipleOf", () => { + expect(errors[1].message).toEqual("must be multiple of 0.03"); + }); + it("should return an errorSchema", () => { + expect(errorSchema.price!.__errors).toHaveLength(2); + expect(errorSchema.price!.__errors).toEqual([ + "must be >= 1", + "must be multiple of 0.03", + ]); + }); + }); + describe("TransformErrors", () => { + let errors: RJSFValidationError[]; + let newErrorMessage: string; + beforeAll(() => { + const schema: RJSFSchema = { + type: "object", + properties: { + foo: { type: "string" }, + [illFormedKey]: { type: "string" }, + }, + }; + newErrorMessage = "Better error message"; + const transformErrors = (errors: RJSFValidationError[]) => { + return [Object.assign({}, errors[0], { message: newErrorMessage })]; + }; + const result = validator.validateFormData( + { foo: 42, [illFormedKey]: 41 }, + schema, + undefined, + transformErrors + ); + errors = result.errors; + }); + + it("should use transformErrors function", () => { + expect(errors).not.toHaveLength(0); + expect(errors[0].message).toEqual(newErrorMessage); + }); + }); + describe("Custom validate function", () => { + let errors: RJSFValidationError[]; + let errorSchema: ErrorSchema; + describe("formData is provided", () => { + beforeAll(() => { + const schema: RJSFSchema = { + type: "object", + required: ["pass1", "pass2"], + properties: { + pass1: { type: "string" }, + pass2: { type: "string" }, + foo: { type: "array", items: { type: "string" } }, // Adding an array for test coverage + }, + }; + + const validate = (formData: any, errors: FormValidation) => { + if (formData.pass1 !== formData.pass2) { + errors.pass2!.addError("passwords don`t match."); + } + return errors; + }; + const formData = { pass1: "a", pass2: "b", foo: ["a"] }; + const result = validator.validateFormData( + formData, + schema, + validate + ); + errors = result.errors; + errorSchema = result.errorSchema; + }); + it("should return an error list", () => { + expect(errors).toHaveLength(1); + expect(errors[0].stack).toEqual(".pass2 passwords don`t match."); + }); + it("should return an errorSchema", () => { + expect(errorSchema.pass2!.__errors).toHaveLength(1); + expect(errorSchema.pass2!.__errors![0]).toEqual( + "passwords don`t match." + ); + }); + }); + describe("formData is missing data", () => { + beforeAll(() => { + const schema: RJSFSchema = { + type: "object", + properties: { + pass1: { type: "string" }, + pass2: { type: "string" }, + }, + }; + const validate = (formData: any, errors: FormValidation) => { + if (formData.pass1 !== formData.pass2) { + errors.pass2!.addError("passwords don`t match."); + } + return errors; + }; + const formData = { pass1: "a" }; + const result = validator.validateFormData( + formData, + schema, + validate + ); + errors = result.errors; + errorSchema = result.errorSchema; + }); + it("should return an error list", () => { + expect(errors).toHaveLength(1); + expect(errors[0].stack).toEqual(".pass2 passwords don`t match."); + }); + it("should return an errorSchema", () => { + expect(errorSchema.pass2!.__errors).toHaveLength(1); + expect(errorSchema.pass2!.__errors![0]).toEqual( + "passwords don`t match." + ); + }); + }); + }); + describe("Data-Url validation", () => { + let schema: RJSFSchema; + beforeAll(() => { + schema = { + type: "object", + properties: { + dataUrlWithName: { type: "string", format: "data-url" }, + dataUrlWithoutName: { type: "string", format: "data-url" }, + }, + }; + }); + it("Data-Url with name is accepted", () => { + const formData = { + dataUrlWithName: "data:text/plain;name=file1.txt;base64,x=", + }; + const result = validator.validateFormData(formData, schema); + expect(result.errors).toHaveLength(0); + }); + it("Data-Url without name is accepted", () => { + const formData = { + dataUrlWithoutName: "data:text/plain;base64,x=", + }; + const result = validator.validateFormData(formData, schema); + expect(result.errors).toHaveLength(0); + }); + }); + describe("Invalid schema", () => { + let errors: RJSFValidationError[]; + let errorSchema: ErrorSchema; + + beforeAll(() => { + const schema: RJSFSchema = { + type: "object", + properties: { + foo: { + type: "string", + required: "invalid_type_non_array" as unknown as string[], + }, + }, + }; + const result = validator.validateFormData({ foo: 42 }, schema); + errors = result.errors; + errorSchema = result.errorSchema; + }); + it("should return an error list", () => { + expect(errors).toHaveLength(1); + expect(errors[0].name).toEqual("type"); + expect(errors[0].property).toEqual(".properties.foo.required"); + // Ajv2019 uses $defs rather than definitions + expect(errors[0].schemaPath).toEqual("#/$defs/stringArray/type"); + expect(errors[0].message).toEqual("must be array"); + }); + it("should return an errorSchema", () => { + expect(errorSchema.properties!.foo!.required!.__errors).toHaveLength( + 1 + ); + expect(errorSchema.properties!.foo!.required!.__errors![0]).toEqual( + "must be array" + ); + }); + }); + }); + }); describe("validator.validateFormData(), custom options, and localizer", () => { let validator: TestValidator; let schema: RJSFSchema; let localizer: Localizer; beforeAll(() => { localizer = jest.fn().mockImplementation(); - validator = new TestValidator({}, localizer); + validator = new TestValidator({}, localizer); + schema = { + $ref: "#/definitions/Dataset", + $schema: "http://json-schema.org/draft-06/schema#", + definitions: { + Dataset: { + properties: { + datasetId: { + pattern: "\\d+", + type: "string", + }, + }, + required: ["datasetId"], + type: "object", + }, + }, + }; + }); + it("should return a validation error about meta schema when meta schema is not defined", () => { + const errors = validator.validateFormData( + { datasetId: "some kind of text" }, + schema + ); + const errMessage = + 'no schema with key or ref "http://json-schema.org/draft-06/schema#"'; + expect(errors.errors).toEqual([{ stack: errMessage }]); + expect(errors.errorSchema).toEqual({ + $schema: { __errors: [errMessage] }, + }); + expect(localizer).toHaveBeenCalledWith(undefined); + }); + describe("validating using single custom meta schema", () => { + let errors: RJSFValidationError[]; + beforeAll(() => { + (localizer as jest.Mock).mockClear(); + validator = new TestValidator( + { + additionalMetaSchemas: [metaSchemaDraft6], + }, + localizer + ); + const result = validator.validateFormData( + { datasetId: "some kind of text" }, + schema + ); + errors = result.errors; + }); + it("should return 1 error about formData", () => { + expect(errors).toHaveLength(1); + }); + it("has a pattern match validation error about formData", () => { + expect(errors[0].stack).toEqual('.datasetId must match pattern "\\d+"'); + }); + it("localizer was called with the errors", () => { + expect(localizer).toHaveBeenCalledWith([ + { + instancePath: "/datasetId", + keyword: "pattern", + message: 'must match pattern "\\d+"', + params: { pattern: "\\d+" }, + schemaPath: "#/definitions/Dataset/properties/datasetId/pattern", + }, + ]); + }); + }); + describe("validating using several custom meta schemas", () => { + let errors: RJSFValidationError[]; + + beforeAll(() => { + validator = new TestValidator({ + additionalMetaSchemas: [metaSchemaDraft6], + }); + const result = validator.validateFormData( + { datasetId: "some kind of text" }, + schema + ); + errors = result.errors; + }); + it("should return 1 error about formData", () => { + expect(errors).toHaveLength(1); + }); + it("has a pattern match validation error about formData", () => { + expect(errors[0].stack).toEqual('.datasetId must match pattern "\\d+"'); + }); + }); + describe("validating using custom string formats", () => { + let validator: ValidatorType; + let schema: RJSFSchema; + beforeAll(() => { + validator = new AJV8Validator({}); + schema = { + type: "object", + properties: { + phone: { + type: "string", + format: "phone-us", + }, + }, + }; + }); + it("should not return a validation error if unknown string format is used", () => { + const result = validator.validateFormData( + { phone: "800.555.2368" }, + schema + ); + expect(result.errors.length).toEqual(0); + }); + describe("validating using a custom formats", () => { + let errors: RJSFValidationError[]; + + beforeAll(() => { + validator = new AJV8Validator({ + customFormats: { + "phone-us": /\(?\d{3}\)?[\s-]?\d{3}[\s-]?\d{4}$/, + "area-code": /\d{3}/, + }, + }); + const result = validator.validateFormData( + { phone: "800.555.2368" }, + schema + ); + errors = result.errors; + }); + it("should return 1 error about formData", () => { + expect(errors).toHaveLength(1); + }); + it("should return a validation error about formData", () => { + expect(errors[0].stack).toEqual( + '.phone must match format "phone-us"' + ); + }); + describe("prop updates with new custom formats are accepted", () => { + beforeAll(() => { + const result = validator.validateFormData( + { phone: "abc" }, + { + type: "object", + properties: { + phone: { + type: "string", + format: "area-code", + }, + }, + } + ); + errors = result.errors; + }); + + it("should return 1 error about formData", () => { + expect(errors).toHaveLength(1); + }); + it("should return a validation error about formData", () => { + expect(errors[0].stack).toEqual( + '.phone must match format "area-code"' + ); + }); + }); + }); + }); + }); + describe("validator.validateFormData(), custom options, localizer and Ajv2019", () => { + let validator: TestValidator; + let schema: RJSFSchema; + let localizer: Localizer; + beforeAll(() => { + localizer = jest.fn().mockImplementation(); + validator = new TestValidator({ AjvClass: Ajv2019 }, localizer); schema = { $ref: "#/definitions/Dataset", $schema: "http://json-schema.org/draft-06/schema#", @@ -438,6 +1352,7 @@ describe("AJV8Validator", () => { validator = new TestValidator( { additionalMetaSchemas: [metaSchemaDraft6], + AjvClass: Ajv2019, }, localizer ); @@ -471,6 +1386,7 @@ describe("AJV8Validator", () => { beforeAll(() => { validator = new TestValidator({ additionalMetaSchemas: [metaSchemaDraft6], + AjvClass: Ajv2019, }); const result = validator.validateFormData( { datasetId: "some kind of text" }, @@ -489,7 +1405,175 @@ describe("AJV8Validator", () => { let validator: ValidatorType; let schema: RJSFSchema; beforeAll(() => { - validator = new AJV8Validator({}); + validator = new AJV8Validator({ AjvClass: Ajv2019 }); + schema = { + type: "object", + properties: { + phone: { + type: "string", + format: "phone-us", + }, + }, + }; + }); + it("should not return a validation error if unknown string format is used", () => { + const result = validator.validateFormData( + { phone: "800.555.2368" }, + schema + ); + expect(result.errors.length).toEqual(0); + }); + describe("validating using a custom formats", () => { + let errors: RJSFValidationError[]; + + beforeAll(() => { + validator = new AJV8Validator({ + customFormats: { + "phone-us": /\(?\d{3}\)?[\s-]?\d{3}[\s-]?\d{4}$/, + "area-code": /\d{3}/, + }, + }); + const result = validator.validateFormData( + { phone: "800.555.2368" }, + schema + ); + errors = result.errors; + }); + it("should return 1 error about formData", () => { + expect(errors).toHaveLength(1); + }); + it("should return a validation error about formData", () => { + expect(errors[0].stack).toEqual( + '.phone must match format "phone-us"' + ); + }); + describe("prop updates with new custom formats are accepted", () => { + beforeAll(() => { + const result = validator.validateFormData( + { phone: "abc" }, + { + type: "object", + properties: { + phone: { + type: "string", + format: "area-code", + }, + }, + } + ); + errors = result.errors; + }); + + it("should return 1 error about formData", () => { + expect(errors).toHaveLength(1); + }); + it("should return a validation error about formData", () => { + expect(errors[0].stack).toEqual( + '.phone must match format "area-code"' + ); + }); + }); + }); + }); + }); + describe("validator.validateFormData(), custom options, localizer and Ajv2020", () => { + let validator: TestValidator; + let schema: RJSFSchema; + let localizer: Localizer; + beforeAll(() => { + localizer = jest.fn().mockImplementation(); + validator = new TestValidator({ AjvClass: Ajv2020 }, localizer); + schema = { + $ref: "#/definitions/Dataset", + $schema: "http://json-schema.org/draft-06/schema#", + definitions: { + Dataset: { + properties: { + datasetId: { + pattern: "\\d+", + type: "string", + }, + }, + required: ["datasetId"], + type: "object", + }, + }, + }; + }); + it("should return a validation error about meta schema when meta schema is not defined", () => { + const errors = validator.validateFormData( + { datasetId: "some kind of text" }, + schema + ); + const errMessage = + 'no schema with key or ref "http://json-schema.org/draft-06/schema#"'; + expect(errors.errors).toEqual([{ stack: errMessage }]); + expect(errors.errorSchema).toEqual({ + $schema: { __errors: [errMessage] }, + }); + expect(localizer).toHaveBeenCalledWith(undefined); + }); + describe("validating using single custom meta schema", () => { + let errors: RJSFValidationError[]; + beforeAll(() => { + (localizer as jest.Mock).mockClear(); + validator = new TestValidator( + { + additionalMetaSchemas: [metaSchemaDraft6], + AjvClass: Ajv2020, + }, + localizer + ); + const result = validator.validateFormData( + { datasetId: "some kind of text" }, + schema + ); + errors = result.errors; + }); + it("should return 1 error about formData", () => { + expect(errors).toHaveLength(1); + }); + it("has a pattern match validation error about formData", () => { + expect(errors[0].stack).toEqual('.datasetId must match pattern "\\d+"'); + }); + it("localizer was called with the errors", () => { + expect(localizer).toHaveBeenCalledWith([ + { + instancePath: "/datasetId", + keyword: "pattern", + message: 'must match pattern "\\d+"', + params: { pattern: "\\d+" }, + schemaPath: "#/definitions/Dataset/properties/datasetId/pattern", + }, + ]); + }); + }); + describe("validating using several custom meta schemas", () => { + let errors: RJSFValidationError[]; + + beforeAll(() => { + validator = new TestValidator({ + additionalMetaSchemas: [metaSchemaDraft6], + AjvClass: Ajv2020, + }); + const result = validator.validateFormData( + { datasetId: "some kind of text" }, + schema + ); + errors = result.errors; + }); + it("should return 1 error about formData", () => { + expect(errors).toHaveLength(1); + }); + it("has a pattern match validation error about formData", () => { + expect(errors[0].stack).toEqual('.datasetId must match pattern "\\d+"'); + }); + }); + describe("validating using custom string formats", () => { + let validator: ValidatorType; + let schema: RJSFSchema; + beforeAll(() => { + validator = new AJV8Validator({ AjvClass: Ajv2020 }); schema = { type: "object", properties: {