Custom matcher for ui logger
User story
As a developer I want to have a good signal to noise ration and want to write my tests with as less boilerplate as possible. Therefor I need a custom matcher for the UI logger.
Bad example:
// ui().logger.error('Something went wrong');
expect(ui().logger.getRenderer().getLogs()).toStrictEqual([
{
stream: 'stderr',
message: '[ red(error) ] Something went wrong'
}
])
Suggested matcher:
expect(ui()).toHaveLogged('...')expect(ui()).toHaveLoggedNth(1, '...')expect(ui()).not.toHaveLogged())
Conversation: https://github.com/code-pushup/cli/pull/487#discussion_r1517516666
Good example:
// ui().logger.error('Something went wrong');
expect(ui()).toHaveLoggedLevelNth(1, 'error');
expect(ui()).toHaveLoggedMessageNth(1, 'Something went wrong');
Acceptance criteria
I want to be able to use the custom matcher in the following libs:
- [ ] vitest
- jest (optional)
Internal logic:
- [ ] it handles removes ASCII color
expect(msg.replace(/\u001B\[\d+m/g, '')).toBe("[ blue(info) ] This is is blue in terminal") - custom matcher
toMatchPlainCharacterinstead ofremoveColorCodes(optional) I want to be able to have different matcher present: - [ ]
toHaveLoggedLevel('...') - [ ]
toHaveLoggedLevelNth(1, '...') - [ ]
toHaveLoggedMessage('...') - [ ]
toHaveLoggedMessageNth(1, '...')
Implementation details
import { expect } from 'vitest';
import {cliui} from "@poppinss/cliui";
declare module 'vitest' {
export interface Assert {
toHaveLoggedMessage(expected: string): void;
toHaveLoggedMessageNth(nth: number, expected: string): void;
}
}
export type CliUi = ReturnType<typeof cliui>;
// Implementation of toHaveLogged
expect.extend({
toHaveLoggedMessage(received: CliUi, expected: string) {
const logs = received.logger.getRenderer().getLogs();
const pass = logs.some(log => log.message === expected);
return {
pass,
message: () => pass
? `expected not to have logged: ${expected}`
: `expected to have logged: ${expected}`,
};
},
toHaveLoggedMessageNth(received: CliUi, nth: number, expected: string) {
const logs = received.logger.getRenderer().getLogs();
const pass = (logs[nth - 1]?.message === expected) ?? false;
return {
pass,
message: () => pass
? `expected not to have logged at position ${nth}: ${expected}`
: `expected to have logged at position ${nth}: ${expected}`,
};
},
});
Usage in global-setup.ts
import './testing/test-utils/src/lib/matcher/ui-logger';
Suggestions:
- Add a helper function to
test-utilsthat returns Nth log directly as a string. - If applicable, distinguish formatted (bold, colour code) and unformatted logs.
Feedback from @matejchalk:
On balance, I'd go with no style testing. The text content is the key part of the assertion for me. Although if create a custom matcher, we could theoretically support both. E.g. .toHaveLogged('...') would match unstyled text, but .toHaveLoggedStyled('...') would be available for matching styles also. Alternatively, .toHaveLogged('...') could try to match both unstyled and styled text under the hood and only fail if neither matches (bit less transparent, but perhaps more convenient?). 🤔