From 1462821b9f9d6ab199518f22d09f99ab59f99cd2 Mon Sep 17 00:00:00 2001 From: hanna-skryl Date: Thu, 16 Jan 2025 19:56:20 -0500 Subject: [PATCH 01/10] test(test-setup): add custom matchers for ui logger --- .../src/lib/extend/ui-logger.matcher.ts | 180 ++++++++++++++++++ .../src/lib/extend/ui-logger.matcher.utils.ts | 59 ++++++ .../ui-logger.matcher.utils.unit.test.ts | 114 +++++++++++ testing/test-setup/src/vitest.d.ts | 4 +- 4 files changed, 355 insertions(+), 2 deletions(-) create mode 100644 testing/test-setup/src/lib/extend/ui-logger.matcher.ts create mode 100644 testing/test-setup/src/lib/extend/ui-logger.matcher.utils.ts create mode 100644 testing/test-setup/src/lib/extend/ui-logger.matcher.utils.unit.test.ts diff --git a/testing/test-setup/src/lib/extend/ui-logger.matcher.ts b/testing/test-setup/src/lib/extend/ui-logger.matcher.ts new file mode 100644 index 000000000..5e1c4770a --- /dev/null +++ b/testing/test-setup/src/lib/extend/ui-logger.matcher.ts @@ -0,0 +1,180 @@ +import { cliui } from '@poppinss/cliui'; +import type { SyncExpectationResult } from '@vitest/expect'; +import { expect } from 'vitest'; +import { + type LogLevel, + extractLevel, + extractMessage, + hasExpectedMessage, + messageContains, +} from './ui-logger.matcher.utils'; + +type CliUi = ReturnType; + +export type CustomUiLoggerMatchers = { + toHaveLoggedMessage: (expected: string) => void; + toHaveLoggedNthMessage: (nth: number, expected: string) => void; + toHaveLoggedLevel: (expected: LogLevel) => void; + toHaveLoggedNthLevel: (nth: number, expected: LogLevel) => void; + toHaveLoggedMessageContaining: (expected: string) => void; + toHaveLoggedNthMessageContaining: (nth: number, expected: string) => void; + toHaveLogged: () => void; + toHaveLoggedTimes: (times: number) => void; +}; + +expect.extend({ + toHaveLoggedMessage: assertMessageLogged, + toHaveLoggedNthMessage: assertNthMessageLogged, + toHaveLoggedLevel: assertLevelLogged, + toHaveLoggedNthLevel: assertNthLevelLogged, + toHaveLoggedMessageContaining: assertMessageContaining, + toHaveLoggedNthMessageContaining: assertNthMessageContaining, + toHaveLogged: assertLogs, + toHaveLoggedTimes: assertLogCount, +}); + +function assertMessageLogged( + actual: CliUi, + expected: string, +): SyncExpectationResult { + const messages = actual.logger + .getRenderer() + .getLogs() + .map(({ message }) => extractMessage(message)); + + const pass = messages.some(msg => hasExpectedMessage(expected, msg)); + return { + pass, + message: () => + pass + ? `Expected not to have logged: ${expected}` + : `Expected to have logged: ${expected}}`, + }; +} + +function assertNthMessageLogged( + actual: CliUi, + nth: number, + expected: string, +): SyncExpectationResult { + const messages = actual.logger + .getRenderer() + .getLogs() + .map(({ message }) => extractMessage(message)); + + const pass = hasExpectedMessage(expected, messages[nth - 1]); + return { + pass, + message: () => + pass + ? `Expected not to have logged at position ${nth}: ${expected}` + : `Expected to have logged at position ${nth}: ${expected}`, + }; +} + +function assertLevelLogged( + actual: CliUi, + expected: LogLevel, +): SyncExpectationResult { + const levels = actual.logger + .getRenderer() + .getLogs() + .map(({ message }) => extractLevel(message)); + + const pass = levels.includes(expected); + return { + pass, + message: () => + pass + ? `Expected not to have ${expected} log level` + : `Expected to have ${expected} log level`, + }; +} + +function assertNthLevelLogged( + actual: CliUi, + nth: number, + expected: LogLevel, +): SyncExpectationResult { + const levels = actual.logger + .getRenderer() + .getLogs() + .map(({ message }) => extractLevel(message)); + + const pass = levels[nth - 1] === expected; + return { + pass, + message: () => + pass + ? `Expected not to have log level at position ${nth}: ${expected}` + : `Expected to have log level at position ${nth}: ${expected}`, + }; +} + +function assertMessageContaining( + actual: CliUi, + expected: string, +): SyncExpectationResult { + const messages = actual.logger + .getRenderer() + .getLogs() + .map(({ message }) => extractMessage(message)); + + const pass = messages.some(msg => messageContains(expected, msg)); + return { + pass, + message: () => + pass + ? `Expected not to find a message containing: ${expected}` + : `Expected to find a message containing: ${expected}, but none matched.`, + }; +} + +function assertNthMessageContaining( + actual: CliUi, + nth: number, + expected: string, +): SyncExpectationResult { + const messages = actual.logger + .getRenderer() + .getLogs() + .map(({ message }) => extractMessage(message)); + + const pass = messageContains(expected, messages[nth - 1]); + return { + pass, + message: () => + pass + ? `Expected not to find the fragment "${expected}" in the message at position ${nth}` + : `Expected to find the fragment "${expected}" in the message at position ${nth}, but it was not found.`, + }; +} + +function assertLogs(actual: CliUi): SyncExpectationResult { + const logs = actual.logger.getRenderer().getLogs(); + + const pass = logs.length > 0; + return { + pass, + message: () => + pass + ? `Expected not to have any logs` + : `Expected to have some logs, but no logs were produced`, + }; +} + +function assertLogCount( + actual: CliUi, + expected: number, +): SyncExpectationResult { + const logs = actual.logger.getRenderer().getLogs(); + + const pass = logs.length === expected; + return { + pass, + message: () => + pass + ? `Expected not to have exactly ${expected} logs` + : `Expected to have ${expected} logs, but got ${logs.length}`, + }; +} diff --git a/testing/test-setup/src/lib/extend/ui-logger.matcher.utils.ts b/testing/test-setup/src/lib/extend/ui-logger.matcher.utils.ts new file mode 100644 index 000000000..460aff3d3 --- /dev/null +++ b/testing/test-setup/src/lib/extend/ui-logger.matcher.utils.ts @@ -0,0 +1,59 @@ +import type { LoggingTypes } from '@poppinss/cliui/build/src/types'; +import { removeColorCodes } from '@code-pushup/test-utils'; + +export type LogLevel = Exclude | 'warn'; + +const LOG_LEVELS = new Set([ + 'success', + 'error', + 'fatal', + 'info', + 'debug', + 'await', + 'warn', +]); + +type ExtractedMessage = { + styledMessage: string; + unstyledMessage: string; +}; + +export function extractLevel(log: string): LogLevel | null { + const match = removeColorCodes(log).match(/^\[\s*\w+\((?\w+)\)\s*]/); + const level = match?.groups?.['level'] as LogLevel | undefined; + return level && LOG_LEVELS.has(level) ? level : null; +} + +export function extractMessage(log: string): ExtractedMessage { + const match = log.match( + /^\[\s*\w+\((?\w+)\)\s*]\s*(?.+?(\.\s*)?)$/, + ); + const styledMessage = match?.groups?.['message'] ?? log; + const unstyledMessage = removeColorCodes(styledMessage); + return { styledMessage, unstyledMessage }; +} + +export function hasExpectedMessage( + expected: string, + message: ExtractedMessage | undefined, +): boolean { + if (!message) { + return false; + } + return ( + message.styledMessage === expected || message.unstyledMessage === expected + ); +} + +export function messageContains( + expected: string, + message: ExtractedMessage | undefined, +): boolean { + if (!message) { + return false; + } + return ( + message.styledMessage.includes(expected) || + message.unstyledMessage.includes(expected) + ); +} diff --git a/testing/test-setup/src/lib/extend/ui-logger.matcher.utils.unit.test.ts b/testing/test-setup/src/lib/extend/ui-logger.matcher.utils.unit.test.ts new file mode 100644 index 000000000..e09803cf7 --- /dev/null +++ b/testing/test-setup/src/lib/extend/ui-logger.matcher.utils.unit.test.ts @@ -0,0 +1,114 @@ +import { describe, expect, it } from 'vitest'; +import { + extractLevel, + extractMessage, + hasExpectedMessage, + messageContains, +} from './ui-logger.matcher.utils'; + +describe('extractLevel', () => { + it('should extract level from an info log', () => { + expect(extractLevel('[ blue(info) ] Info message')).toBe('info'); + }); + + it('should extract level from a warning log', () => { + expect(extractLevel('[ yellow(warn) ] Warning message')).toBe('warn'); + }); + + it('should return null for a log without a level', () => { + expect(extractLevel('Message without level')).toBeNull(); + }); + + it('should return null for an invalid log level', () => { + expect(extractLevel('[ unknown ] Message with invalid level')).toBeNull(); + }); +}); + +describe('extractMessage', () => { + it('should extract styled and unstyled messages from a log', () => { + const { styledMessage, unstyledMessage } = extractMessage( + '[ blue(info) ] \u001B[90mRun merge-diffs...\u001B[39m', + ); + expect(styledMessage).toBe('\u001B[90mRun merge-diffs...\u001B[39m'); + expect(unstyledMessage).toBe('Run merge-diffs...'); + }); + + it('should handle logs without styling', () => { + const { styledMessage, unstyledMessage } = extractMessage( + 'Warning message without styles.', + ); + expect(styledMessage).toBe('Warning message without styles.'); + expect(unstyledMessage).toBe('Warning message without styles.'); + }); + + it('should return raw log for unmatchable logs', () => { + const log = 'Unmatchable log format'; + const { styledMessage, unstyledMessage } = extractMessage(log); + expect(styledMessage).toBe(log); + expect(unstyledMessage).toBe(log); + }); +}); + +describe('hasExpectedMessage', () => { + it('should return true for a matching styled message', () => { + const result = hasExpectedMessage('Styled message', { + styledMessage: 'Styled message', + unstyledMessage: 'Plain message', + }); + expect(result).toBe(true); + }); + + it('should return true for a matching unstyled message', () => { + const result = hasExpectedMessage('Plain message', { + styledMessage: 'Styled message', + unstyledMessage: 'Plain message', + }); + expect(result).toBe(true); + }); + + it('should return false for a non-matching message', () => { + const result = hasExpectedMessage('Non-matching message', { + styledMessage: 'Styled message', + unstyledMessage: 'Plain message', + }); + expect(result).toBe(false); + }); + + it('should return false for undefined message', () => { + const result = hasExpectedMessage('Expected message', undefined); + expect(result).toBe(false); + }); +}); + +describe('messageContains', () => { + it('should return true when styled message contains the substring', () => { + expect( + messageContains('message', { + styledMessage: 'Styled message content', + unstyledMessage: 'Plain message content', + }), + ).toBe(true); + }); + + it('should return true when unstyled message contains the substring', () => { + expect( + messageContains('Plain', { + styledMessage: 'Styled message content', + unstyledMessage: 'Plain message content', + }), + ).toBe(true); + }); + + it('should return false when neither message contains the substring', () => { + expect( + messageContains('Non-existent', { + styledMessage: 'Styled message content', + unstyledMessage: 'Plain message content', + }), + ).toBe(false); + }); + + it('should return false for undefined message', () => { + expect(messageContains('Expected substring', undefined)).toBe(false); + }); +}); diff --git a/testing/test-setup/src/vitest.d.ts b/testing/test-setup/src/vitest.d.ts index 801a7667b..2ee2fb9f5 100644 --- a/testing/test-setup/src/vitest.d.ts +++ b/testing/test-setup/src/vitest.d.ts @@ -3,10 +3,10 @@ import type { CustomAsymmetricPathMatchers, CustomPathMatchers, } from './lib/extend/path.matcher.js'; +import type { CustomUiLoggerMatchers } from './lib/extend/ui-logger.matcher.js'; declare module 'vitest' { - // eslint-disable-next-line @typescript-eslint/no-empty-object-type - interface Assertion extends CustomPathMatchers {} + interface Assertion extends CustomPathMatchers, CustomUiLoggerMatchers {} // eslint-disable-next-line @typescript-eslint/no-empty-object-type interface AsymmetricMatchersContaining extends CustomAsymmetricPathMatchers {} } From fa8cad5b69ab3d9711df025ff076cbe2862316a9 Mon Sep 17 00:00:00 2001 From: hanna-skryl Date: Thu, 16 Jan 2025 20:00:50 -0500 Subject: [PATCH 02/10] test(cli): update unit tests to use custom matchers --- .../lib/compare/compare-command.unit.test.ts | 4 +- .../filter.middleware.unit.test.ts | 13 ++++-- .../implementation/global.utils.unit.test.ts | 8 +--- ...validate-filter-options.utils.unit.test.ts | 43 ++++++++++--------- .../merge-diffs-command.unit.test.ts | 3 +- .../print-config-command.unit.test.ts | 10 ++--- packages/cli/tsconfig.test.json | 3 +- packages/cli/vite.config.unit.ts | 1 + 8 files changed, 43 insertions(+), 42 deletions(-) diff --git a/packages/cli/src/lib/compare/compare-command.unit.test.ts b/packages/cli/src/lib/compare/compare-command.unit.test.ts index d38e69517..a6a754122 100644 --- a/packages/cli/src/lib/compare/compare-command.unit.test.ts +++ b/packages/cli/src/lib/compare/compare-command.unit.test.ts @@ -5,7 +5,6 @@ import { DEFAULT_PERSIST_FORMAT, DEFAULT_PERSIST_OUTPUT_DIR, } from '@code-pushup/models'; -import { getLogMessages } from '@code-pushup/test-utils'; import { ui } from '@code-pushup/utils'; import { DEFAULT_CLI_CONFIGURATION } from '../../../mocks/constants.js'; import { yargsCli } from '../yargs-cli.js'; @@ -75,7 +74,8 @@ describe('compare-command', () => { { ...DEFAULT_CLI_CONFIGURATION, commands: [yargsCompareCommandObject()] }, ).parseAsync(); - expect(getLogMessages(ui().logger).at(-1)).toContain( + expect(ui()).toHaveLoggedLevel('info'); + expect(ui()).toHaveLoggedMessage( `Reports diff written to ${bold( '.code-pushup/report-diff.json', )} and ${bold('.code-pushup/report-diff.md')}`, diff --git a/packages/cli/src/lib/implementation/filter.middleware.unit.test.ts b/packages/cli/src/lib/implementation/filter.middleware.unit.test.ts index ecc5de17f..f8729a4b1 100644 --- a/packages/cli/src/lib/implementation/filter.middleware.unit.test.ts +++ b/packages/cli/src/lib/implementation/filter.middleware.unit.test.ts @@ -291,8 +291,6 @@ describe('filterMiddleware', () => { ); it('should trigger verbose logging when skipPlugins or onlyPlugins removes categories', () => { - const loggerSpy = vi.spyOn(ui().logger, 'info'); - filterMiddleware({ onlyPlugins: ['p1'], skipPlugins: ['p2'], @@ -316,8 +314,15 @@ describe('filterMiddleware', () => { verbose: true, }); - expect(loggerSpy).toHaveBeenCalledWith( - expect.stringContaining('removed the following categories'), + expect(ui()).toHaveLoggedNthLevel(1, 'info'); + expect(ui()).toHaveLoggedNthLevel(2, 'info'); + expect(ui()).toHaveLoggedNthMessage( + 1, + 'The --skipPlugins argument removed the following categories: c1, c2.', + ); + expect(ui()).toHaveLoggedNthMessage( + 2, + 'The --onlyPlugins argument removed the following categories: c1, c2.', ); }); diff --git a/packages/cli/src/lib/implementation/global.utils.unit.test.ts b/packages/cli/src/lib/implementation/global.utils.unit.test.ts index 07627f060..ab2351e38 100644 --- a/packages/cli/src/lib/implementation/global.utils.unit.test.ts +++ b/packages/cli/src/lib/implementation/global.utils.unit.test.ts @@ -55,10 +55,6 @@ describe('logErrorBeforeThrow', () => { }); it('should log a custom error when OptionValidationError is thrown', async () => { - const loggerSpy = vi.spyOn(ui().logger, 'error').mockImplementation(() => { - /* empty */ - }); - const errorFn = vi .fn() .mockRejectedValue(new OptionValidationError('Option validation failed')); @@ -68,8 +64,8 @@ describe('logErrorBeforeThrow', () => { } catch { /* suppress */ } - - expect(loggerSpy).toHaveBeenCalledWith('Option validation failed'); + expect(ui()).toHaveLoggedLevel('error'); + expect(ui()).toHaveLoggedMessage('Option validation failed'); }); it('should rethrow errors other than OptionValidationError', async () => { diff --git a/packages/cli/src/lib/implementation/validate-filter-options.utils.unit.test.ts b/packages/cli/src/lib/implementation/validate-filter-options.utils.unit.test.ts index 4bee2b3c2..3ed2d03bb 100644 --- a/packages/cli/src/lib/implementation/validate-filter-options.utils.unit.test.ts +++ b/packages/cli/src/lib/implementation/validate-filter-options.utils.unit.test.ts @@ -1,6 +1,5 @@ import { describe, expect } from 'vitest'; import type { CategoryConfig, PluginConfig } from '@code-pushup/models'; -import { getLogMessages } from '@code-pushup/test-utils'; import { ui } from '@code-pushup/utils'; import type { FilterOptionType, Filterables } from './filter.model.js'; import { @@ -19,22 +18,22 @@ describe('validateFilterOption', () => { [ 'onlyPlugins', ['p1', 'p3', 'p4'], - 'The --onlyPlugins argument references plugins that do not exist: p3, p4.', + 'The --onlyPlugins argument references plugins that do not exist: p3, p4. The only valid plugin is p1.', ], [ 'onlyPlugins', ['p1', 'p3'], - 'The --onlyPlugins argument references a plugin that does not exist: p3.', + 'The --onlyPlugins argument references a plugin that does not exist: p3. The only valid plugin is p1.', ], [ 'onlyCategories', ['c1', 'c3', 'c4'], - 'The --onlyCategories argument references categories that do not exist: c3, c4.', + 'The --onlyCategories argument references categories that do not exist: c3, c4. The only valid category is c1.', ], [ 'onlyCategories', ['c1', 'c3'], - 'The --onlyCategories argument references a category that does not exist: c3.', + 'The --onlyCategories argument references a category that does not exist: c3. The only valid category is c1.', ], ])( 'should log a warning if the only argument %s references nonexistent slugs %o along with valid ones', @@ -51,8 +50,7 @@ describe('validateFilterOption', () => { }, { itemsToFilter, skippedItems: [], verbose: false }, ); - const logs = getLogMessages(ui().logger); - expect(logs[0]).toContain(expected); + expect(ui()).toHaveLoggedMessage(expected); }, ); @@ -60,22 +58,22 @@ describe('validateFilterOption', () => { [ 'skipPlugins', ['p3', 'p4'], - 'The --skipPlugins argument references plugins that do not exist: p3, p4.', + 'The --skipPlugins argument references plugins that do not exist: p3, p4. The only valid plugin is p1.', ], [ 'skipPlugins', ['p3'], - 'The --skipPlugins argument references a plugin that does not exist: p3.', + 'The --skipPlugins argument references a plugin that does not exist: p3. The only valid plugin is p1.', ], [ 'skipCategories', ['c3', 'c4'], - 'The --skipCategories argument references categories that do not exist: c3, c4.', + 'The --skipCategories argument references categories that do not exist: c3, c4. The only valid category is c1.', ], [ 'skipCategories', ['c3'], - 'The --skipCategories argument references a category that does not exist: c3.', + 'The --skipCategories argument references a category that does not exist: c3. The only valid category is c1.', ], ])( 'should log a warning if the skip argument %s references nonexistent slugs %o', @@ -95,8 +93,7 @@ describe('validateFilterOption', () => { }, { itemsToFilter, skippedItems: [], verbose: false }, ); - const logs = getLogMessages(ui().logger); - expect(logs[0]).toContain(expected); + expect(ui()).toHaveLoggedMessage(expected); }, ); @@ -111,7 +108,7 @@ describe('validateFilterOption', () => { }, { itemsToFilter: ['p1'], skippedItems: [], verbose: false }, ); - expect(getLogMessages(ui().logger)).toHaveLength(0); + expect(ui()).not.toHaveLogged(); }); it('should log a category ignored as a result of plugin filtering', () => { @@ -130,9 +127,9 @@ describe('validateFilterOption', () => { }, { itemsToFilter: ['p1'], skippedItems: [], verbose: true }, ); - expect(getLogMessages(ui().logger)).toHaveLength(1); - expect(getLogMessages(ui().logger)[0]).toContain( - 'The --onlyPlugins argument removed the following categories: c1, c3', + expect(ui()).toHaveLoggedTimes(1); + expect(ui()).toHaveLoggedMessage( + 'The --onlyPlugins argument removed the following categories: c1, c3.', ); }); @@ -221,10 +218,14 @@ describe('validateFilterOption', () => { { plugins, categories }, { itemsToFilter: ['p1'], skippedItems: ['p1'], verbose: true }, ); - const logs = getLogMessages(ui().logger); - expect(logs[0]).toContain( + expect(ui()).toHaveLoggedNthMessage( + 1, 'The --skipPlugins argument references a skipped plugin: p1.', ); + expect(ui()).toHaveLoggedNthMessage( + 2, + 'The --skipPlugins argument removed the following categories: c1.', + ); }); }); @@ -446,7 +447,6 @@ describe('validateSkippedCategories', () => { ] as NonNullable; it('should log info when categories are removed', () => { - const loggerSpy = vi.spyOn(ui().logger, 'info'); validateSkippedCategories( categories, [ @@ -457,7 +457,8 @@ describe('validateSkippedCategories', () => { ] as NonNullable, true, ); - expect(loggerSpy).toHaveBeenCalledWith( + expect(ui()).toHaveLoggedLevel('info'); + expect(ui()).toHaveLoggedMessage( 'Category c1 was removed because all its refs were skipped. Affected refs: g1 (group)', ); }); diff --git a/packages/cli/src/lib/merge-diffs/merge-diffs-command.unit.test.ts b/packages/cli/src/lib/merge-diffs/merge-diffs-command.unit.test.ts index 6e95f5a90..d3637006e 100644 --- a/packages/cli/src/lib/merge-diffs/merge-diffs-command.unit.test.ts +++ b/packages/cli/src/lib/merge-diffs/merge-diffs-command.unit.test.ts @@ -5,7 +5,6 @@ import { DEFAULT_PERSIST_FORMAT, DEFAULT_PERSIST_OUTPUT_DIR, } from '@code-pushup/models'; -import { getLogMessages } from '@code-pushup/test-utils'; import { ui } from '@code-pushup/utils'; import { DEFAULT_CLI_CONFIGURATION } from '../../../mocks/constants.js'; import { yargsCli } from '../yargs-cli.js'; @@ -65,7 +64,7 @@ describe('merge-diffs-command', () => { }, ).parseAsync(); - expect(getLogMessages(ui().logger).at(-1)).toContain( + expect(ui()).toHaveLoggedMessage( `Reports diff written to ${bold('.code-pushup/report-diff.md')}`, ); }); diff --git a/packages/cli/src/lib/print-config/print-config-command.unit.test.ts b/packages/cli/src/lib/print-config/print-config-command.unit.test.ts index 8e3c8cae4..3b998cf39 100644 --- a/packages/cli/src/lib/print-config/print-config-command.unit.test.ts +++ b/packages/cli/src/lib/print-config/print-config-command.unit.test.ts @@ -1,5 +1,4 @@ import { describe, expect, vi } from 'vitest'; -import { getLogMessages } from '@code-pushup/test-utils'; import { ui } from '@code-pushup/utils'; import { DEFAULT_CLI_CONFIGURATION } from '../../../mocks/constants.js'; import { yargsCli } from '../yargs-cli.js'; @@ -27,11 +26,10 @@ describe('print-config-command', () => { { ...DEFAULT_CLI_CONFIGURATION, commands: [yargsConfigCommandObject()] }, ).parseAsync(); - const log = getLogMessages(ui().logger)[0]; - expect(log).not.toContain('"$0":'); - expect(log).not.toContain('"_":'); + expect(ui()).not.toHaveLoggedMessageContaining('"$0":'); + expect(ui()).not.toHaveLoggedMessageContaining('"_":'); - expect(log).toContain('"outputDir": "destinationDir"'); - expect(log).not.toContain('"output-dir":'); + expect(ui()).toHaveLoggedMessageContaining('"outputDir": "destinationDir"'); + expect(ui()).not.toHaveLoggedMessageContaining('"output-dir":'); }); }); diff --git a/packages/cli/tsconfig.test.json b/packages/cli/tsconfig.test.json index bb1ab5e0c..4de6650fc 100644 --- a/packages/cli/tsconfig.test.json +++ b/packages/cli/tsconfig.test.json @@ -12,6 +12,7 @@ "src/**/*.test.tsx", "src/**/*.test.js", "src/**/*.test.jsx", - "src/**/*.d.ts" + "src/**/*.d.ts", + "../../testing/test-setup/src/vitest.d.ts" ] } diff --git a/packages/cli/vite.config.unit.ts b/packages/cli/vite.config.unit.ts index 8ce1638f4..cb4a01fb0 100644 --- a/packages/cli/vite.config.unit.ts +++ b/packages/cli/vite.config.unit.ts @@ -28,6 +28,7 @@ export default defineConfig({ '../../testing/test-setup/src/lib/console.mock.ts', '../../testing/test-setup/src/lib/portal-client.mock.ts', '../../testing/test-setup/src/lib/reset.mocks.ts', + '../../testing/test-setup/src/lib/extend/ui-logger.matcher.ts', ], }, }); From fc0415d86145fe802fc37974c6b8dca2d547d573 Mon Sep 17 00:00:00 2001 From: hanna-skryl Date: Thu, 16 Jan 2025 21:46:25 -0500 Subject: [PATCH 03/10] test(plugin-lighthouse): update unit tests to use custom matchers --- .../src/lib/normalize-flags.unit.test.ts | 17 ++++++++------- .../lib/runner/details/details.unit.test.ts | 15 ++++++------- .../runner/details/item-value.unit.test.ts | 21 +++++++++++-------- .../src/lib/runner/utils.unit.test.ts | 14 +++++-------- packages/plugin-lighthouse/tsconfig.test.json | 3 ++- .../plugin-lighthouse/vite.config.unit.ts | 1 + 6 files changed, 37 insertions(+), 34 deletions(-) diff --git a/packages/plugin-lighthouse/src/lib/normalize-flags.unit.test.ts b/packages/plugin-lighthouse/src/lib/normalize-flags.unit.test.ts index 31c817d02..d0076030e 100644 --- a/packages/plugin-lighthouse/src/lib/normalize-flags.unit.test.ts +++ b/packages/plugin-lighthouse/src/lib/normalize-flags.unit.test.ts @@ -1,7 +1,6 @@ import { bold, yellow } from 'ansis'; import path from 'node:path'; import { describe, expect, it } from 'vitest'; -import { getLogMessages } from '@code-pushup/test-utils'; import { ui } from '@code-pushup/utils'; import { DEFAULT_CHROME_FLAGS, LIGHTHOUSE_OUTPUT_PATH } from './constants.js'; import { logUnsupportedFlagsInUse, normalizeFlags } from './normalize-flags.js'; @@ -11,9 +10,10 @@ import type { LighthouseOptions } from './types.js'; describe('logUnsupportedFlagsInUse', () => { it('should log unsupported entries', () => { logUnsupportedFlagsInUse({ 'list-all-audits': true } as LighthouseOptions); - expect(getLogMessages(ui().logger)).toHaveLength(1); - expect(getLogMessages(ui().logger).at(0)).toBe( - `[ cyan(debug) ] ${yellow('⚠')} Plugin ${bold( + expect(ui()).toHaveLoggedTimes(1); + expect(ui()).toHaveLoggedLevel('debug'); + expect(ui()).toHaveLoggedMessage( + `${yellow('⚠')} Plugin ${bold( 'lighthouse', )} used unsupported flags: ${bold('list-all-audits')}`, ); @@ -32,9 +32,10 @@ describe('logUnsupportedFlagsInUse', () => { // unsupported ...unsupportedFlags, } as unknown as LighthouseOptions); - expect(getLogMessages(ui().logger)).toHaveLength(1); - expect(getLogMessages(ui().logger).at(0)).toBe( - `[ cyan(debug) ] ${yellow('⚠')} Plugin ${bold( + expect(ui()).toHaveLoggedTimes(1); + expect(ui()).toHaveLoggedLevel('debug'); + expect(ui()).toHaveLoggedMessage( + `${yellow('⚠')} Plugin ${bold( 'lighthouse', )} used unsupported flags: ${bold( 'list-all-audits, list-locales, list-trace-categories', @@ -118,7 +119,7 @@ describe('normalizeFlags', () => { ...supportedFlags, } as unknown as LighthouseOptions), ).toEqual(expect.not.objectContaining({ 'list-all-audits': true })); - expect(getLogMessages(ui().logger)).toHaveLength(1); + expect(ui()).toHaveLoggedTimes(1); }); it('should remove any flag with an empty array as a value', () => { diff --git a/packages/plugin-lighthouse/src/lib/runner/details/details.unit.test.ts b/packages/plugin-lighthouse/src/lib/runner/details/details.unit.test.ts index 276a839e1..b0c4101fd 100644 --- a/packages/plugin-lighthouse/src/lib/runner/details/details.unit.test.ts +++ b/packages/plugin-lighthouse/src/lib/runner/details/details.unit.test.ts @@ -3,7 +3,6 @@ import type { FormattedIcu } from 'lighthouse'; import type Details from 'lighthouse/types/lhr/audit-details'; import type { Result } from 'lighthouse/types/lhr/audit-result'; import { describe, expect, it } from 'vitest'; -import { getLogMessages } from '@code-pushup/test-utils'; import { ui } from '@code-pushup/utils'; import { logUnsupportedDetails, toAuditDetails } from './details.js'; @@ -12,9 +11,10 @@ describe('logUnsupportedDetails', () => { logUnsupportedDetails([ { details: { type: 'screenshot' } }, ] as unknown as Result[]); - expect(getLogMessages(ui().logger)).toHaveLength(1); - expect(getLogMessages(ui().logger).at(0)).toBe( - `[ cyan(debug) ] ${yellow('⚠')} Plugin ${bold( + expect(ui()).toHaveLoggedTimes(1); + expect(ui()).toHaveLoggedLevel('debug'); + expect(ui()).toHaveLoggedMessage( + `${yellow('⚠')} Plugin ${bold( 'lighthouse', )} skipped parsing of unsupported audit details: ${bold('screenshot')}`, ); @@ -30,9 +30,10 @@ describe('logUnsupportedDetails', () => { { details: { type: 'treemap-data' } }, { details: { type: 'criticalrequestchain' } }, ] as unknown as Result[]); - expect(getLogMessages(ui().logger)).toHaveLength(1); - expect(getLogMessages(ui().logger).at(0)).toBe( - `[ cyan(debug) ] ${yellow('⚠')} Plugin ${bold( + expect(ui()).toHaveLoggedTimes(1); + expect(ui()).toHaveLoggedLevel('debug'); + expect(ui()).toHaveLoggedMessage( + `${yellow('⚠')} Plugin ${bold( 'lighthouse', )} skipped parsing of unsupported audit details: ${bold( 'filmstrip, screenshot, debugdata', diff --git a/packages/plugin-lighthouse/src/lib/runner/details/item-value.unit.test.ts b/packages/plugin-lighthouse/src/lib/runner/details/item-value.unit.test.ts index 2e344b56f..17f5f2172 100644 --- a/packages/plugin-lighthouse/src/lib/runner/details/item-value.unit.test.ts +++ b/packages/plugin-lighthouse/src/lib/runner/details/item-value.unit.test.ts @@ -1,7 +1,6 @@ import { bold } from 'ansis'; import type Details from 'lighthouse/types/lhr/audit-details'; import { beforeAll, describe, expect, it } from 'vitest'; -import { getLogMessages } from '@code-pushup/test-utils'; import { ui } from '@code-pushup/utils'; import { type SimpleItemValue, @@ -147,15 +146,17 @@ describe('parseTableItemPropertyValue', () => { }), ).toBe(''); - expect(getLogMessages(ui().logger).at(0)).toBe( - `[ blue(info) ] Value type ${bold('subitems')} is not implemented`, + expect(ui()).toHaveLoggedLevel('info'); + expect(ui()).toHaveLoggedMessage( + `Value type ${bold('subitems')} is not implemented`, ); }); it('should parse value item debugdata to empty string and log implemented', () => { expect(parseTableItemPropertyValue({ type: 'debugdata' })).toBe(''); - expect(getLogMessages(ui().logger).at(0)).toBe( - `[ blue(info) ] Value type ${bold('debugdata')} is not implemented`, + expect(ui()).toHaveLoggedLevel('info'); + expect(ui()).toHaveLoggedMessage( + `Value type ${bold('debugdata')} is not implemented`, ); }); @@ -362,8 +363,9 @@ describe('formatTableItemPropertyValue', () => { ), ).toBe(''); - expect(getLogMessages(ui().logger).at(0)).toBe( - `[ blue(info) ] Format type ${bold('multi')} is not implemented`, + expect(ui()).toHaveLoggedLevel('info'); + expect(ui()).toHaveLoggedMessage( + `Format type ${bold('multi')} is not implemented`, ); }); @@ -374,8 +376,9 @@ describe('formatTableItemPropertyValue', () => { 'thumbnail', ), ).toBe(''); - expect(getLogMessages(ui().logger).at(0)).toBe( - `[ blue(info) ] Format type ${bold('thumbnail')} is not implemented`, + expect(ui()).toHaveLoggedLevel('info'); + expect(ui()).toHaveLoggedMessage( + `Format type ${bold('thumbnail')} is not implemented`, ); }); diff --git a/packages/plugin-lighthouse/src/lib/runner/utils.unit.test.ts b/packages/plugin-lighthouse/src/lib/runner/utils.unit.test.ts index 8f2aff376..d52fe735d 100644 --- a/packages/plugin-lighthouse/src/lib/runner/utils.unit.test.ts +++ b/packages/plugin-lighthouse/src/lib/runner/utils.unit.test.ts @@ -11,7 +11,7 @@ import { type CoreConfig, auditOutputsSchema, } from '@code-pushup/models'; -import { MEMFS_VOLUME, getLogMessages } from '@code-pushup/test-utils'; +import { MEMFS_VOLUME } from '@code-pushup/test-utils'; import { ui } from '@code-pushup/utils'; import { unsupportedDetailTypes } from './details/details.js'; import { @@ -242,7 +242,7 @@ describe('toAuditOutputs', () => { }) as Result, ), ); - expect(getLogMessages(ui().logger)).toHaveLength(0); + expect(ui()).not.toHaveLogged(); }); it('should inform that for all unsupported details if verbose IS given', () => { @@ -260,7 +260,7 @@ describe('toAuditOutputs', () => { ), { verbose: true }, ); - expect(getLogMessages(ui().logger)).toHaveLength(1); + expect(ui()).toHaveLoggedTimes(1); }); it('should not parse empty audit details', () => { @@ -343,9 +343,7 @@ describe('getConfig', () => { await expect( getConfig({ preset: 'wrong' as 'desktop' }), ).resolves.toBeUndefined(); - expect(getLogMessages(ui().logger).at(0)).toMatch( - 'Preset "wrong" is not supported', - ); + expect(ui()).toHaveLoggedMessage('Preset "wrong" is not supported'); }); it('should load config from json file if configPath is specified', async () => { @@ -378,9 +376,7 @@ describe('getConfig', () => { await expect( getConfig({ configPath: path.join('wrong.not') }), ).resolves.toBeUndefined(); - expect(getLogMessages(ui().logger).at(0)).toMatch( - 'Format of file wrong.not not supported', - ); + expect(ui()).toHaveLoggedMessage('Format of file wrong.not not supported'); }); }); diff --git a/packages/plugin-lighthouse/tsconfig.test.json b/packages/plugin-lighthouse/tsconfig.test.json index bb1ab5e0c..4de6650fc 100644 --- a/packages/plugin-lighthouse/tsconfig.test.json +++ b/packages/plugin-lighthouse/tsconfig.test.json @@ -12,6 +12,7 @@ "src/**/*.test.tsx", "src/**/*.test.js", "src/**/*.test.jsx", - "src/**/*.d.ts" + "src/**/*.d.ts", + "../../testing/test-setup/src/vitest.d.ts" ] } diff --git a/packages/plugin-lighthouse/vite.config.unit.ts b/packages/plugin-lighthouse/vite.config.unit.ts index 932dbde85..6b797a223 100644 --- a/packages/plugin-lighthouse/vite.config.unit.ts +++ b/packages/plugin-lighthouse/vite.config.unit.ts @@ -26,6 +26,7 @@ export default defineConfig({ '../../testing/test-setup/src/lib/fs.mock.ts', '../../testing/test-setup/src/lib/console.mock.ts', '../../testing/test-setup/src/lib/reset.mocks.ts', + '../../testing/test-setup/src/lib/extend/ui-logger.matcher.ts', ], }, }); From af0f0d90ab3c5ec18bdd9ea560a11d97ec071417 Mon Sep 17 00:00:00 2001 From: hanna-skryl Date: Thu, 16 Jan 2025 21:59:14 -0500 Subject: [PATCH 04/10] test(core): update unit tests to use custom matchers --- .../src/lib/collect-and-persist.unit.test.ts | 4 +-- .../lib/implementation/persist.unit.test.ts | 34 +++++++++++-------- .../core/src/lib/merge-diffs.unit.test.ts | 17 +++++----- packages/core/tsconfig.test.json | 3 +- packages/core/vite.config.unit.ts | 1 + 5 files changed, 31 insertions(+), 28 deletions(-) diff --git a/packages/core/src/lib/collect-and-persist.unit.test.ts b/packages/core/src/lib/collect-and-persist.unit.test.ts index f9b6f8d22..347556f1e 100644 --- a/packages/core/src/lib/collect-and-persist.unit.test.ts +++ b/packages/core/src/lib/collect-and-persist.unit.test.ts @@ -3,7 +3,6 @@ import { ISO_STRING_REGEXP, MINIMAL_CONFIG_MOCK, MINIMAL_REPORT_MOCK, - getLogMessages, } from '@code-pushup/test-utils'; import { type ScoredReport, @@ -115,7 +114,6 @@ describe('collectAndPersistReports', () => { await collectAndPersistReports( MINIMAL_CONFIG_MOCK as CollectAndPersistReportsOptions, ); - const logs = getLogMessages(ui().logger); - expect(logs.at(-2)).toContain('Made with ❤ by code-pushup.dev'); + expect(ui()).toHaveLoggedMessage('Made with ❤ by code-pushup.dev'); }); }); diff --git a/packages/core/src/lib/implementation/persist.unit.test.ts b/packages/core/src/lib/implementation/persist.unit.test.ts index d512a355a..548b34a3b 100644 --- a/packages/core/src/lib/implementation/persist.unit.test.ts +++ b/packages/core/src/lib/implementation/persist.unit.test.ts @@ -7,7 +7,6 @@ import { MEMFS_VOLUME, MINIMAL_REPORT_MOCK, REPORT_MOCK, - getLogMessages, } from '@code-pushup/test-utils'; import { scoreReport, sortReport, ui } from '@code-pushup/utils'; import { logPersistedResults, persistReport } from './persist.js'; @@ -91,17 +90,20 @@ describe('persistReport', () => { describe('logPersistedResults', () => { it('should log report sizes correctly`', () => { logPersistedResults([{ status: 'fulfilled', value: ['out.json', 10_000] }]); - const logs = getLogMessages(ui().logger); - expect(logs[0]).toBe('[ green(success) ] Generated reports successfully: '); - expect(logs[1]).toContain('9.77 kB'); - expect(logs[1]).toContain('out.json'); + expect(ui()).toHaveLoggedNthLevel(1, 'success'); + expect(ui()).toHaveLoggedNthMessageContaining( + 1, + 'Generated reports successfully: ', + ); + expect(ui()).toHaveLoggedNthMessageContaining(2, '9.77 kB'); + expect(ui()).toHaveLoggedNthMessageContaining(2, 'out.json'); }); it('should log fails correctly`', () => { logPersistedResults([{ status: 'rejected', reason: 'fail' }]); - const logs = getLogMessages(ui().logger); - expect(logs[0]).toBe('[ yellow(warn) ] Generated reports failed: '); - expect(logs[1]).toContain('fail'); + expect(ui()).toHaveLoggedNthLevel(1, 'warn'); + expect(ui()).toHaveLoggedNthMessage(1, 'Generated reports failed: '); + expect(ui()).toHaveLoggedNthMessageContaining(2, 'fail'); }); it('should log report sizes and fails correctly`', () => { @@ -109,12 +111,14 @@ describe('logPersistedResults', () => { { status: 'fulfilled', value: ['out.json', 10_000] }, { status: 'rejected', reason: 'fail' }, ]); - const logs = getLogMessages(ui().logger); - expect(logs[0]).toBe('[ green(success) ] Generated reports successfully: '); - expect(logs[1]).toContain('out.json'); - expect(logs[1]).toContain('9.77 kB'); - - expect(logs[2]).toContain('Generated reports failed: '); - expect(logs[2]).toContain('fail'); + expect(ui()).toHaveLoggedNthLevel(1, 'success'); + expect(ui()).toHaveLoggedNthMessage(1, 'Generated reports successfully: '); + expect(ui()).toHaveLoggedNthMessageContaining(2, 'out.json'); + expect(ui()).toHaveLoggedNthMessageContaining(2, '9.77 kB'); + expect(ui()).toHaveLoggedNthMessageContaining( + 3, + 'Generated reports failed: ', + ); + expect(ui()).toHaveLoggedNthMessageContaining(3, 'fail'); }); }); diff --git a/packages/core/src/lib/merge-diffs.unit.test.ts b/packages/core/src/lib/merge-diffs.unit.test.ts index 483f720c7..36b525d83 100644 --- a/packages/core/src/lib/merge-diffs.unit.test.ts +++ b/packages/core/src/lib/merge-diffs.unit.test.ts @@ -4,7 +4,6 @@ import path from 'node:path'; import type { PersistConfig } from '@code-pushup/models'; import { MEMFS_VOLUME, - getLogMessages, reportsDiffAddedPluginMock, reportsDiffAltMock, reportsDiffMock, @@ -64,13 +63,13 @@ describe('mergeDiffs', () => { ), ).resolves.toBe(path.join(MEMFS_VOLUME, 'report-diff.md')); - expect(getLogMessages(ui().logger)).toEqual([ - expect.stringContaining( - 'Skipped invalid report diff - Failed to read JSON file missing-report-diff.json', - ), - expect.stringContaining( - 'Skipped invalid report diff - Invalid reports diff in invalid-report-diff.json', - ), - ]); + expect(ui()).toHaveLoggedNthMessageContaining( + 1, + 'Skipped invalid report diff - Failed to read JSON file missing-report-diff.json', + ); + expect(ui()).toHaveLoggedNthMessageContaining( + 2, + 'Skipped invalid report diff - Invalid reports diff in invalid-report-diff.json', + ); }); }); diff --git a/packages/core/tsconfig.test.json b/packages/core/tsconfig.test.json index d8dfd40c8..6d65ad4d6 100644 --- a/packages/core/tsconfig.test.json +++ b/packages/core/tsconfig.test.json @@ -12,6 +12,7 @@ "src/**/*.test.js", "src/**/*.test.jsx", "src/**/*.d.ts", - "mocks/**/*.ts" + "mocks/**/*.ts", + "../../testing/test-setup/src/vitest.d.ts" ] } diff --git a/packages/core/vite.config.unit.ts b/packages/core/vite.config.unit.ts index ce56fb16c..729480d93 100644 --- a/packages/core/vite.config.unit.ts +++ b/packages/core/vite.config.unit.ts @@ -28,6 +28,7 @@ export default defineConfig({ '../../testing/test-setup/src/lib/console.mock.ts', '../../testing/test-setup/src/lib/reset.mocks.ts', '../../testing/test-setup/src/lib/portal-client.mock.ts', + '../../testing/test-setup/src/lib/extend/ui-logger.matcher.ts', ], }, }); From 43048a69b50d2860174e51d40f1d1ce4aa60a487 Mon Sep 17 00:00:00 2001 From: hanna-skryl Date: Thu, 16 Jan 2025 22:02:16 -0500 Subject: [PATCH 05/10] test(plugin-coverage): update unit tests to use custom matchers --- .../src/lib/runner/lcov/lcov-runner.unit.test.ts | 5 ++--- packages/plugin-coverage/tsconfig.test.json | 3 ++- packages/plugin-coverage/vite.config.unit.ts | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/plugin-coverage/src/lib/runner/lcov/lcov-runner.unit.test.ts b/packages/plugin-coverage/src/lib/runner/lcov/lcov-runner.unit.test.ts index 2ab6bcd2f..b07376496 100644 --- a/packages/plugin-coverage/src/lib/runner/lcov/lcov-runner.unit.test.ts +++ b/packages/plugin-coverage/src/lib/runner/lcov/lcov-runner.unit.test.ts @@ -1,7 +1,6 @@ import { vol } from 'memfs'; import path from 'node:path'; import { describe, expect, it } from 'vitest'; -import { getLogMessages } from '@code-pushup/test-utils'; import { ui } from '@code-pushup/utils'; import { parseLcovFiles } from './lcov-runner.js'; @@ -99,11 +98,11 @@ end_of_record path.join('coverage', 'lcov.info'), ]); - expect(getLogMessages(ui().logger)[0]).toContain( + expect(ui()).toHaveLoggedMessage( `Coverage plugin: Empty lcov report file detected at ${path.join( 'coverage', 'lcov.info', - )}`, + )}.`, ); }); }); diff --git a/packages/plugin-coverage/tsconfig.test.json b/packages/plugin-coverage/tsconfig.test.json index 9f29d6bb0..211f44ad2 100644 --- a/packages/plugin-coverage/tsconfig.test.json +++ b/packages/plugin-coverage/tsconfig.test.json @@ -8,6 +8,7 @@ "vite.config.unit.ts", "vite.config.integration.ts", "mocks/**/*.ts", - "src/**/*.test.ts" + "src/**/*.test.ts", + "../../testing/test-setup/src/vitest.d.ts" ] } diff --git a/packages/plugin-coverage/vite.config.unit.ts b/packages/plugin-coverage/vite.config.unit.ts index e359bb1bb..e86d3f2e5 100644 --- a/packages/plugin-coverage/vite.config.unit.ts +++ b/packages/plugin-coverage/vite.config.unit.ts @@ -26,6 +26,7 @@ export default defineConfig({ '../../testing/test-setup/src/lib/fs.mock.ts', '../../testing/test-setup/src/lib/console.mock.ts', '../../testing/test-setup/src/lib/reset.mocks.ts', + '../../testing/test-setup/src/lib/extend/ui-logger.matcher.ts', ], }, }); From eece8195023865fad00c992156bcf6bc71325869 Mon Sep 17 00:00:00 2001 From: hanna-skryl Date: Thu, 16 Jan 2025 22:07:16 -0500 Subject: [PATCH 06/10] test(utils): update unit tests to use custom matchers --- packages/utils/src/lib/log-results.unit.test.ts | 15 ++++++++------- packages/utils/src/lib/verbose-utils.unit.test.ts | 7 +++---- packages/utils/tsconfig.test.json | 3 ++- packages/utils/vite.config.unit.ts | 1 + 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/packages/utils/src/lib/log-results.unit.test.ts b/packages/utils/src/lib/log-results.unit.test.ts index b0549733b..fe535fa50 100644 --- a/packages/utils/src/lib/log-results.unit.test.ts +++ b/packages/utils/src/lib/log-results.unit.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it, vi } from 'vitest'; -import { getLogMessages } from '@code-pushup/test-utils'; import type { FileResult } from './file-system.js'; import { logMultipleResults, logPromiseResults } from './log-results.js'; import { ui } from './logging.js'; @@ -68,9 +67,10 @@ describe('logPromiseResults', () => { 'Uploaded reports successfully:', (result): string => result.value.toString(), ); - const logs = getLogMessages(ui().logger); - expect(logs[0]).toBe('[ green(success) ] Uploaded reports successfully:'); - expect(logs[1]).toBe('[ green(success) ] out.json'); + expect(ui()).toHaveLoggedNthLevel(1, 'success'); + expect(ui()).toHaveLoggedNthMessage(1, 'Uploaded reports successfully:'); + expect(ui()).toHaveLoggedNthLevel(2, 'success'); + expect(ui()).toHaveLoggedNthMessage(2, 'out.json'); }); it('should log on fail', () => { @@ -79,8 +79,9 @@ describe('logPromiseResults', () => { 'Generated reports failed:', (result: { reason: string }) => result.reason, ); - const logs = getLogMessages(ui().logger); - expect(logs[0]).toBe('[ yellow(warn) ] Generated reports failed:'); - expect(logs[1]).toBe('[ yellow(warn) ] fail'); + expect(ui()).toHaveLoggedNthLevel(1, 'warn'); + expect(ui()).toHaveLoggedNthMessage(1, 'Generated reports failed:'); + expect(ui()).toHaveLoggedNthLevel(2, 'warn'); + expect(ui()).toHaveLoggedNthMessage(2, 'fail'); }); }); diff --git a/packages/utils/src/lib/verbose-utils.unit.test.ts b/packages/utils/src/lib/verbose-utils.unit.test.ts index 339131cf7..4aebbee49 100644 --- a/packages/utils/src/lib/verbose-utils.unit.test.ts +++ b/packages/utils/src/lib/verbose-utils.unit.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it, vi } from 'vitest'; -import { getLogMessages } from '@code-pushup/test-utils'; import { ui } from './logging.js'; import { verboseUtils } from './verbose-utils.js'; @@ -24,16 +23,16 @@ describe('verbose-utils', () => { it('logs should be off by default', () => { verboseUtils(false).log('42'); - expect(getLogMessages(ui().logger)).toHaveLength(0); + expect(ui()).not.toHaveLogged(); }); it('should not print any logs when verbose is off', () => { verboseUtils(false).log('42'); - expect(getLogMessages(ui().logger)).toHaveLength(0); + expect(ui()).not.toHaveLogged(); }); it('should log when verbose is on', () => { verboseUtils(true).log('42'); - expect(getLogMessages(ui().logger)[0]).toContain('42'); + expect(ui()).toHaveLoggedMessageContaining('42'); }); }); diff --git a/packages/utils/tsconfig.test.json b/packages/utils/tsconfig.test.json index bb1ab5e0c..4de6650fc 100644 --- a/packages/utils/tsconfig.test.json +++ b/packages/utils/tsconfig.test.json @@ -12,6 +12,7 @@ "src/**/*.test.tsx", "src/**/*.test.js", "src/**/*.test.jsx", - "src/**/*.d.ts" + "src/**/*.d.ts", + "../../testing/test-setup/src/vitest.d.ts" ] } diff --git a/packages/utils/vite.config.unit.ts b/packages/utils/vite.config.unit.ts index 66d9e7717..79a1203e3 100644 --- a/packages/utils/vite.config.unit.ts +++ b/packages/utils/vite.config.unit.ts @@ -26,6 +26,7 @@ export default defineConfig({ '../../testing/test-setup/src/lib/fs.mock.ts', '../../testing/test-setup/src/lib/console.mock.ts', '../../testing/test-setup/src/lib/reset.mocks.ts', + '../../testing/test-setup/src/lib/extend/ui-logger.matcher.ts', ], }, }); From f84260d3c0894898e250fc20fa2fcb22bf5e7796 Mon Sep 17 00:00:00 2001 From: hanna-skryl Date: Thu, 16 Jan 2025 22:09:10 -0500 Subject: [PATCH 07/10] refactor: remove unused logging utility --- testing/test-utils/src/index.ts | 1 - testing/test-utils/src/lib/utils/logging.ts | 8 -------- 2 files changed, 9 deletions(-) delete mode 100644 testing/test-utils/src/lib/utils/logging.ts diff --git a/testing/test-utils/src/index.ts b/testing/test-utils/src/index.ts index 2adbfb7b9..25a36b96c 100644 --- a/testing/test-utils/src/index.ts +++ b/testing/test-utils/src/index.ts @@ -1,7 +1,6 @@ export * from './lib/constants.js'; export * from './lib/utils/execute-process-helper.mock.js'; export * from './lib/utils/os-agnostic-paths.js'; -export * from './lib/utils/logging.js'; export * from './lib/utils/env.js'; export * from './lib/utils/git.js'; export * from './lib/utils/string.js'; diff --git a/testing/test-utils/src/lib/utils/logging.ts b/testing/test-utils/src/lib/utils/logging.ts deleted file mode 100644 index 43ab1d6c3..000000000 --- a/testing/test-utils/src/lib/utils/logging.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { Logger } from '@poppinss/cliui'; - -export function getLogMessages(logger: Logger): string[] { - return logger - .getRenderer() - .getLogs() - .map(({ message }) => message); -} From 7a49e36166fb0fd22778353e9617f4caa9584f0a Mon Sep 17 00:00:00 2001 From: hanna-skryl Date: Fri, 17 Jan 2025 15:29:33 -0500 Subject: [PATCH 08/10] test(test-setup): simplify custom matchers --- .../src/lib/extend/ui-logger.matcher.ts | 149 ++++-------------- .../src/lib/extend/ui-logger.matcher.utils.ts | 58 +++++-- .../ui-logger.matcher.utils.unit.test.ts | 54 ++----- 3 files changed, 84 insertions(+), 177 deletions(-) diff --git a/testing/test-setup/src/lib/extend/ui-logger.matcher.ts b/testing/test-setup/src/lib/extend/ui-logger.matcher.ts index 5e1c4770a..0114f2340 100644 --- a/testing/test-setup/src/lib/extend/ui-logger.matcher.ts +++ b/testing/test-setup/src/lib/extend/ui-logger.matcher.ts @@ -2,151 +2,66 @@ import { cliui } from '@poppinss/cliui'; import type { SyncExpectationResult } from '@vitest/expect'; import { expect } from 'vitest'; import { + type ExpectedMessage, type LogLevel, - extractLevel, - extractMessage, + extractLogDetails, hasExpectedMessage, - messageContains, } from './ui-logger.matcher.utils'; type CliUi = ReturnType; export type CustomUiLoggerMatchers = { - toHaveLoggedMessage: (expected: string) => void; - toHaveLoggedNthMessage: (nth: number, expected: string) => void; - toHaveLoggedLevel: (expected: LogLevel) => void; - toHaveLoggedNthLevel: (nth: number, expected: LogLevel) => void; - toHaveLoggedMessageContaining: (expected: string) => void; - toHaveLoggedNthMessageContaining: (nth: number, expected: string) => void; - toHaveLogged: () => void; + toHaveLogged: (level: LogLevel, message: ExpectedMessage) => void; + toHaveNthLogged: ( + nth: number, + level: LogLevel, + message: ExpectedMessage, + ) => void; toHaveLoggedTimes: (times: number) => void; + toHaveLogs: () => void; }; expect.extend({ - toHaveLoggedMessage: assertMessageLogged, - toHaveLoggedNthMessage: assertNthMessageLogged, - toHaveLoggedLevel: assertLevelLogged, - toHaveLoggedNthLevel: assertNthLevelLogged, - toHaveLoggedMessageContaining: assertMessageContaining, - toHaveLoggedNthMessageContaining: assertNthMessageContaining, - toHaveLogged: assertLogs, + toHaveLogged: assertLogged, + toHaveNthLogged: assertNthLogged, toHaveLoggedTimes: assertLogCount, + toHaveLogs: assertLogs, }); -function assertMessageLogged( +function assertLogged( actual: CliUi, - expected: string, + level: LogLevel, + message: ExpectedMessage, ): SyncExpectationResult { - const messages = actual.logger - .getRenderer() - .getLogs() - .map(({ message }) => extractMessage(message)); + const logs = extractLogDetails(actual.logger); - const pass = messages.some(msg => hasExpectedMessage(expected, msg)); + const pass = logs.some( + log => log.level === level && hasExpectedMessage(message, log.message), + ); return { pass, message: () => pass - ? `Expected not to have logged: ${expected}` - : `Expected to have logged: ${expected}}`, + ? `Expected not to find a log with level "${level}" and message matching: ${message}` + : `Expected a log with level "${level}" and message matching: ${message}`, }; } -function assertNthMessageLogged( +function assertNthLogged( actual: CliUi, nth: number, - expected: string, + level: LogLevel, + message: ExpectedMessage, ): SyncExpectationResult { - const messages = actual.logger - .getRenderer() - .getLogs() - .map(({ message }) => extractMessage(message)); + const log = extractLogDetails(actual.logger)[nth - 1]; - const pass = hasExpectedMessage(expected, messages[nth - 1]); + const pass = log?.level === level && hasExpectedMessage(message, log.message); return { pass, message: () => pass - ? `Expected not to have logged at position ${nth}: ${expected}` - : `Expected to have logged at position ${nth}: ${expected}`, - }; -} - -function assertLevelLogged( - actual: CliUi, - expected: LogLevel, -): SyncExpectationResult { - const levels = actual.logger - .getRenderer() - .getLogs() - .map(({ message }) => extractLevel(message)); - - const pass = levels.includes(expected); - return { - pass, - message: () => - pass - ? `Expected not to have ${expected} log level` - : `Expected to have ${expected} log level`, - }; -} - -function assertNthLevelLogged( - actual: CliUi, - nth: number, - expected: LogLevel, -): SyncExpectationResult { - const levels = actual.logger - .getRenderer() - .getLogs() - .map(({ message }) => extractLevel(message)); - - const pass = levels[nth - 1] === expected; - return { - pass, - message: () => - pass - ? `Expected not to have log level at position ${nth}: ${expected}` - : `Expected to have log level at position ${nth}: ${expected}`, - }; -} - -function assertMessageContaining( - actual: CliUi, - expected: string, -): SyncExpectationResult { - const messages = actual.logger - .getRenderer() - .getLogs() - .map(({ message }) => extractMessage(message)); - - const pass = messages.some(msg => messageContains(expected, msg)); - return { - pass, - message: () => - pass - ? `Expected not to find a message containing: ${expected}` - : `Expected to find a message containing: ${expected}, but none matched.`, - }; -} - -function assertNthMessageContaining( - actual: CliUi, - nth: number, - expected: string, -): SyncExpectationResult { - const messages = actual.logger - .getRenderer() - .getLogs() - .map(({ message }) => extractMessage(message)); - - const pass = messageContains(expected, messages[nth - 1]); - return { - pass, - message: () => - pass - ? `Expected not to find the fragment "${expected}" in the message at position ${nth}` - : `Expected to find the fragment "${expected}" in the message at position ${nth}, but it was not found.`, + ? `Expected not to find a log at position ${nth} with level "${level}" and message matching: ${message}` + : `Expected a log at position ${nth} with level "${level}" and message matching: ${message}`, }; } @@ -158,8 +73,8 @@ function assertLogs(actual: CliUi): SyncExpectationResult { pass, message: () => pass - ? `Expected not to have any logs` - : `Expected to have some logs, but no logs were produced`, + ? `Expected no logs, but found ${logs.length}` + : `Expected some logs, but no logs were produced`, }; } @@ -174,7 +89,7 @@ function assertLogCount( pass, message: () => pass - ? `Expected not to have exactly ${expected} logs` - : `Expected to have ${expected} logs, but got ${logs.length}`, + ? `Expected not to find exactly ${expected} logs, but found ${logs.length}` + : `Expected exactly ${expected} logs, but found ${logs.length}`, }; } diff --git a/testing/test-setup/src/lib/extend/ui-logger.matcher.utils.ts b/testing/test-setup/src/lib/extend/ui-logger.matcher.utils.ts index 460aff3d3..4c7f8b789 100644 --- a/testing/test-setup/src/lib/extend/ui-logger.matcher.utils.ts +++ b/testing/test-setup/src/lib/extend/ui-logger.matcher.utils.ts @@ -1,7 +1,22 @@ +import type { Logger } from '@poppinss/cliui'; import type { LoggingTypes } from '@poppinss/cliui/build/src/types'; import { removeColorCodes } from '@code-pushup/test-utils'; -export type LogLevel = Exclude | 'warn'; +export type LogLevel = Exclude | 'warn' | 'log'; + +export type ExpectedMessage = + | string + | { asymmetricMatch: (value: string) => boolean }; + +type ExtractedMessage = { + styledMessage: string; + unstyledMessage: string; +}; + +type LogDetails = { + level: LogLevel; + message: ExtractedMessage; +}; const LOG_LEVELS = new Set([ 'success', @@ -11,17 +26,25 @@ const LOG_LEVELS = new Set([ 'debug', 'await', 'warn', + 'log', ]); -type ExtractedMessage = { - styledMessage: string; - unstyledMessage: string; -}; +export function extractLogDetails(logger: Logger): LogDetails[] { + return logger + .getRenderer() + .getLogs() + .map( + ({ message }): LogDetails => ({ + level: extractLevel(message), + message: extractMessage(message), + }), + ); +} -export function extractLevel(log: string): LogLevel | null { +export function extractLevel(log: string): LogLevel { const match = removeColorCodes(log).match(/^\[\s*\w+\((?\w+)\)\s*]/); const level = match?.groups?.['level'] as LogLevel | undefined; - return level && LOG_LEVELS.has(level) ? level : null; + return level && LOG_LEVELS.has(level) ? level : 'log'; } export function extractMessage(log: string): ExtractedMessage { @@ -34,26 +57,27 @@ export function extractMessage(log: string): ExtractedMessage { } export function hasExpectedMessage( - expected: string, + expected: ExpectedMessage, message: ExtractedMessage | undefined, ): boolean { if (!message) { return false; } + if (isAsymmetricMatcher(expected)) { + return ( + expected.asymmetricMatch(message.styledMessage) || + expected.asymmetricMatch(message.unstyledMessage) + ); + } return ( message.styledMessage === expected || message.unstyledMessage === expected ); } -export function messageContains( - expected: string, - message: ExtractedMessage | undefined, -): boolean { - if (!message) { - return false; - } +function isAsymmetricMatcher( + value: unknown, +): value is { asymmetricMatch: (input: string) => boolean } { return ( - message.styledMessage.includes(expected) || - message.unstyledMessage.includes(expected) + typeof value === 'object' && value != null && 'asymmetricMatch' in value ); } diff --git a/testing/test-setup/src/lib/extend/ui-logger.matcher.utils.unit.test.ts b/testing/test-setup/src/lib/extend/ui-logger.matcher.utils.unit.test.ts index e09803cf7..df91006c3 100644 --- a/testing/test-setup/src/lib/extend/ui-logger.matcher.utils.unit.test.ts +++ b/testing/test-setup/src/lib/extend/ui-logger.matcher.utils.unit.test.ts @@ -3,7 +3,6 @@ import { extractLevel, extractMessage, hasExpectedMessage, - messageContains, } from './ui-logger.matcher.utils'; describe('extractLevel', () => { @@ -15,12 +14,12 @@ describe('extractLevel', () => { expect(extractLevel('[ yellow(warn) ] Warning message')).toBe('warn'); }); - it('should return null for a log without a level', () => { - expect(extractLevel('Message without level')).toBeNull(); + it('should fall back to a default log level for a log without a level', () => { + expect(extractLevel('Message without level')).toBe('log'); }); - it('should return null for an invalid log level', () => { - expect(extractLevel('[ unknown ] Message with invalid level')).toBeNull(); + it('should fall back to a default log level for an invalid log level', () => { + expect(extractLevel('[ unknown ] Message with invalid level')).toBe('log'); }); }); @@ -40,13 +39,6 @@ describe('extractMessage', () => { expect(styledMessage).toBe('Warning message without styles.'); expect(unstyledMessage).toBe('Warning message without styles.'); }); - - it('should return raw log for unmatchable logs', () => { - const log = 'Unmatchable log format'; - const { styledMessage, unstyledMessage } = extractMessage(log); - expect(styledMessage).toBe(log); - expect(unstyledMessage).toBe(log); - }); }); describe('hasExpectedMessage', () => { @@ -78,37 +70,13 @@ describe('hasExpectedMessage', () => { const result = hasExpectedMessage('Expected message', undefined); expect(result).toBe(false); }); -}); - -describe('messageContains', () => { - it('should return true when styled message contains the substring', () => { - expect( - messageContains('message', { - styledMessage: 'Styled message content', - unstyledMessage: 'Plain message content', - }), - ).toBe(true); - }); - - it('should return true when unstyled message contains the substring', () => { - expect( - messageContains('Plain', { - styledMessage: 'Styled message content', - unstyledMessage: 'Plain message content', - }), - ).toBe(true); - }); - it('should return false when neither message contains the substring', () => { - expect( - messageContains('Non-existent', { - styledMessage: 'Styled message content', - unstyledMessage: 'Plain message content', - }), - ).toBe(false); - }); - - it('should return false for undefined message', () => { - expect(messageContains('Expected substring', undefined)).toBe(false); + it('should handle asymmetric matchers', () => { + const asymmetricMatcher = expect.stringContaining('Styled'); + const result = hasExpectedMessage(asymmetricMatcher, { + styledMessage: 'Styled message', + unstyledMessage: 'Plain message', + }); + expect(result).toBe(true); }); }); From 7363d501a8da002842a1a639b668dc72356a7da3 Mon Sep 17 00:00:00 2001 From: hanna-skryl Date: Fri, 17 Jan 2025 15:31:46 -0500 Subject: [PATCH 09/10] test: update unit tests to use simplified custom matchers --- .../lib/compare/compare-command.unit.test.ts | 4 +- .../filter.middleware.unit.test.ts | 8 +-- .../implementation/global.utils.unit.test.ts | 3 +- ...validate-filter-options.utils.unit.test.ts | 19 ++++--- .../merge-diffs-command.unit.test.ts | 3 +- .../print-config-command.unit.test.ts | 14 ++++-- .../src/lib/collect-and-persist.unit.test.ts | 2 +- .../lib/implementation/persist.unit.test.ts | 49 +++++++++++++------ .../core/src/lib/merge-diffs.unit.test.ts | 14 ++++-- .../lib/runner/lcov/lcov-runner.unit.test.ts | 3 +- .../src/lib/normalize-flags.unit.test.ts | 8 +-- .../lib/runner/details/details.unit.test.ts | 8 +-- .../runner/details/item-value.unit.test.ts | 16 +++--- .../src/lib/runner/utils.unit.test.ts | 6 +-- .../utils/src/lib/log-results.unit.test.ts | 16 +++--- .../utils/src/lib/verbose-utils.unit.test.ts | 6 +-- 16 files changed, 107 insertions(+), 72 deletions(-) diff --git a/packages/cli/src/lib/compare/compare-command.unit.test.ts b/packages/cli/src/lib/compare/compare-command.unit.test.ts index a6a754122..571d5b1e2 100644 --- a/packages/cli/src/lib/compare/compare-command.unit.test.ts +++ b/packages/cli/src/lib/compare/compare-command.unit.test.ts @@ -74,8 +74,8 @@ describe('compare-command', () => { { ...DEFAULT_CLI_CONFIGURATION, commands: [yargsCompareCommandObject()] }, ).parseAsync(); - expect(ui()).toHaveLoggedLevel('info'); - expect(ui()).toHaveLoggedMessage( + expect(ui()).toHaveLogged( + 'info', `Reports diff written to ${bold( '.code-pushup/report-diff.json', )} and ${bold('.code-pushup/report-diff.md')}`, diff --git a/packages/cli/src/lib/implementation/filter.middleware.unit.test.ts b/packages/cli/src/lib/implementation/filter.middleware.unit.test.ts index f8729a4b1..d3088f52d 100644 --- a/packages/cli/src/lib/implementation/filter.middleware.unit.test.ts +++ b/packages/cli/src/lib/implementation/filter.middleware.unit.test.ts @@ -314,14 +314,14 @@ describe('filterMiddleware', () => { verbose: true, }); - expect(ui()).toHaveLoggedNthLevel(1, 'info'); - expect(ui()).toHaveLoggedNthLevel(2, 'info'); - expect(ui()).toHaveLoggedNthMessage( + expect(ui()).toHaveNthLogged( 1, + 'info', 'The --skipPlugins argument removed the following categories: c1, c2.', ); - expect(ui()).toHaveLoggedNthMessage( + expect(ui()).toHaveNthLogged( 2, + 'info', 'The --onlyPlugins argument removed the following categories: c1, c2.', ); }); diff --git a/packages/cli/src/lib/implementation/global.utils.unit.test.ts b/packages/cli/src/lib/implementation/global.utils.unit.test.ts index ab2351e38..e24e97cf6 100644 --- a/packages/cli/src/lib/implementation/global.utils.unit.test.ts +++ b/packages/cli/src/lib/implementation/global.utils.unit.test.ts @@ -64,8 +64,7 @@ describe('logErrorBeforeThrow', () => { } catch { /* suppress */ } - expect(ui()).toHaveLoggedLevel('error'); - expect(ui()).toHaveLoggedMessage('Option validation failed'); + expect(ui()).toHaveLogged('error', 'Option validation failed'); }); it('should rethrow errors other than OptionValidationError', async () => { diff --git a/packages/cli/src/lib/implementation/validate-filter-options.utils.unit.test.ts b/packages/cli/src/lib/implementation/validate-filter-options.utils.unit.test.ts index 3ed2d03bb..58a72247e 100644 --- a/packages/cli/src/lib/implementation/validate-filter-options.utils.unit.test.ts +++ b/packages/cli/src/lib/implementation/validate-filter-options.utils.unit.test.ts @@ -50,7 +50,7 @@ describe('validateFilterOption', () => { }, { itemsToFilter, skippedItems: [], verbose: false }, ); - expect(ui()).toHaveLoggedMessage(expected); + expect(ui()).toHaveLogged('warn', expected); }, ); @@ -93,7 +93,7 @@ describe('validateFilterOption', () => { }, { itemsToFilter, skippedItems: [], verbose: false }, ); - expect(ui()).toHaveLoggedMessage(expected); + expect(ui()).toHaveLogged('warn', expected); }, ); @@ -108,7 +108,7 @@ describe('validateFilterOption', () => { }, { itemsToFilter: ['p1'], skippedItems: [], verbose: false }, ); - expect(ui()).not.toHaveLogged(); + expect(ui()).not.toHaveLogs(); }); it('should log a category ignored as a result of plugin filtering', () => { @@ -128,7 +128,8 @@ describe('validateFilterOption', () => { { itemsToFilter: ['p1'], skippedItems: [], verbose: true }, ); expect(ui()).toHaveLoggedTimes(1); - expect(ui()).toHaveLoggedMessage( + expect(ui()).toHaveLogged( + 'info', 'The --onlyPlugins argument removed the following categories: c1, c3.', ); }); @@ -218,12 +219,14 @@ describe('validateFilterOption', () => { { plugins, categories }, { itemsToFilter: ['p1'], skippedItems: ['p1'], verbose: true }, ); - expect(ui()).toHaveLoggedNthMessage( + expect(ui()).toHaveNthLogged( 1, + 'warn', 'The --skipPlugins argument references a skipped plugin: p1.', ); - expect(ui()).toHaveLoggedNthMessage( + expect(ui()).toHaveNthLogged( 2, + 'info', 'The --skipPlugins argument removed the following categories: c1.', ); }); @@ -457,8 +460,8 @@ describe('validateSkippedCategories', () => { ] as NonNullable, true, ); - expect(ui()).toHaveLoggedLevel('info'); - expect(ui()).toHaveLoggedMessage( + expect(ui()).toHaveLogged( + 'info', 'Category c1 was removed because all its refs were skipped. Affected refs: g1 (group)', ); }); diff --git a/packages/cli/src/lib/merge-diffs/merge-diffs-command.unit.test.ts b/packages/cli/src/lib/merge-diffs/merge-diffs-command.unit.test.ts index d3637006e..aca98fb91 100644 --- a/packages/cli/src/lib/merge-diffs/merge-diffs-command.unit.test.ts +++ b/packages/cli/src/lib/merge-diffs/merge-diffs-command.unit.test.ts @@ -64,7 +64,8 @@ describe('merge-diffs-command', () => { }, ).parseAsync(); - expect(ui()).toHaveLoggedMessage( + expect(ui()).toHaveLogged( + 'info', `Reports diff written to ${bold('.code-pushup/report-diff.md')}`, ); }); diff --git a/packages/cli/src/lib/print-config/print-config-command.unit.test.ts b/packages/cli/src/lib/print-config/print-config-command.unit.test.ts index 3b998cf39..23e09893b 100644 --- a/packages/cli/src/lib/print-config/print-config-command.unit.test.ts +++ b/packages/cli/src/lib/print-config/print-config-command.unit.test.ts @@ -26,10 +26,16 @@ describe('print-config-command', () => { { ...DEFAULT_CLI_CONFIGURATION, commands: [yargsConfigCommandObject()] }, ).parseAsync(); - expect(ui()).not.toHaveLoggedMessageContaining('"$0":'); - expect(ui()).not.toHaveLoggedMessageContaining('"_":'); + expect(ui()).not.toHaveLogged('log', expect.stringContaining('"$0":')); + expect(ui()).not.toHaveLogged('log', expect.stringContaining('"_":')); - expect(ui()).toHaveLoggedMessageContaining('"outputDir": "destinationDir"'); - expect(ui()).not.toHaveLoggedMessageContaining('"output-dir":'); + expect(ui()).toHaveLogged( + 'log', + expect.stringContaining('"outputDir": "destinationDir"'), + ); + expect(ui()).not.toHaveLogged( + 'log', + expect.stringContaining('"output-dir":'), + ); }); }); diff --git a/packages/core/src/lib/collect-and-persist.unit.test.ts b/packages/core/src/lib/collect-and-persist.unit.test.ts index 347556f1e..1256b9e41 100644 --- a/packages/core/src/lib/collect-and-persist.unit.test.ts +++ b/packages/core/src/lib/collect-and-persist.unit.test.ts @@ -114,6 +114,6 @@ describe('collectAndPersistReports', () => { await collectAndPersistReports( MINIMAL_CONFIG_MOCK as CollectAndPersistReportsOptions, ); - expect(ui()).toHaveLoggedMessage('Made with ❤ by code-pushup.dev'); + expect(ui()).toHaveLogged('log', 'Made with ❤ by code-pushup.dev'); }); }); diff --git a/packages/core/src/lib/implementation/persist.unit.test.ts b/packages/core/src/lib/implementation/persist.unit.test.ts index 548b34a3b..e62ae19ae 100644 --- a/packages/core/src/lib/implementation/persist.unit.test.ts +++ b/packages/core/src/lib/implementation/persist.unit.test.ts @@ -90,20 +90,27 @@ describe('persistReport', () => { describe('logPersistedResults', () => { it('should log report sizes correctly`', () => { logPersistedResults([{ status: 'fulfilled', value: ['out.json', 10_000] }]); - expect(ui()).toHaveLoggedNthLevel(1, 'success'); - expect(ui()).toHaveLoggedNthMessageContaining( + expect(ui()).toHaveNthLogged( 1, - 'Generated reports successfully: ', + 'success', + expect.stringContaining('Generated reports successfully: '), + ); + expect(ui()).toHaveNthLogged( + 2, + 'success', + expect.stringContaining('9.77 kB'), + ); + expect(ui()).toHaveNthLogged( + 2, + 'success', + expect.stringContaining('out.json'), ); - expect(ui()).toHaveLoggedNthMessageContaining(2, '9.77 kB'); - expect(ui()).toHaveLoggedNthMessageContaining(2, 'out.json'); }); it('should log fails correctly`', () => { logPersistedResults([{ status: 'rejected', reason: 'fail' }]); - expect(ui()).toHaveLoggedNthLevel(1, 'warn'); - expect(ui()).toHaveLoggedNthMessage(1, 'Generated reports failed: '); - expect(ui()).toHaveLoggedNthMessageContaining(2, 'fail'); + expect(ui()).toHaveNthLogged(1, 'warn', 'Generated reports failed: '); + expect(ui()).toHaveNthLogged(2, 'warn', expect.stringContaining('fail')); }); it('should log report sizes and fails correctly`', () => { @@ -111,14 +118,26 @@ describe('logPersistedResults', () => { { status: 'fulfilled', value: ['out.json', 10_000] }, { status: 'rejected', reason: 'fail' }, ]); - expect(ui()).toHaveLoggedNthLevel(1, 'success'); - expect(ui()).toHaveLoggedNthMessage(1, 'Generated reports successfully: '); - expect(ui()).toHaveLoggedNthMessageContaining(2, 'out.json'); - expect(ui()).toHaveLoggedNthMessageContaining(2, '9.77 kB'); - expect(ui()).toHaveLoggedNthMessageContaining( + expect(ui()).toHaveNthLogged( + 1, + 'success', + 'Generated reports successfully: ', + ); + expect(ui()).toHaveNthLogged( + 2, + 'success', + expect.stringContaining('out.json'), + ); + expect(ui()).toHaveNthLogged( + 2, + 'success', + expect.stringContaining('9.77 kB'), + ); + expect(ui()).toHaveNthLogged( 3, - 'Generated reports failed: ', + 'warn', + expect.stringContaining('Generated reports failed: '), ); - expect(ui()).toHaveLoggedNthMessageContaining(3, 'fail'); + expect(ui()).toHaveNthLogged(3, 'warn', expect.stringContaining('fail')); }); }); diff --git a/packages/core/src/lib/merge-diffs.unit.test.ts b/packages/core/src/lib/merge-diffs.unit.test.ts index 36b525d83..97b8cb163 100644 --- a/packages/core/src/lib/merge-diffs.unit.test.ts +++ b/packages/core/src/lib/merge-diffs.unit.test.ts @@ -63,13 +63,19 @@ describe('mergeDiffs', () => { ), ).resolves.toBe(path.join(MEMFS_VOLUME, 'report-diff.md')); - expect(ui()).toHaveLoggedNthMessageContaining( + expect(ui()).toHaveNthLogged( 1, - 'Skipped invalid report diff - Failed to read JSON file missing-report-diff.json', + 'warn', + expect.stringContaining( + 'Skipped invalid report diff - Failed to read JSON file missing-report-diff.json', + ), ); - expect(ui()).toHaveLoggedNthMessageContaining( + expect(ui()).toHaveNthLogged( 2, - 'Skipped invalid report diff - Invalid reports diff in invalid-report-diff.json', + 'warn', + expect.stringContaining( + 'Skipped invalid report diff - Invalid reports diff in invalid-report-diff.json', + ), ); }); }); diff --git a/packages/plugin-coverage/src/lib/runner/lcov/lcov-runner.unit.test.ts b/packages/plugin-coverage/src/lib/runner/lcov/lcov-runner.unit.test.ts index b07376496..63be5307d 100644 --- a/packages/plugin-coverage/src/lib/runner/lcov/lcov-runner.unit.test.ts +++ b/packages/plugin-coverage/src/lib/runner/lcov/lcov-runner.unit.test.ts @@ -98,7 +98,8 @@ end_of_record path.join('coverage', 'lcov.info'), ]); - expect(ui()).toHaveLoggedMessage( + expect(ui()).toHaveLogged( + 'warn', `Coverage plugin: Empty lcov report file detected at ${path.join( 'coverage', 'lcov.info', diff --git a/packages/plugin-lighthouse/src/lib/normalize-flags.unit.test.ts b/packages/plugin-lighthouse/src/lib/normalize-flags.unit.test.ts index d0076030e..39e711976 100644 --- a/packages/plugin-lighthouse/src/lib/normalize-flags.unit.test.ts +++ b/packages/plugin-lighthouse/src/lib/normalize-flags.unit.test.ts @@ -11,8 +11,8 @@ describe('logUnsupportedFlagsInUse', () => { it('should log unsupported entries', () => { logUnsupportedFlagsInUse({ 'list-all-audits': true } as LighthouseOptions); expect(ui()).toHaveLoggedTimes(1); - expect(ui()).toHaveLoggedLevel('debug'); - expect(ui()).toHaveLoggedMessage( + expect(ui()).toHaveLogged( + 'debug', `${yellow('⚠')} Plugin ${bold( 'lighthouse', )} used unsupported flags: ${bold('list-all-audits')}`, @@ -33,8 +33,8 @@ describe('logUnsupportedFlagsInUse', () => { ...unsupportedFlags, } as unknown as LighthouseOptions); expect(ui()).toHaveLoggedTimes(1); - expect(ui()).toHaveLoggedLevel('debug'); - expect(ui()).toHaveLoggedMessage( + expect(ui()).toHaveLogged( + 'debug', `${yellow('⚠')} Plugin ${bold( 'lighthouse', )} used unsupported flags: ${bold( diff --git a/packages/plugin-lighthouse/src/lib/runner/details/details.unit.test.ts b/packages/plugin-lighthouse/src/lib/runner/details/details.unit.test.ts index b0c4101fd..79be8d2db 100644 --- a/packages/plugin-lighthouse/src/lib/runner/details/details.unit.test.ts +++ b/packages/plugin-lighthouse/src/lib/runner/details/details.unit.test.ts @@ -12,8 +12,8 @@ describe('logUnsupportedDetails', () => { { details: { type: 'screenshot' } }, ] as unknown as Result[]); expect(ui()).toHaveLoggedTimes(1); - expect(ui()).toHaveLoggedLevel('debug'); - expect(ui()).toHaveLoggedMessage( + expect(ui()).toHaveLogged( + 'debug', `${yellow('⚠')} Plugin ${bold( 'lighthouse', )} skipped parsing of unsupported audit details: ${bold('screenshot')}`, @@ -31,8 +31,8 @@ describe('logUnsupportedDetails', () => { { details: { type: 'criticalrequestchain' } }, ] as unknown as Result[]); expect(ui()).toHaveLoggedTimes(1); - expect(ui()).toHaveLoggedLevel('debug'); - expect(ui()).toHaveLoggedMessage( + expect(ui()).toHaveLogged( + 'debug', `${yellow('⚠')} Plugin ${bold( 'lighthouse', )} skipped parsing of unsupported audit details: ${bold( diff --git a/packages/plugin-lighthouse/src/lib/runner/details/item-value.unit.test.ts b/packages/plugin-lighthouse/src/lib/runner/details/item-value.unit.test.ts index 17f5f2172..5c6a0617f 100644 --- a/packages/plugin-lighthouse/src/lib/runner/details/item-value.unit.test.ts +++ b/packages/plugin-lighthouse/src/lib/runner/details/item-value.unit.test.ts @@ -146,16 +146,16 @@ describe('parseTableItemPropertyValue', () => { }), ).toBe(''); - expect(ui()).toHaveLoggedLevel('info'); - expect(ui()).toHaveLoggedMessage( + expect(ui()).toHaveLogged( + 'info', `Value type ${bold('subitems')} is not implemented`, ); }); it('should parse value item debugdata to empty string and log implemented', () => { expect(parseTableItemPropertyValue({ type: 'debugdata' })).toBe(''); - expect(ui()).toHaveLoggedLevel('info'); - expect(ui()).toHaveLoggedMessage( + expect(ui()).toHaveLogged( + 'info', `Value type ${bold('debugdata')} is not implemented`, ); }); @@ -363,8 +363,8 @@ describe('formatTableItemPropertyValue', () => { ), ).toBe(''); - expect(ui()).toHaveLoggedLevel('info'); - expect(ui()).toHaveLoggedMessage( + expect(ui()).toHaveLogged( + 'info', `Format type ${bold('multi')} is not implemented`, ); }); @@ -376,8 +376,8 @@ describe('formatTableItemPropertyValue', () => { 'thumbnail', ), ).toBe(''); - expect(ui()).toHaveLoggedLevel('info'); - expect(ui()).toHaveLoggedMessage( + expect(ui()).toHaveLogged( + 'info', `Format type ${bold('thumbnail')} is not implemented`, ); }); diff --git a/packages/plugin-lighthouse/src/lib/runner/utils.unit.test.ts b/packages/plugin-lighthouse/src/lib/runner/utils.unit.test.ts index d52fe735d..2743e665e 100644 --- a/packages/plugin-lighthouse/src/lib/runner/utils.unit.test.ts +++ b/packages/plugin-lighthouse/src/lib/runner/utils.unit.test.ts @@ -242,7 +242,7 @@ describe('toAuditOutputs', () => { }) as Result, ), ); - expect(ui()).not.toHaveLogged(); + expect(ui()).not.toHaveLogs(); }); it('should inform that for all unsupported details if verbose IS given', () => { @@ -343,7 +343,7 @@ describe('getConfig', () => { await expect( getConfig({ preset: 'wrong' as 'desktop' }), ).resolves.toBeUndefined(); - expect(ui()).toHaveLoggedMessage('Preset "wrong" is not supported'); + expect(ui()).toHaveLogged('info', 'Preset "wrong" is not supported'); }); it('should load config from json file if configPath is specified', async () => { @@ -376,7 +376,7 @@ describe('getConfig', () => { await expect( getConfig({ configPath: path.join('wrong.not') }), ).resolves.toBeUndefined(); - expect(ui()).toHaveLoggedMessage('Format of file wrong.not not supported'); + expect(ui()).toHaveLogged('info', 'Format of file wrong.not not supported'); }); }); diff --git a/packages/utils/src/lib/log-results.unit.test.ts b/packages/utils/src/lib/log-results.unit.test.ts index fe535fa50..e9203fbae 100644 --- a/packages/utils/src/lib/log-results.unit.test.ts +++ b/packages/utils/src/lib/log-results.unit.test.ts @@ -67,10 +67,12 @@ describe('logPromiseResults', () => { 'Uploaded reports successfully:', (result): string => result.value.toString(), ); - expect(ui()).toHaveLoggedNthLevel(1, 'success'); - expect(ui()).toHaveLoggedNthMessage(1, 'Uploaded reports successfully:'); - expect(ui()).toHaveLoggedNthLevel(2, 'success'); - expect(ui()).toHaveLoggedNthMessage(2, 'out.json'); + expect(ui()).toHaveNthLogged( + 1, + 'success', + 'Uploaded reports successfully:', + ); + expect(ui()).toHaveNthLogged(2, 'success', 'out.json'); }); it('should log on fail', () => { @@ -79,9 +81,7 @@ describe('logPromiseResults', () => { 'Generated reports failed:', (result: { reason: string }) => result.reason, ); - expect(ui()).toHaveLoggedNthLevel(1, 'warn'); - expect(ui()).toHaveLoggedNthMessage(1, 'Generated reports failed:'); - expect(ui()).toHaveLoggedNthLevel(2, 'warn'); - expect(ui()).toHaveLoggedNthMessage(2, 'fail'); + expect(ui()).toHaveNthLogged(1, 'warn', 'Generated reports failed:'); + expect(ui()).toHaveNthLogged(2, 'warn', 'fail'); }); }); diff --git a/packages/utils/src/lib/verbose-utils.unit.test.ts b/packages/utils/src/lib/verbose-utils.unit.test.ts index 4aebbee49..1403d44f7 100644 --- a/packages/utils/src/lib/verbose-utils.unit.test.ts +++ b/packages/utils/src/lib/verbose-utils.unit.test.ts @@ -23,16 +23,16 @@ describe('verbose-utils', () => { it('logs should be off by default', () => { verboseUtils(false).log('42'); - expect(ui()).not.toHaveLogged(); + expect(ui()).not.toHaveLogs(); }); it('should not print any logs when verbose is off', () => { verboseUtils(false).log('42'); - expect(ui()).not.toHaveLogged(); + expect(ui()).not.toHaveLogs(); }); it('should log when verbose is on', () => { verboseUtils(true).log('42'); - expect(ui()).toHaveLoggedMessageContaining('42'); + expect(ui()).toHaveLogged('info', '42'); }); }); From 7fcaa8bafa393231ec6f854e78ba678ed5a3accd Mon Sep 17 00:00:00 2001 From: hanna-skryl Date: Fri, 17 Jan 2025 16:02:32 -0500 Subject: [PATCH 10/10] test(test-setup): suppress TypeScript error --- testing/test-setup/src/lib/extend/ui-logger.matcher.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/testing/test-setup/src/lib/extend/ui-logger.matcher.ts b/testing/test-setup/src/lib/extend/ui-logger.matcher.ts index 0114f2340..e8b6e4a25 100644 --- a/testing/test-setup/src/lib/extend/ui-logger.matcher.ts +++ b/testing/test-setup/src/lib/extend/ui-logger.matcher.ts @@ -23,6 +23,7 @@ export type CustomUiLoggerMatchers = { expect.extend({ toHaveLogged: assertLogged, + // @ts-expect-error Custom matcher works despite TypeScript signature mismatch toHaveNthLogged: assertNthLogged, toHaveLoggedTimes: assertLogCount, toHaveLogs: assertLogs,