Skip to content

Commit 000e6bf

Browse files
committed
Include README and other cleanup
1 parent 2b90175 commit 000e6bf

8 files changed

+143
-58
lines changed

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
* text eol=lf
2+
*.png binary

README.md

Lines changed: 78 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,85 @@
11
# Hyperjump - JSON Schema Test Coverage
22

3-
## Usage
3+
This package provides tools for testing JSON Schemas and providing test coverage
4+
for schema files in your code base. Integration is provided for Vitest, but the
5+
component for collecting the coverage data is also exposed if you want to
6+
do some other integration.
7+
8+
Validation is done by `@hyperjump/json-schema`, so you can use any version of
9+
JSON Schema supported by that package.
410

511
```bash
6-
rm ./.nyc_output/*
7-
npm test
8-
npx nyc report --reporter=html --extension .schema.json
9-
npx http-server coverage
12+
-------------|---------|----------|---------|---------|-------------------
13+
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
14+
-------------|---------|----------|---------|---------|-------------------
15+
All files | 81.81 | 66.66 | 80 | 88.88 |
16+
schema.json | 81.81 | 66.66 | 80 | 88.88 | 5
17+
-------------|---------|----------|---------|---------|-------------------
18+
```
19+
20+
![HTML coverage example](coverage.png)
21+
22+
**Legend**
23+
- Statements = Keywords and subschemas
24+
- Branches = true/false branches for each keyword
25+
- Functions = Subschemas
26+
27+
## Vitest
28+
29+
Integration with vitest is provided as `@hyperjump/json-schema-coverage/vitest`.
30+
You'll need a vitest config specifically for running schema coverage.
31+
32+
`vitest-schema.config.js`
33+
```JavaScript
34+
import { defineConfig } from "vitest/config";
35+
36+
export default defineConfig({
37+
test: {
38+
coverage: {
39+
provider: "custom",
40+
customProviderModule: "@hyperjump/json-schema-coverage/vitest"
41+
}
42+
}
43+
});
44+
```
45+
46+
When you use the provided custom matcher `matchJsonSchema`/`toMatchJsonSchema`,
47+
if vitest has coverage is enabled, it will collect coverage data from those
48+
tests.
49+
50+
```JavaScript
51+
import { describe, expect, test } from "vitest";
52+
import "@hyperjump/json-schema-coverage/vitest";
53+
54+
describe("Worksheet", () => {
55+
test("matches with uri", async () => {
56+
await expect({ foo: 42 }).toMatchJsonSchema("./schema.json");
57+
});
58+
59+
test("doesn't match with uri", async () => {
60+
await expect({ foo: null }).not.toMatchJsonSchema("./schema.json");
61+
});
62+
});
63+
```
64+
65+
You can also use the matcher with inline schemas, but you only get coverage for
66+
file-based schemas.
67+
68+
```JavaScript
69+
import { describe, expect, test } from "vitest";
70+
import "@hyperjump/json-schema-coverage/vitest";
71+
72+
describe("Worksheet", () => {
73+
test("matches with schema", async () => {
74+
await expect("foo").to.matchJsonSchema({ type: "string" });
75+
});
76+
77+
test("doesn't match with schema", async () => {
78+
await expect(42).to.not.matchJsonSchema({ type: "string" });
79+
});
80+
});
1081
```
11-
## Legend
1282

13-
Statements = Keywords
14-
Branches = true/false for each keyword
15-
Functions = Subschemas
83+
## TestCoverageEvaluationPlugin
1684

85+
TODO

coverage.png

134 KB
Loading

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
},
2222
"scripts": {
2323
"lint": "eslint src",
24-
"test": "vitest --watch=false",
24+
"test": "vitest run",
2525
"type-check": "tsc --noEmit",
2626
"docs": "typedoc --excludeExternals"
2727
},
@@ -42,6 +42,8 @@
4242
"@hyperjump/json-schema": "^1.16.0",
4343
"@hyperjump/uri": "^1.3.1",
4444
"istanbul-lib-coverage": "^3.2.2",
45+
"istanbul-lib-report": "^3.0.1",
46+
"istanbul-reports": "^3.1.7",
4547
"moo": "^0.5.2",
4648
"pathe": "^2.0.3",
4749
"vfile": "^6.0.3"

src/test-coverage-evaluation-plugin.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export class TestCoverageEvaluationPlugin {
4343
}
4444

4545
const schemaPath = fileURLToPath(schemaUri);
46+
this.coverageMap[schemaPath].s[schemaUri]++;
4647
this.coverageMap[schemaPath].f[schemaUri]++;
4748
}
4849

@@ -83,11 +84,17 @@ export class TestCoverageEvaluationPlugin {
8384
end: { line: node.position.start.line, column: node.position.start.column - 1 }
8485
};
8586

87+
const locRange = positionToRange(node.position);
88+
89+
// Create statement
90+
this.coverageMap[schemaPath].statementMap[schemaLocation] = locRange;
91+
this.coverageMap[schemaPath].s[schemaLocation] = 0;
92+
8693
// Create function
8794
this.coverageMap[schemaPath].fnMap[schemaLocation] = {
8895
name: schemaLocation,
8996
decl: declRange,
90-
loc: positionToRange(node.position),
97+
loc: locRange,
9198
line: node.position.start.line
9299
};
93100
this.coverageMap[schemaPath].f[schemaLocation] = 0;

src/vitest/json-schema-coverage-provider.js

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
import { existsSync, readdirSync } from "node:fs";
22
import * as fs from "node:fs/promises";
33
import { createCoverageMap } from "istanbul-lib-coverage";
4-
import { resolve } from "pathe";
5-
import c from "tinyrainbow";
64
import libReport from "istanbul-lib-report";
75
import reports from "istanbul-reports";
6+
import { resolve } from "pathe";
7+
import c from "tinyrainbow";
88
import { coverageConfigDefaults } from "vitest/config";
99

1010
/**
11-
* @import { BaseCoverageOptions, CoverageProvider, CoverageProviderModule, ResolvedCoverageOptions, Vitest} from "vitest"
11+
* @import {
12+
* BaseCoverageOptions,
13+
* CoverageProvider,
14+
* CoverageProviderModule,
15+
* ResolvedCoverageOptions,
16+
* Vitest
17+
* } from "vitest"
1218
* @import { CoverageMap, CoverageMapData } from "istanbul-lib-coverage"
1319
*/
1420

@@ -48,8 +54,7 @@ class JsonSchemaCoverageProvider {
4854
ctx.config.root,
4955
config.reportsDirectory || coverageConfigDefaults.reportsDirectory
5056
),
51-
reporter: resolveCoverageReporters(config.reporter || coverageConfigDefaults.reporter),
52-
extension: ".schema.json"
57+
reporter: resolveCoverageReporters(config.reporter || coverageConfigDefaults.reporter)
5358
};
5459

5560
this.coverageFilesDirectory = "./.json-schema-coverage";
@@ -111,9 +116,7 @@ class JsonSchemaCoverageProvider {
111116
});
112117

113118
if (this.hasTerminalReporter(this.options.reporter)) {
114-
this.ctx.logger.log(
115-
c.blue(" % ") + c.dim("Coverage report from ") + c.yellow(this.name)
116-
);
119+
this.ctx.logger.log(c.blue(" % ") + c.dim("Coverage report from ") + c.yellow(this.name));
117120
}
118121

119122
for (const reporter of this.options.reporter) {
@@ -129,13 +132,12 @@ class JsonSchemaCoverageProvider {
129132

130133
/** @type (reporters: ResolvedCoverageOptions["reporter"])=> boolean */
131134
hasTerminalReporter(reporters) {
132-
return reporters.some(
133-
([reporter]) =>
134-
reporter === "text"
135+
return reporters.some(([reporter]) => {
136+
return reporter === "text"
135137
|| reporter === "text-summary"
136138
|| reporter === "text-lcov"
137-
|| reporter === "teamcity"
138-
);
139+
|| reporter === "teamcity";
140+
});
139141
}
140142

141143
/** @type () => CoverageMap */

src/vitest/json-schema-matcher.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import "vitest";
33

44
declare module "vitest" {
55
interface Matchers<R = unknown> {
6-
matchJsonSchema: (uriOrSchema: string | SchemaObject) => Promise<R>;
6+
matchJsonSchema: (uriOrSchema: string | SchemaObject | boolean) => Promise<R>;
7+
toMatchJsonSchema: (uriOrSchema: string | SchemaObject | boolean) => Promise<R>;
78
}
89
}

src/vitest/json-schema-matcher.js

Lines changed: 36 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -11,50 +11,53 @@ import { BASIC } from "@hyperjump/json-schema/experimental";
1111
import { TestCoverageEvaluationPlugin } from "../test-coverage-evaluation-plugin.js";
1212

1313
/**
14-
* @import { OutputUnit } from "@hyperjump/json-schema"
14+
* @import { OutputUnit, SchemaObject } from "@hyperjump/json-schema"
15+
* @import { AsyncExpectationResult } from "@vitest/expect"
1516
*/
1617

17-
expect.extend({
18-
async matchJsonSchema(instance, uriOrSchema) {
19-
/** @type OutputUnit */
20-
let output;
18+
/** @type (instance: any, uriOrSchema: string | SchemaObject | boolean) => AsyncExpectationResult */
19+
const schemaMatcher = async (instance, uriOrSchema) => {
20+
/** @type OutputUnit */
21+
let output;
2122

22-
const isCoverageEnabled = existsSync(".json-schema-coverage");
23-
const plugins = isCoverageEnabled
24-
? [new TestCoverageEvaluationPlugin()]
25-
: [];
23+
if (typeof uriOrSchema === "string") {
24+
const uri = uriOrSchema;
2625

27-
if (typeof uriOrSchema === "string") {
28-
const uri = uriOrSchema;
26+
if (existsSync(".json-schema-coverage")) {
27+
const testCoveragePlugin = new TestCoverageEvaluationPlugin();
2928

3029
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
3130
output = await validate(uri, instance, {
3231
outputFormat: BASIC,
33-
plugins: plugins
32+
plugins: [testCoveragePlugin]
3433
});
34+
35+
const coverageMapPath = `.json-schema-coverage/${randomUUID()}.json`;
36+
const coverageMapJson = JSON.stringify(testCoveragePlugin.coverageMap);
37+
await writeFile(coverageMapPath, coverageMapJson);
3538
} else {
36-
const schema = uriOrSchema;
37-
const uri = `urn:uuid:${randomUUID()}`;
38-
registerSchema(schema, uri, "https://json-schema.org/draft/2020-12/schema");
39-
try {
40-
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
41-
output = await validate(uri, instance, {
42-
outputFormat: BASIC,
43-
plugins: plugins
44-
});
45-
} finally {
46-
unregisterSchema(uri);
47-
}
39+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
40+
output = await validate(uri, instance, BASIC);
4841
}
49-
50-
if (isCoverageEnabled) {
51-
const testCoveragePlugin = plugins[0];
52-
await writeFile(`.json-schema-coverage/${randomUUID()}.json`, JSON.stringify(testCoveragePlugin.coverageMap, null, " "));
42+
} else {
43+
const schema = uriOrSchema;
44+
const uri = `urn:uuid:${randomUUID()}`;
45+
registerSchema(schema, uri, "https://json-schema.org/draft/2020-12/schema");
46+
try {
47+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
48+
output = await validate(uri, instance, BASIC);
49+
} finally {
50+
unregisterSchema(uri);
5351
}
54-
55-
return {
56-
pass: output.valid,
57-
message: () => JSON.stringify(output, null, " ")
58-
};
5952
}
53+
54+
return {
55+
pass: output.valid,
56+
message: () => JSON.stringify(output, null, " ")
57+
};
58+
};
59+
60+
expect.extend({
61+
matchJsonSchema: schemaMatcher,
62+
toMatchJsonSchema: schemaMatcher
6063
});

0 commit comments

Comments
 (0)