-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathproxyConsole.ts
More file actions
215 lines (188 loc) · 5.58 KB
/
Copy pathproxyConsole.ts
File metadata and controls
215 lines (188 loc) · 5.58 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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
import { stripVTControlCharacters as stripAnsi, styleText } from 'node:util';
import { toPosixPath } from './toPosixPath.js';
export type ConsoleType = 'error' | 'group' | 'info' | 'log' | 'table' | 'warn';
type LogPattern = string | RegExp | ((log: string) => boolean);
type MatchPatternOptions = {
/**
* Whether to use exact line matching instead of substring matching.
* @default false
*/
strict?: boolean;
/**
* Whether to convert file paths to POSIX format before matching.
* @default false
*/
posix?: boolean;
};
const matchPattern = (
log: string,
pattern: LogPattern,
options: MatchPatternOptions = {},
) => {
const logToCheck = options.posix ? toPosixPath(log) : log;
if (typeof pattern === 'string') {
return options.strict
? logToCheck.split('\n').some((line) => line.trim() === pattern.trim())
: logToCheck.includes(pattern);
}
if (pattern instanceof RegExp) {
return pattern.test(logToCheck);
}
return pattern(logToCheck);
};
export const createLogHelper = () => {
const logs: string[] = [];
const originalLogs: string[] = [];
let rawOutput = '';
const logPatterns = new Set<{
pattern: LogPattern;
resolve: (value: boolean) => void;
options: MatchPatternOptions;
}>();
const clearLogs = () => {
logs.splice(0);
rawOutput = '';
};
const addLog = (input: string, options?: { newline?: boolean }) => {
const log = stripAnsi(input);
logs.push(log);
originalLogs.push(input);
rawOutput += options?.newline ? `${input}\n` : input;
for (const { pattern, resolve, options } of logPatterns) {
if (matchPattern(log, pattern, options)) {
resolve(true);
}
}
};
const expectLog = async (
pattern: LogPattern,
options: MatchPatternOptions = {},
) => {
if (logs.some((log) => matchPattern(log, pattern, options))) {
return true;
}
return new Promise<boolean>((resolve, reject) => {
const timeoutId = setTimeout(() => {
const title = styleText(
['bold', 'red'],
'Timeout: Expected log not found within 5 seconds.',
);
const expected = styleText('yellow', pattern.toString());
reject(
new Error(
`${title}\nExpect: ${expected}\nGet:\n${originalLogs.join('\n')}`,
),
);
}, 5000);
const patternEntry = {
pattern,
options,
resolve: (value: boolean) => {
clearTimeout(timeoutId);
logPatterns.delete(patternEntry);
resolve(value);
},
};
logPatterns.add(patternEntry);
});
};
const expectNoLog = (
pattern: LogPattern,
options: MatchPatternOptions = {},
) => {
const result = logs.some((log) => matchPattern(log, pattern, options));
if (result) {
const title = styleText(['bold', 'red'], 'Unexpected log found.');
const unexpected = styleText('yellow', pattern.toString());
throw new Error(
`${title}\nUnexpected: ${unexpected}\nGet:\n${originalLogs.join('\n')}`,
);
}
};
/** Assert the number of non-overlapping matches in the captured output. */
const expectLogTimes = (pattern: string | RegExp, times: number) => {
const output = stripAnsi(rawOutput);
let actualTimes = 0;
if (typeof pattern === 'string') {
let position = 0;
while (position <= output.length) {
position = output.indexOf(pattern, position);
if (position === -1) {
break;
}
actualTimes++;
position += pattern.length || 1;
}
} else {
const regexp = new RegExp(
pattern.source,
pattern.flags.includes('g') ? pattern.flags : `${pattern.flags}g`,
);
actualTimes = output.match(regexp)?.length ?? 0;
}
if (actualTimes !== times) {
const title = styleText(['bold', 'red'], 'Unexpected log count.');
const expected = styleText('yellow', pattern.toString());
throw new Error(
`${title}\nPattern: ${expected}\nExpected: ${times}\nReceived: ${actualTimes}\nGet:\n${originalLogs.join('\n')}`,
);
}
};
return {
logs,
originalLogs,
addLog,
clearLogs,
expectLog,
expectNoLog,
expectLogTimes,
};
};
export type LogHelper = ReturnType<typeof createLogHelper>;
export type ProxyConsoleOptions = {
types?: ConsoleType | ConsoleType[];
};
export type ExtendedLogHelper = LogHelper & {
/** Restore the original console methods. */
restore: () => void;
/** Restore the original console methods and print the captured logs. */
printCapturedLogs: () => void;
};
/** Proxy the console methods to capture logs. */
export const proxyConsole = ({
types = ['log', 'warn', 'info', 'error'],
}: ProxyConsoleOptions = {}): ExtendedLogHelper => {
const restores: Array<() => void> = [];
const logHelper = createLogHelper();
for (const type of Array.isArray(types) ? types : [types]) {
const method = console[type];
restores.push(() => {
console[type] = method;
});
console[type] = (...args: unknown[]) => {
const logMessage = args
.map((arg) => {
if (typeof arg === 'string') {
return arg;
}
return typeof arg === 'object' ? JSON.stringify(arg) : String(arg);
})
.join(' ');
logHelper.addLog(logMessage, { newline: true });
};
}
const restore = () => {
for (const restoreMethod of restores) {
restoreMethod();
}
};
const printCapturedLogs = () => {
restore();
console.log(logHelper.originalLogs.join('\n'));
};
return {
restore,
printCapturedLogs,
...logHelper,
};
};