From ba8cac7f113bbb090766cfbac3e5c99b1ab3e9a3 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Mon, 15 Jan 2024 22:03:42 -0500 Subject: [PATCH] Report validation errors when tests fail - Define custom matcher `expect(...).toValidate({ schema })` - Use JSON Schema DETAILED mode - Print errors as highlighted YAML --- packages/tests/package.json | 2 + packages/tests/schemas/examples.test.ts | 14 ++--- packages/tests/src/loadSchemas.ts | 54 +++++++++++++++++++ packages/tests/tsconfig.json | 2 +- packages/tests/typings.d.ts | 7 +++ yarn.lock | 71 ++++++++++++++++++++++++- 6 files changed, 138 insertions(+), 12 deletions(-) create mode 100644 packages/tests/typings.d.ts diff --git a/packages/tests/package.json b/packages/tests/package.json index dabb4409..13850cc7 100644 --- a/packages/tests/package.json +++ b/packages/tests/package.json @@ -13,6 +13,8 @@ "@ethdebug/format": "^0.1.0-0", "@hyperjump/json-schema": "1.6.7", "@jest/globals": "^29.7.0", + "cli-highlight": "^2.1.11", + "indent-string": "^5.0.0", "jest": "^29.7.0", "ts-jest": "^29.1.1", "typescript": "^5.3.3" diff --git a/packages/tests/schemas/examples.test.ts b/packages/tests/schemas/examples.test.ts index 1b9fa588..c1ffd2df 100644 --- a/packages/tests/schemas/examples.test.ts +++ b/packages/tests/schemas/examples.test.ts @@ -1,12 +1,8 @@ import { expect, describe, it } from "@jest/globals"; -import { validate } from "@hyperjump/json-schema/draft-2020-12"; import type { JSONSchema } from "@ethdebug/format"; -import schemas, { - schemaExtensions -} from "../src/schemas.js"; -import printErrors from "../src/printErrors.js"; +import schemas, { schemaExtensions } from "../src/schemas.js"; // loads schemas into global hyperjump json schema validator import "../src/loadSchemas.js"; @@ -77,8 +73,7 @@ function testExamples(options: { for (const [index, example] of examples.entries()) { describe(`example #${index}`, () => { it(`is a valid ${schema.title || id}`, async () => { - const output = await validate(id, example); - expect(output.valid).toBe(true); + await expect(example).toValidate({ schema: { id } }); }) const testedParentSchemas = new Set(); @@ -95,8 +90,9 @@ function testExamples(options: { } it(`is also a valid ${parentSchemaId}`, async () => { - const output = await validate(parentSchemaId, example); - expect(output.valid).toBe(true); + await expect(example).toValidate({ + schema: { id: parentSchemaId } + }); }); // recurse to ancestors diff --git a/packages/tests/src/loadSchemas.ts b/packages/tests/src/loadSchemas.ts index 02c4e6dd..0dd5b5ea 100644 --- a/packages/tests/src/loadSchemas.ts +++ b/packages/tests/src/loadSchemas.ts @@ -1,8 +1,18 @@ +import { expect } from "@jest/globals"; import { addSchema, validate, setMetaSchemaOutputFormat, } from "@hyperjump/json-schema/draft-2020-12"; +import { bundle } from "@hyperjump/json-schema/bundle"; +import YAML from "yaml"; +import indentString from "indent-string"; +import { highlight } from "cli-highlight"; + +import { + describeSchema, + type DescribeSchemaOptions +} from "@ethdebug/format"; import schemas from "./schemas.js"; @@ -12,6 +22,50 @@ const main = () => { for (const schema of Object.values(schemas)) { addSchema(schema as any); } + + expect.extend({ + async toValidate(received: any, schemaOptions: DescribeSchemaOptions) { + const { id, pointer, schema } = describeSchema(schemaOptions); + + if (typeof id !== "string") { + throw new Error("Schema is not known to validator by ID"); + } + + const schemaReference = pointer + ? `${id}${pointer}` + : id; + + const schemaName = + schema.title + ? schema.title + : schemaReference; + + const output = await validate(schemaReference, received, "VERBOSE"); + + const pass = output.valid; + + return { + pass, + message: () => `expected ${ + JSON.stringify(received) + } ${ + pass + ? "not to be" + : "to be" + } valid ${schemaName}.${ + pass + ? "" + : `\noutput:\n${ + indentString( + highlight(YAML.stringify(output), { language: "yaml" }), + 2 + ) + }` + }` + }; + } + }); + } main(); diff --git a/packages/tests/tsconfig.json b/packages/tests/tsconfig.json index 36f0ef57..7aaf2400 100644 --- a/packages/tests/tsconfig.json +++ b/packages/tests/tsconfig.json @@ -26,7 +26,7 @@ /* Modules */ "module": "esnext", /* Specify what module code is generated. */ - // "rootDir": "./", /* Specify the root folder within your source files. */ + "rootDir": "./", /* Specify the root folder within your source files. */ "moduleResolution": "nodenext", /* Specify how TypeScript looks up a file from a given module specifier. */ // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ diff --git a/packages/tests/typings.d.ts b/packages/tests/typings.d.ts new file mode 100644 index 00000000..112c82fd --- /dev/null +++ b/packages/tests/typings.d.ts @@ -0,0 +1,7 @@ +import type { DescribeSchemaOptions } from "@ethdebug/format"; + +declare module "@jest/expect" { + interface Matchers { + toValidate(schemaOptions: DescribeSchemaOptions): R; + } +} diff --git a/yarn.lock b/yarn.lock index 0d17ecea..e35dfae2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3507,6 +3507,11 @@ ansi-styles@^6.1.0: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== +any-promise@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" + integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A== + anymatch@^3.0.3, anymatch@~3.1.2: version "3.1.3" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" @@ -4168,6 +4173,18 @@ cli-cursor@3.1.0, cli-cursor@^3.1.0: dependencies: restore-cursor "^3.1.0" +cli-highlight@^2.1.11: + version "2.1.11" + resolved "https://registry.yarnpkg.com/cli-highlight/-/cli-highlight-2.1.11.tgz#49736fa452f0aaf4fae580e30acb26828d2dc1bf" + integrity sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg== + dependencies: + chalk "^4.0.0" + highlight.js "^10.7.1" + mz "^2.4.0" + parse5 "^5.1.1" + parse5-htmlparser2-tree-adapter "^6.0.0" + yargs "^16.0.0" + cli-spinners@2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.6.1.tgz#adc954ebe281c37a6319bfa401e6dd2488ffb70d" @@ -6296,6 +6313,11 @@ he@^1.2.0: resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== +highlight.js@^10.7.1: + version "10.7.3" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531" + integrity sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A== + history@^4.9.0: version "4.10.1" resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3" @@ -6640,6 +6662,11 @@ indent-string@^4.0.0: resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== +indent-string@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-5.0.0.tgz#4fd2980fccaf8622d14c64d694f4cf33c81951a5" + integrity sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg== + infima@0.2.0-alpha.43: version "0.2.0-alpha.43" resolved "https://registry.yarnpkg.com/infima/-/infima-0.2.0-alpha.43.tgz#f7aa1d7b30b6c08afef441c726bac6150228cbe0" @@ -8902,6 +8929,15 @@ mute-stream@~1.0.0: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-1.0.0.tgz#e31bd9fe62f0aed23520aa4324ea6671531e013e" integrity sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA== +mz@^2.4.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" + integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q== + dependencies: + any-promise "^1.0.0" + object-assign "^4.0.1" + thenify-all "^1.0.0" + nanoid@^3.3.7: version "3.3.7" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" @@ -9273,7 +9309,7 @@ nx@17.2.8, "nx@>=17.1.2 < 18": "@nx/nx-win32-arm64-msvc" "17.2.8" "@nx/nx-win32-x64-msvc" "17.2.8" -object-assign@^4.1.1: +object-assign@^4.0.1, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== @@ -9598,6 +9634,13 @@ parse-url@^8.1.0: dependencies: parse-path "^7.0.0" +parse5-htmlparser2-tree-adapter@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6" + integrity sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA== + dependencies: + parse5 "^6.0.1" + parse5-htmlparser2-tree-adapter@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz#23c2cc233bcf09bb7beba8b8a69d46b08c62c2f1" @@ -9606,6 +9649,16 @@ parse5-htmlparser2-tree-adapter@^7.0.0: domhandler "^5.0.2" parse5 "^7.0.0" +parse5@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178" + integrity sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug== + +parse5@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" + integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== + parse5@^7.0.0: version "7.1.2" resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" @@ -11651,6 +11704,20 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== +thenify-all@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" + integrity sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA== + dependencies: + thenify ">= 3.1.0 < 4" + +"thenify@>= 3.1.0 < 4": + version "3.3.1" + resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.1.tgz#8932e686a4066038a016dd9e2ca46add9838a95f" + integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw== + dependencies: + any-promise "^1.0.0" + through2@^2.0.0: version "2.0.5" resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" @@ -12566,7 +12633,7 @@ yargs@17.7.2, yargs@^17.3.1, yargs@^17.6.2, yargs@^17.7.2: y18n "^5.0.5" yargs-parser "^21.1.1" -yargs@^16.2.0: +yargs@^16.0.0, yargs@^16.2.0: version "16.2.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==