@altano/vitest-plugins 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.eslintrc.cjs ADDED
@@ -0,0 +1,19 @@
1
+ module.exports = {
2
+ extends: ["altano"],
3
+ overrides: [
4
+ {
5
+ files: ["src/**/!(*.d).{ts,tsx}", "*.config.ts"],
6
+ parserOptions: {
7
+ project: ["./tsconfig.json"],
8
+ tsconfigRootDir: __dirname,
9
+ },
10
+ },
11
+ {
12
+ files: ["tests/**/*.ts", "tests/**/*.tsx"],
13
+ parserOptions: {
14
+ project: ["./tsconfig.json"],
15
+ tsconfigRootDir: `${__dirname}/tests/`,
16
+ },
17
+ },
18
+ ],
19
+ };
@@ -0,0 +1,5 @@
1
+
2
+ > @altano/vitest-plugins@0.0.2 lint /home/runner/work/npm-packages/npm-packages/packages/vitest-plugins
3
+ > eslint package.json src/**/*.ts*
4
+
5
+ Warning: React version was set to "detect" in eslint-plugin-react settings, but the "react" package is not installed. Assuming latest React version for linting.
package/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # @altano/vitest-plugins
2
+
3
+ ## 0.0.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 7d75608: initial package publish
package/README.md ADDED
@@ -0,0 +1,115 @@
1
+ # vitest-plugins
2
+
3
+ [![npm](https://badgen.net/npm/v/@altano/vitest-plugins)](https://www.npmjs.com/package/@altano/vitest-plugins) ![Typed with TypeScript](https://badgen.net/npm/types/@altano/vitest-plugins) ![ESM only](https://badgen.net/badge/module/esm%20only?icon=js)
4
+
5
+ Custom matchers and snapshot serializers to enhance vitest.
6
+
7
+ # Installation
8
+
9
+ ```
10
+ npm install -D @altano/vitest-plugins
11
+ ```
12
+
13
+ ## To install the custom matchers
14
+
15
+ Modify vite.config.ts / vitest.config.ts:
16
+
17
+ ```ts
18
+ export default defineConfig({
19
+ test: {
20
+ setupFiles: [
21
+ "@altano/vitest-plugins/matchers",
22
+ // ...
23
+ ],
24
+ // ...
25
+ },
26
+ });
27
+ ```
28
+
29
+ Add matcher types to your tsconfig.json (if using TypeScript):
30
+
31
+ ```json
32
+ {
33
+ "compilerOptions": {
34
+ "types": ["@altano/vitest-plugins/matchers"]
35
+ }
36
+ }
37
+ ```
38
+
39
+ ## To install the snapshot serializers
40
+
41
+ Modify vite.config.ts / vitest.config.ts:
42
+
43
+ ```ts
44
+ export default defineConfig({
45
+ test: {
46
+ setupFiles: [
47
+ "@altano/vitest-plugins/serializers",
48
+ // ...
49
+ ],
50
+ // ...
51
+ },
52
+ });
53
+ ```
54
+
55
+ NOTE: You can pick and choose what to install: the matchers and serializers don't depend on each other.
56
+
57
+ # Details
58
+
59
+ ## Snapshot Serializers
60
+
61
+ ### VFile
62
+
63
+ Will format the contents of a [vfile](https://unifiedjs.com/explore/package/vfile/) using Prettier (auto-detecting the type from the vfile's filename), e.g.
64
+
65
+ ```snap
66
+ FormattedVFile {
67
+ "cwd": "<cwd>",
68
+ "data": {},
69
+ "history": [
70
+ "tests/unit/__fixtures__/basic/input.js",
71
+ ],
72
+ "map": undefined,
73
+ "messages": [],
74
+ "value": "function face() { }"
75
+ }
76
+ ```
77
+
78
+ ### Absolute Paths
79
+
80
+ Will replace any instances of `process.cwd()` with `<cwd>` in the snapshot. Useful when serializing strings that contain absolute paths, since those will be different on other machines running the tests.
81
+
82
+ ## Matchers
83
+
84
+ Vitest's error matchers let you match against the error message, but not the rest of the Error object:
85
+
86
+ - `toThrow(error?)` - error is thrown ([docs](https://jestjs.io/docs/expect#tothrowerror))
87
+ - `toThrowErrorMatching[Inline]Snapshot` - an error _exactly_ matches a snapshot ([docs](https://jestjs.io/docs/expect#tothrowerrormatchingsnapshothint))
88
+
89
+ If you want to assert anything more complicated (e.g. an error contains some substring in the stack) then you'll need these custom matchers:
90
+
91
+ ### toMatchError
92
+
93
+ Assert on any part of an error object (e.g. the stack):
94
+
95
+ ```ts
96
+ expect(new Error("face")).toMatchError(
97
+ expect.objectContaining({
98
+ stack: expect.stringContaining("readme.spec.ts"),
99
+ }),
100
+ );
101
+ ```
102
+
103
+ ### toThrowErrorMatching
104
+
105
+ Assert on any part of a _thrown_ error object (e.g. the stack):
106
+
107
+ ```ts
108
+ expect(() => {
109
+ throw new Error("face");
110
+ }).toThrowErrorMatching(
111
+ expect.objectContaining({
112
+ stack: expect.stringContaining("readme.spec.ts"),
113
+ }),
114
+ );
115
+ ```
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@altano/vitest-plugins",
3
+ "version": "0.0.2",
4
+ "description": "Custom matchers and snapshot serializers to enhance vitest",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "exports": {
8
+ "./matchers": {
9
+ "default": "./src/matchers/setup.ts",
10
+ "types": "./src/matchers/types.d.ts"
11
+ },
12
+ "./matchers/*": "./src/matchers/*.ts",
13
+ "./serializers": "./src/serializers/setup.ts",
14
+ "./serializers/*": "./src/serializers/*.ts"
15
+ },
16
+ "author": {
17
+ "name": "Alan Norbauer",
18
+ "email": "altano@gmail.com",
19
+ "url": "https://alan.norbauer.com"
20
+ },
21
+ "license": "ISC",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/altano/npm-packages",
25
+ "directory": "packages/vitest-plugins"
26
+ },
27
+ "homepage": "https://github.com/altano/npm-packages/tree/main/packages/vitest-plugins",
28
+ "devDependencies": {
29
+ "@mdx-js/mdx": "^2.3.0",
30
+ "@prettier/sync": "^0.5.1",
31
+ "@types/node": "^20.11.25",
32
+ "@vitest/coverage-v8": "^1.4.0",
33
+ "@vitest/expect": "^1.4.0",
34
+ "@vitest/ui": "^1.4.0",
35
+ "eslint": "^8.57.0",
36
+ "mdast-util-mdxjs-esm": "^2.0.1",
37
+ "prettier": "^3.2.5",
38
+ "typescript": "^5.4.2",
39
+ "unified": "^10.1.2",
40
+ "vfile": "^5.3.7",
41
+ "vitest": "^1.4.0",
42
+ "@altano/tsconfig": "0.0.0",
43
+ "eslint-config-altano": "0.0.0"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "dependencies": {
49
+ "serialize-error": "^11.0.3"
50
+ },
51
+ "scripts": {
52
+ "clean": "rm -rf .turbo && rm -rf .tsbuildinfo && rm -rf node_modules && rm -rf dist",
53
+ "format": "prettier --check src",
54
+ "format:fix": "prettier --write src",
55
+ "lint": "eslint package.json src/**/*.ts*",
56
+ "lint:fix": "TIMING=1 pnpm lint --fix",
57
+ "lint:timing": "TIMING=1 pnpm lint",
58
+ "test:typecheck": "tsc --project ./tests/tsconfig.json --noEmit",
59
+ "test:unit": "vitest --run",
60
+ "test:unit:watch": "vitest",
61
+ "typecheck:src": "tsc --noEmit",
62
+ "typecheck:test": "tsc --noEmit --project ./tests/tsconfig.json"
63
+ }
64
+ }
@@ -0,0 +1,5 @@
1
+ import { expect } from "vitest";
2
+
3
+ type MatchersObject = Parameters<typeof expect.extend>[0];
4
+
5
+ export type Matcher = MatchersObject[keyof MatchersObject];
@@ -0,0 +1,9 @@
1
+ import { expect } from "vitest";
2
+
3
+ import toMatchError from "./toMatchError";
4
+ import toThrowErrorMatching from "./toThrowErrorMatching";
5
+
6
+ expect.extend({
7
+ toMatchError,
8
+ toThrowErrorMatching,
9
+ });
@@ -0,0 +1,18 @@
1
+ import { serializeError } from "serialize-error";
2
+ import type { Matcher } from "./matcher";
3
+
4
+ const toMatchError: Matcher = function (received: Error, expected: unknown) {
5
+ // If we don't serialize the error before passing it to vitest, vitest will
6
+ // output just the error message (e.g. `"[Error: face]"`). If we let people
7
+ // match against the other Error fields (e.g. `stack`) we should display those
8
+ // instead of simplifying them out of the output.
9
+ const serializedError = serializeError(received);
10
+ return {
11
+ message: () => `Expected errors to match`,
12
+ pass: this.equals(received, expected, this.customTesters),
13
+ actual: serializedError,
14
+ expected: expected,
15
+ };
16
+ };
17
+
18
+ export default toMatchError;
@@ -0,0 +1,26 @@
1
+ import toMatchError from "./toMatchError";
2
+ import type { Matcher } from "./matcher";
3
+
4
+ const toThrowErrorMatching: Matcher = function (
5
+ received: () => never,
6
+ expected: unknown,
7
+ ) {
8
+ let error: unknown = null;
9
+ try {
10
+ received();
11
+ } catch (e: unknown) {
12
+ error = e;
13
+ }
14
+ if (error instanceof Error) {
15
+ return toMatchError.call(this, error, expected);
16
+ } else {
17
+ return {
18
+ message: () => `Did not throw an Error`,
19
+ pass: false,
20
+ actual: error,
21
+ expected: `[Error]`,
22
+ };
23
+ }
24
+ };
25
+
26
+ export default toThrowErrorMatching;
@@ -0,0 +1,13 @@
1
+ // Have to import these to get declaration merging.
2
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
3
+ import type { Assertion, AsymmetricMatchersContaining } from "vitest";
4
+
5
+ interface CustomMatchers<R = unknown> {
6
+ toMatchError(expected: unknown): R;
7
+ toThrowErrorMatching(expected: unknown): R;
8
+ }
9
+
10
+ declare module "vitest" {
11
+ interface Assertion<T> extends CustomMatchers<T> {}
12
+ interface AsymmetricMatchersContaining extends CustomMatchers {}
13
+ }
@@ -0,0 +1,30 @@
1
+ import process from "node:process";
2
+ import type { SnapshotSerializer } from "vitest";
3
+
4
+ function getCWD(): string {
5
+ return process.cwd();
6
+ }
7
+
8
+ /**
9
+ * Strip absolute paths from the output to make tests portable
10
+ */
11
+ function stripAbsolutePaths(str: string): string {
12
+ return str.replaceAll(getCWD(), "<cwd>");
13
+ }
14
+
15
+ const serializer: SnapshotSerializer = {
16
+ serialize(val: string, config, indentation, depth, refs, printer) {
17
+ return printer(stripAbsolutePaths(val), config, indentation, depth, refs);
18
+ },
19
+ test(val) {
20
+ return (
21
+ typeof val === "string" &&
22
+ // This is potentially really expensive, given large strings, but I can't
23
+ // find a better way to do this. Without this check, the string will keep
24
+ // getting reprocessed by this serializer over and over.
25
+ val.includes(getCWD())
26
+ );
27
+ },
28
+ };
29
+
30
+ export default serializer;
@@ -0,0 +1,7 @@
1
+ import { expect } from "vitest";
2
+
3
+ import absolutePathSerializer from "./absolutePath";
4
+ expect.addSnapshotSerializer(absolutePathSerializer);
5
+
6
+ import vFileSerializer from "./vFile";
7
+ expect.addSnapshotSerializer(vFileSerializer);
@@ -0,0 +1,45 @@
1
+ import prettier from "@prettier/sync";
2
+ import prettierConfig from "../../../../.prettierrc.json";
3
+ import { VFile, type Value as VFileValue } from "vfile";
4
+ import type { Options } from "prettier";
5
+ import type { SnapshotSerializer } from "vitest";
6
+
7
+ class FormattedVFile extends VFile {
8
+ constructor(vfile: VFile, formattedContents: VFileValue) {
9
+ super(vfile);
10
+ this.value = formattedContents;
11
+ }
12
+ }
13
+
14
+ const vFileSerializer: SnapshotSerializer = {
15
+ serialize(vfile: VFile, config, indentation, depth, refs, printer) {
16
+ const unformattedContents = vfile.toString();
17
+ let vFileToPrint: VFile;
18
+
19
+ try {
20
+ const formattedContents = prettier
21
+ .format(unformattedContents, {
22
+ filepath: vfile.path,
23
+ ...(prettierConfig as Options),
24
+ })
25
+ .trim();
26
+ vFileToPrint = new FormattedVFile(vfile, formattedContents);
27
+ } catch (e: unknown) {
28
+ if (e instanceof Error && e.name === "UndefinedParserError") {
29
+ // Prettier doesn't have a parser for this vfile. That's fine, pass it
30
+ // through unformatted.
31
+ vFileToPrint = new FormattedVFile(vfile, vfile.value);
32
+ } else {
33
+ // Rethrow any other errors
34
+ throw e;
35
+ }
36
+ }
37
+
38
+ return printer(vFileToPrint, config, indentation, depth, refs);
39
+ },
40
+ test(val: unknown) {
41
+ return val instanceof VFile && !(val instanceof FormattedVFile);
42
+ },
43
+ };
44
+
45
+ export default vFileSerializer;
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../tsconfig.json",
3
+ "include": ["."],
4
+ "compilerOptions": {
5
+ "noEmit": true,
6
+ "rootDir": "..",
7
+ "types": ["../src/matchers/types"]
8
+ }
9
+ }
@@ -0,0 +1,21 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import "../../../src/matchers/setup";
3
+
4
+ describe("readme", () => {
5
+ it("should describe toMatchError", () => {
6
+ expect(new Error("face")).toMatchError(
7
+ expect.objectContaining({
8
+ stack: expect.stringContaining("readme.spec.ts"),
9
+ }),
10
+ );
11
+ });
12
+ it("should describe toThrowErrorMatching", () => {
13
+ expect(() => {
14
+ throw new Error("face");
15
+ }).toThrowErrorMatching(
16
+ expect.objectContaining({
17
+ stack: expect.stringContaining("readme.spec.ts"),
18
+ }),
19
+ );
20
+ });
21
+ });
@@ -0,0 +1,45 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import "../../../src/matchers/setup";
3
+
4
+ describe("matchers", () => {
5
+ describe("toMatchError", () => {
6
+ it("should match an error exactly", () => {
7
+ expect(new Error("face")).toMatchError(
8
+ expect.objectContaining({
9
+ message: "face",
10
+ name: "Error",
11
+ stack: expect.any(String),
12
+ }),
13
+ );
14
+ });
15
+ it("should match an error by substring", () => {
16
+ expect(new Error("your stupid face")).toMatchError(
17
+ expect.objectContaining({
18
+ message: expect.stringContaining("stupid"),
19
+ }),
20
+ );
21
+ });
22
+ it("should NOT match an invalid string", () => {
23
+ expect(new Error("face")).not.toMatchError(
24
+ expect.objectContaining({
25
+ message: "hat",
26
+ }),
27
+ );
28
+ });
29
+ it("should throw an error when there is no match", () => {
30
+ expect(() => {
31
+ expect(new Error("face")).toMatchError(
32
+ expect.objectContaining({
33
+ message: "hat",
34
+ }),
35
+ );
36
+ }).toThrowErrorMatchingInlineSnapshot(
37
+ `[Error: Expected errors to match]`,
38
+ );
39
+ });
40
+ it.todo("should display the other Error fields in the error", () => {
41
+ // I don't know how to capture vitest's full error output to verify it
42
+ // contains what we expect.
43
+ });
44
+ });
45
+ });
@@ -0,0 +1,31 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import "../../../src/matchers/setup";
3
+
4
+ describe("matchers", () => {
5
+ describe("toThrowErrorMatching", () => {
6
+ it("should match an error exactly", () => {
7
+ expect(() => {
8
+ throw new Error("face");
9
+ }).toThrow("fac");
10
+ });
11
+ it("should NOT match an just the message", () => {
12
+ expect(() => {
13
+ throw new Error("face");
14
+ }).not.toThrowErrorMatching("face");
15
+ });
16
+ it("should NOT match an invalid message", () => {
17
+ expect(() => {
18
+ throw new Error("face");
19
+ }).not.toThrowErrorMatching(
20
+ expect.objectContaining({
21
+ message: "hat",
22
+ }),
23
+ );
24
+ });
25
+ it("should throw an error when an error wasn't thrown", () => {
26
+ expect(() => {
27
+ expect(() => 1).toThrowErrorMatching("face");
28
+ }).toThrowErrorMatchingInlineSnapshot(`[Error: Did not throw an Error]`);
29
+ });
30
+ });
31
+ });
@@ -0,0 +1,16 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import absolutePathSerializer from "../../../src/serializers/absolutePath";
4
+ expect.addSnapshotSerializer(absolutePathSerializer);
5
+
6
+ describe("serializers", () => {
7
+ describe("absolutePath", () => {
8
+ it("should remove absolute file paths in a string", () => {
9
+ expect(
10
+ `String with an absolute path: ${import.meta.dirname}`,
11
+ ).toMatchInlineSnapshot(
12
+ `"String with an absolute path: <cwd>/tests/unit/serializers"`,
13
+ );
14
+ });
15
+ });
16
+ });
@@ -0,0 +1,80 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { VFile } from "vfile";
3
+
4
+ // We're not testing the absolute path serializer, but the test isn't
5
+ // reproducible without it. So just add both.
6
+ import "../../../src/serializers/setup";
7
+
8
+ describe("serializers", () => {
9
+ describe("vFile", () => {
10
+ it("should produce a snapshot for a plain text VFile (not Prettier supported)", () => {
11
+ const file = new VFile({
12
+ path: "~/example.txt",
13
+ value: "Alpha *braavo* charlie.",
14
+ });
15
+ expect(file).toMatchInlineSnapshot(`
16
+ FormattedVFile {
17
+ "cwd": "<cwd>",
18
+ "data": {},
19
+ "history": [
20
+ "~/example.txt",
21
+ ],
22
+ "messages": [],
23
+ "value": "Alpha *braavo* charlie.",
24
+ }
25
+ `);
26
+ });
27
+ it("should produce a formatted snapshot for a Prettier-compatible file", () => {
28
+ const file = new VFile({
29
+ path: "~/example.js",
30
+ value: `function face( ) {console.log("hi" ); }`,
31
+ });
32
+ expect(file).toMatchInlineSnapshot(`
33
+ FormattedVFile {
34
+ "cwd": "<cwd>",
35
+ "data": {},
36
+ "history": [
37
+ "~/example.js",
38
+ ],
39
+ "messages": [],
40
+ "value": "function face() {
41
+ console.log("hi");
42
+ }",
43
+ }
44
+ `);
45
+ });
46
+ it("should throw if given a file that Prettier supports but can't format", () => {
47
+ const file = new VFile({
48
+ path: "~/example.js",
49
+ value: `function ( ) {1 }`,
50
+ });
51
+ file.cwd = "/root/path"; // make snapshot reproducible
52
+ expect(() => {
53
+ expect(file).toMatchInlineSnapshot(`"function ( ) {1 }"`);
54
+ }).toThrowErrorMatchingInlineSnapshot(`
55
+ [PrettyFormatPluginError: Unexpected token (1:10)
56
+ > 1 | function ( ) {1 }
57
+ | ^]
58
+ `);
59
+ });
60
+ it("should remove absolute file paths in a vFile", () => {
61
+ const file = new VFile({
62
+ path: "~/example.json",
63
+ value: `{ "key": "${import.meta.dirname}" }`,
64
+ });
65
+ expect(file).toMatchInlineSnapshot(`
66
+ FormattedVFile {
67
+ "cwd": "<cwd>",
68
+ "data": {},
69
+ "history": [
70
+ "~/example.json",
71
+ ],
72
+ "messages": [],
73
+ "value": "{
74
+ "key": "<cwd>/tests/unit/serializers"
75
+ }",
76
+ }
77
+ `);
78
+ });
79
+ });
80
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "@altano/tsconfig/node.json",
3
+ "include": ["src", "*.config.ts"]
4
+ }
@@ -0,0 +1,24 @@
1
+ import { defineProject } from "vitest/config";
2
+
3
+ export default defineProject({
4
+ test: {
5
+ dir: "tests/unit",
6
+ coverage: {
7
+ enabled: true,
8
+
9
+ exclude: [
10
+ "tests/e2e",
11
+ "playwright.config.ts",
12
+ ".eslintrc.cjs",
13
+ "tests/unit/**/*.benchmark.tsx",
14
+ ],
15
+
16
+ provider: "v8",
17
+ all: true,
18
+ clean: true,
19
+ skipFull: true,
20
+ thresholds: { "100": true },
21
+ reportsDirectory: ".coverage",
22
+ },
23
+ },
24
+ });