-
Notifications
You must be signed in to change notification settings - Fork 13.3k
Expand file tree
/
Copy pathcustomMatchers.ts
More file actions
131 lines (112 loc) · 4.16 KB
/
customMatchers.ts
File metadata and controls
131 lines (112 loc) · 4.16 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/// <reference types="vitest/globals" />
import { expect, type Assertion } from 'vitest';
import path from 'node:path';
import stripAnsi from 'strip-ansi';
import type { TextBuffer } from '../ui/components/shared/text-buffer.js';
// RegExp to detect invalid characters: backspace, and ANSI escape codes
// eslint-disable-next-line no-control-regex
const invalidCharsRegex = /[\b\x1b]/;
const callCountByTest = new Map<string, number>();
export async function toMatchSvgSnapshot(
this: Assertion,
renderInstance: {
lastFrameRaw?: (options?: { allowEmpty?: boolean }) => string;
lastFrame?: (options?: { allowEmpty?: boolean }) => string;
generateSvg: () => string;
},
options?: { allowEmpty?: boolean; name?: string },
) {
const currentTestName = expect.getState().currentTestName;
if (!currentTestName) {
throw new Error('toMatchSvgSnapshot must be called within a test');
}
const testPath = expect.getState().testPath;
if (!testPath) {
throw new Error('toMatchSvgSnapshot requires testPath');
}
let textContent: string;
if (renderInstance.lastFrameRaw) {
textContent = renderInstance.lastFrameRaw({
allowEmpty: options?.allowEmpty,
});
} else if (renderInstance.lastFrame) {
textContent = renderInstance.lastFrame({ allowEmpty: options?.allowEmpty });
} else {
throw new Error(
'toMatchSvgSnapshot requires a renderInstance with either lastFrameRaw or lastFrame',
);
}
const svgContent = renderInstance.generateSvg();
const sanitize = (name: string) =>
name.replace(/[^a-zA-Z0-9_-]/g, '-').replace(/-+/g, '-');
const testId = testPath + ':' + currentTestName;
let count = callCountByTest.get(testId) ?? 0;
count++;
callCountByTest.set(testId, count);
const snapshotName =
options?.name ??
(count > 1 ? `${currentTestName}-${count}` : currentTestName);
const svgFileName =
sanitize(path.basename(testPath).replace(/\.test\.tsx?$/, '')) +
'-' +
sanitize(snapshotName) +
'.snap.svg';
const svgDir = path.join(path.dirname(testPath), '__snapshots__');
const svgFilePath = path.join(svgDir, svgFileName);
// Assert the text matches standard snapshot, stripping ANSI for stability
expect(stripAnsi(textContent)).toMatchSnapshot();
// Assert the SVG matches the file snapshot
await expect(svgContent).toMatchFileSnapshot(svgFilePath);
return { pass: true, message: () => '' };
}
function toHaveOnlyValidCharacters(this: Assertion, buffer: TextBuffer) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { isNot } = this as any;
let pass = true;
const invalidLines: Array<{ line: number; content: string }> = [];
for (let i = 0; i < buffer.lines.length; i++) {
const line = buffer.lines[i];
if (line.includes('\n')) {
pass = false;
invalidLines.push({ line: i, content: line });
break; // Fail fast on newlines
}
if (invalidCharsRegex.test(line)) {
pass = false;
invalidLines.push({ line: i, content: line });
}
}
return {
pass,
message: () =>
`Expected buffer ${isNot ? 'not ' : ''}to have only valid characters, but found invalid characters in lines:\n${invalidLines
.map((l) => ` [${l.line}]: "${l.content}"`) /* This line was changed */
.join('\n')}`,
actual: buffer.lines,
expected: 'Lines with no line breaks, backspaces, or escape codes.',
};
}
expect.extend({
toHaveOnlyValidCharacters,
toMatchSvgSnapshot,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any);
// Extend Vitest's `expect` interface with the custom matcher's type definition.
declare module 'vitest' {
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-empty-object-type
interface Assertion<T = any> extends CustomMatchers<T> {}
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
interface AsymmetricMatchersContaining extends CustomMatchers {}
interface CustomMatchers<T = unknown> {
toHaveOnlyValidCharacters(): T;
toMatchSvgSnapshot(options?: {
allowEmpty?: boolean;
name?: string;
}): Promise<void>;
}
}