Skip to content

Commit 7a49e36

Browse files
committed
test(test-setup): simplify custom matchers
1 parent f84260d commit 7a49e36

File tree

3 files changed

+84
-177
lines changed

3 files changed

+84
-177
lines changed

testing/test-setup/src/lib/extend/ui-logger.matcher.ts

Lines changed: 32 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -2,151 +2,66 @@ import { cliui } from '@poppinss/cliui';
22
import type { SyncExpectationResult } from '@vitest/expect';
33
import { expect } from 'vitest';
44
import {
5+
type ExpectedMessage,
56
type LogLevel,
6-
extractLevel,
7-
extractMessage,
7+
extractLogDetails,
88
hasExpectedMessage,
9-
messageContains,
109
} from './ui-logger.matcher.utils';
1110

1211
type CliUi = ReturnType<typeof cliui>;
1312

1413
export type CustomUiLoggerMatchers = {
15-
toHaveLoggedMessage: (expected: string) => void;
16-
toHaveLoggedNthMessage: (nth: number, expected: string) => void;
17-
toHaveLoggedLevel: (expected: LogLevel) => void;
18-
toHaveLoggedNthLevel: (nth: number, expected: LogLevel) => void;
19-
toHaveLoggedMessageContaining: (expected: string) => void;
20-
toHaveLoggedNthMessageContaining: (nth: number, expected: string) => void;
21-
toHaveLogged: () => void;
14+
toHaveLogged: (level: LogLevel, message: ExpectedMessage) => void;
15+
toHaveNthLogged: (
16+
nth: number,
17+
level: LogLevel,
18+
message: ExpectedMessage,
19+
) => void;
2220
toHaveLoggedTimes: (times: number) => void;
21+
toHaveLogs: () => void;
2322
};
2423

2524
expect.extend({
26-
toHaveLoggedMessage: assertMessageLogged,
27-
toHaveLoggedNthMessage: assertNthMessageLogged,
28-
toHaveLoggedLevel: assertLevelLogged,
29-
toHaveLoggedNthLevel: assertNthLevelLogged,
30-
toHaveLoggedMessageContaining: assertMessageContaining,
31-
toHaveLoggedNthMessageContaining: assertNthMessageContaining,
32-
toHaveLogged: assertLogs,
25+
toHaveLogged: assertLogged,
26+
toHaveNthLogged: assertNthLogged,
3327
toHaveLoggedTimes: assertLogCount,
28+
toHaveLogs: assertLogs,
3429
});
3530

36-
function assertMessageLogged(
31+
function assertLogged(
3732
actual: CliUi,
38-
expected: string,
33+
level: LogLevel,
34+
message: ExpectedMessage,
3935
): SyncExpectationResult {
40-
const messages = actual.logger
41-
.getRenderer()
42-
.getLogs()
43-
.map(({ message }) => extractMessage(message));
36+
const logs = extractLogDetails(actual.logger);
4437

45-
const pass = messages.some(msg => hasExpectedMessage(expected, msg));
38+
const pass = logs.some(
39+
log => log.level === level && hasExpectedMessage(message, log.message),
40+
);
4641
return {
4742
pass,
4843
message: () =>
4944
pass
50-
? `Expected not to have logged: ${expected}`
51-
: `Expected to have logged: ${expected}}`,
45+
? `Expected not to find a log with level "${level}" and message matching: ${message}`
46+
: `Expected a log with level "${level}" and message matching: ${message}`,
5247
};
5348
}
5449

55-
function assertNthMessageLogged(
50+
function assertNthLogged(
5651
actual: CliUi,
5752
nth: number,
58-
expected: string,
53+
level: LogLevel,
54+
message: ExpectedMessage,
5955
): SyncExpectationResult {
60-
const messages = actual.logger
61-
.getRenderer()
62-
.getLogs()
63-
.map(({ message }) => extractMessage(message));
56+
const log = extractLogDetails(actual.logger)[nth - 1];
6457

65-
const pass = hasExpectedMessage(expected, messages[nth - 1]);
58+
const pass = log?.level === level && hasExpectedMessage(message, log.message);
6659
return {
6760
pass,
6861
message: () =>
6962
pass
70-
? `Expected not to have logged at position ${nth}: ${expected}`
71-
: `Expected to have logged at position ${nth}: ${expected}`,
72-
};
73-
}
74-
75-
function assertLevelLogged(
76-
actual: CliUi,
77-
expected: LogLevel,
78-
): SyncExpectationResult {
79-
const levels = actual.logger
80-
.getRenderer()
81-
.getLogs()
82-
.map(({ message }) => extractLevel(message));
83-
84-
const pass = levels.includes(expected);
85-
return {
86-
pass,
87-
message: () =>
88-
pass
89-
? `Expected not to have ${expected} log level`
90-
: `Expected to have ${expected} log level`,
91-
};
92-
}
93-
94-
function assertNthLevelLogged(
95-
actual: CliUi,
96-
nth: number,
97-
expected: LogLevel,
98-
): SyncExpectationResult {
99-
const levels = actual.logger
100-
.getRenderer()
101-
.getLogs()
102-
.map(({ message }) => extractLevel(message));
103-
104-
const pass = levels[nth - 1] === expected;
105-
return {
106-
pass,
107-
message: () =>
108-
pass
109-
? `Expected not to have log level at position ${nth}: ${expected}`
110-
: `Expected to have log level at position ${nth}: ${expected}`,
111-
};
112-
}
113-
114-
function assertMessageContaining(
115-
actual: CliUi,
116-
expected: string,
117-
): SyncExpectationResult {
118-
const messages = actual.logger
119-
.getRenderer()
120-
.getLogs()
121-
.map(({ message }) => extractMessage(message));
122-
123-
const pass = messages.some(msg => messageContains(expected, msg));
124-
return {
125-
pass,
126-
message: () =>
127-
pass
128-
? `Expected not to find a message containing: ${expected}`
129-
: `Expected to find a message containing: ${expected}, but none matched.`,
130-
};
131-
}
132-
133-
function assertNthMessageContaining(
134-
actual: CliUi,
135-
nth: number,
136-
expected: string,
137-
): SyncExpectationResult {
138-
const messages = actual.logger
139-
.getRenderer()
140-
.getLogs()
141-
.map(({ message }) => extractMessage(message));
142-
143-
const pass = messageContains(expected, messages[nth - 1]);
144-
return {
145-
pass,
146-
message: () =>
147-
pass
148-
? `Expected not to find the fragment "${expected}" in the message at position ${nth}`
149-
: `Expected to find the fragment "${expected}" in the message at position ${nth}, but it was not found.`,
63+
? `Expected not to find a log at position ${nth} with level "${level}" and message matching: ${message}`
64+
: `Expected a log at position ${nth} with level "${level}" and message matching: ${message}`,
15065
};
15166
}
15267

@@ -158,8 +73,8 @@ function assertLogs(actual: CliUi): SyncExpectationResult {
15873
pass,
15974
message: () =>
16075
pass
161-
? `Expected not to have any logs`
162-
: `Expected to have some logs, but no logs were produced`,
76+
? `Expected no logs, but found ${logs.length}`
77+
: `Expected some logs, but no logs were produced`,
16378
};
16479
}
16580

@@ -174,7 +89,7 @@ function assertLogCount(
17489
pass,
17590
message: () =>
17691
pass
177-
? `Expected not to have exactly ${expected} logs`
178-
: `Expected to have ${expected} logs, but got ${logs.length}`,
92+
? `Expected not to find exactly ${expected} logs, but found ${logs.length}`
93+
: `Expected exactly ${expected} logs, but found ${logs.length}`,
17994
};
18095
}
Lines changed: 41 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,22 @@
1+
import type { Logger } from '@poppinss/cliui';
12
import type { LoggingTypes } from '@poppinss/cliui/build/src/types';
23
import { removeColorCodes } from '@code-pushup/test-utils';
34

4-
export type LogLevel = Exclude<LoggingTypes, 'warning'> | 'warn';
5+
export type LogLevel = Exclude<LoggingTypes, 'warning'> | 'warn' | 'log';
6+
7+
export type ExpectedMessage =
8+
| string
9+
| { asymmetricMatch: (value: string) => boolean };
10+
11+
type ExtractedMessage = {
12+
styledMessage: string;
13+
unstyledMessage: string;
14+
};
15+
16+
type LogDetails = {
17+
level: LogLevel;
18+
message: ExtractedMessage;
19+
};
520

621
const LOG_LEVELS = new Set<LogLevel>([
722
'success',
@@ -11,17 +26,25 @@ const LOG_LEVELS = new Set<LogLevel>([
1126
'debug',
1227
'await',
1328
'warn',
29+
'log',
1430
]);
1531

16-
type ExtractedMessage = {
17-
styledMessage: string;
18-
unstyledMessage: string;
19-
};
32+
export function extractLogDetails(logger: Logger): LogDetails[] {
33+
return logger
34+
.getRenderer()
35+
.getLogs()
36+
.map(
37+
({ message }): LogDetails => ({
38+
level: extractLevel(message),
39+
message: extractMessage(message),
40+
}),
41+
);
42+
}
2043

21-
export function extractLevel(log: string): LogLevel | null {
44+
export function extractLevel(log: string): LogLevel {
2245
const match = removeColorCodes(log).match(/^\[\s*\w+\((?<level>\w+)\)\s*]/);
2346
const level = match?.groups?.['level'] as LogLevel | undefined;
24-
return level && LOG_LEVELS.has(level) ? level : null;
47+
return level && LOG_LEVELS.has(level) ? level : 'log';
2548
}
2649

2750
export function extractMessage(log: string): ExtractedMessage {
@@ -34,26 +57,27 @@ export function extractMessage(log: string): ExtractedMessage {
3457
}
3558

3659
export function hasExpectedMessage(
37-
expected: string,
60+
expected: ExpectedMessage,
3861
message: ExtractedMessage | undefined,
3962
): boolean {
4063
if (!message) {
4164
return false;
4265
}
66+
if (isAsymmetricMatcher(expected)) {
67+
return (
68+
expected.asymmetricMatch(message.styledMessage) ||
69+
expected.asymmetricMatch(message.unstyledMessage)
70+
);
71+
}
4372
return (
4473
message.styledMessage === expected || message.unstyledMessage === expected
4574
);
4675
}
4776

48-
export function messageContains(
49-
expected: string,
50-
message: ExtractedMessage | undefined,
51-
): boolean {
52-
if (!message) {
53-
return false;
54-
}
77+
function isAsymmetricMatcher(
78+
value: unknown,
79+
): value is { asymmetricMatch: (input: string) => boolean } {
5580
return (
56-
message.styledMessage.includes(expected) ||
57-
message.unstyledMessage.includes(expected)
81+
typeof value === 'object' && value != null && 'asymmetricMatch' in value
5882
);
5983
}

testing/test-setup/src/lib/extend/ui-logger.matcher.utils.unit.test.ts

Lines changed: 11 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import {
33
extractLevel,
44
extractMessage,
55
hasExpectedMessage,
6-
messageContains,
76
} from './ui-logger.matcher.utils';
87

98
describe('extractLevel', () => {
@@ -15,12 +14,12 @@ describe('extractLevel', () => {
1514
expect(extractLevel('[ yellow(warn) ] Warning message')).toBe('warn');
1615
});
1716

18-
it('should return null for a log without a level', () => {
19-
expect(extractLevel('Message without level')).toBeNull();
17+
it('should fall back to a default log level for a log without a level', () => {
18+
expect(extractLevel('Message without level')).toBe('log');
2019
});
2120

22-
it('should return null for an invalid log level', () => {
23-
expect(extractLevel('[ unknown ] Message with invalid level')).toBeNull();
21+
it('should fall back to a default log level for an invalid log level', () => {
22+
expect(extractLevel('[ unknown ] Message with invalid level')).toBe('log');
2423
});
2524
});
2625

@@ -40,13 +39,6 @@ describe('extractMessage', () => {
4039
expect(styledMessage).toBe('Warning message without styles.');
4140
expect(unstyledMessage).toBe('Warning message without styles.');
4241
});
43-
44-
it('should return raw log for unmatchable logs', () => {
45-
const log = 'Unmatchable log format';
46-
const { styledMessage, unstyledMessage } = extractMessage(log);
47-
expect(styledMessage).toBe(log);
48-
expect(unstyledMessage).toBe(log);
49-
});
5042
});
5143

5244
describe('hasExpectedMessage', () => {
@@ -78,37 +70,13 @@ describe('hasExpectedMessage', () => {
7870
const result = hasExpectedMessage('Expected message', undefined);
7971
expect(result).toBe(false);
8072
});
81-
});
82-
83-
describe('messageContains', () => {
84-
it('should return true when styled message contains the substring', () => {
85-
expect(
86-
messageContains('message', {
87-
styledMessage: 'Styled message content',
88-
unstyledMessage: 'Plain message content',
89-
}),
90-
).toBe(true);
91-
});
92-
93-
it('should return true when unstyled message contains the substring', () => {
94-
expect(
95-
messageContains('Plain', {
96-
styledMessage: 'Styled message content',
97-
unstyledMessage: 'Plain message content',
98-
}),
99-
).toBe(true);
100-
});
10173

102-
it('should return false when neither message contains the substring', () => {
103-
expect(
104-
messageContains('Non-existent', {
105-
styledMessage: 'Styled message content',
106-
unstyledMessage: 'Plain message content',
107-
}),
108-
).toBe(false);
109-
});
110-
111-
it('should return false for undefined message', () => {
112-
expect(messageContains('Expected substring', undefined)).toBe(false);
74+
it('should handle asymmetric matchers', () => {
75+
const asymmetricMatcher = expect.stringContaining('Styled');
76+
const result = hasExpectedMessage(asymmetricMatcher, {
77+
styledMessage: 'Styled message',
78+
unstyledMessage: 'Plain message',
79+
});
80+
expect(result).toBe(true);
11381
});
11482
});

0 commit comments

Comments
 (0)