diff --git a/README.md b/README.md index 1370b73..8e361fa 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,7 @@ try { console.error('Something failed'); await logHelper.expectLog('Something happened'); + logHelper.expectLogTimes('Something happened', 1); logHelper.expectNoLog('Unexpected error'); } finally { logHelper.restore(); @@ -141,6 +142,8 @@ try { console.log(logHelper.logs); ``` +`expectLogTimes(pattern, times)` synchronously asserts how many times a string or regular expression matches the captured output. + ### normalizeEol Normalizes CRLF line endings to LF. diff --git a/src/proxyConsole.ts b/src/proxyConsole.ts index befe907..3a47396 100644 --- a/src/proxyConsole.ts +++ b/src/proxyConsole.ts @@ -38,6 +38,7 @@ const matchPattern = ( export const createLogHelper = () => { const logs: string[] = []; const originalLogs: string[] = []; + let rawOutput = ''; const logPatterns = new Set<{ pattern: LogPattern; @@ -47,12 +48,14 @@ export const createLogHelper = () => { const clearLogs = () => { logs.splice(0); + rawOutput = ''; }; - const addLog = (input: string) => { + const addLog = (input: string, options?: { newline?: boolean }) => { const log = stripAnsi(input); logs.push(log); originalLogs.push(input); + rawOutput += options?.newline ? `${input}\n` : input; for (const { pattern, resolve, options } of logPatterns) { if (matchPattern(log, pattern, options)) { @@ -112,6 +115,38 @@ export const createLogHelper = () => { } }; + /** Assert the number of non-overlapping matches in the captured output. */ + const expectLogTimes = (pattern: string | RegExp, times: number) => { + const output = stripAnsi(rawOutput); + let actualTimes = 0; + + if (typeof pattern === 'string') { + let position = 0; + while (position <= output.length) { + position = output.indexOf(pattern, position); + if (position === -1) { + break; + } + actualTimes++; + position += pattern.length || 1; + } + } else { + const regexp = new RegExp( + pattern.source, + pattern.flags.includes('g') ? pattern.flags : `${pattern.flags}g`, + ); + actualTimes = output.match(regexp)?.length ?? 0; + } + + if (actualTimes !== times) { + const title = styleText(['bold', 'red'], 'Unexpected log count.'); + const expected = styleText('yellow', pattern.toString()); + throw new Error( + `${title}\nPattern: ${expected}\nExpected: ${times}\nReceived: ${actualTimes}\nGet:\n${originalLogs.join('\n')}`, + ); + } + }; + return { logs, originalLogs, @@ -119,6 +154,7 @@ export const createLogHelper = () => { clearLogs, expectLog, expectNoLog, + expectLogTimes, }; }; @@ -156,7 +192,7 @@ export const proxyConsole = ({ return typeof arg === 'object' ? JSON.stringify(arg) : String(arg); }) .join(' '); - logHelper.addLog(logMessage); + logHelper.addLog(logMessage, { newline: true }); }; } diff --git a/tests/proxyConsole.test.ts b/tests/proxyConsole.test.ts index 44ad928..32e1045 100644 --- a/tests/proxyConsole.test.ts +++ b/tests/proxyConsole.test.ts @@ -1,5 +1,5 @@ import { expect, test } from 'rstack/test'; -import { proxyConsole } from '../src/index'; +import { createLogHelper, proxyConsole } from '../src/index'; test('should capture console output and expose the Rsbuild log helpers', async () => { const originalLog = console.log; @@ -18,6 +18,8 @@ test('should capture console output and expose the Rsbuild log helpers', async ( 'second log', ]); expect(() => logHelper.expectNoLog('missing log')).not.toThrow(); + logHelper.expectLogTimes('{"value":1}second log', 0); + logHelper.expectLogTimes(/^second log$/m, 1); logHelper.clearLogs(); expect(logHelper.logs).toEqual([]); @@ -45,3 +47,30 @@ test('should support strict and POSIX log matching', async () => { logHelper.restore(); } }); + +test('should count log occurrences across output chunks', () => { + const logHelper = createLogHelper(); + const message = 'watching for changes...'; + + logHelper.addLog(`${message}\n${message}\nwatching for `); + logHelper.addLog('changes...\n'); + logHelper.expectLogTimes(message, 3); + logHelper.expectLogTimes(/watching for changes\.\.\./, 3); + + expect(() => logHelper.expectLogTimes(message, 1)).toThrow( + 'Expected: 1\nReceived: 3', + ); + + logHelper.clearLogs(); + logHelper.expectLogTimes(message, 0); + logHelper.addLog(message); + logHelper.expectLogTimes(message, 1); +}); + +test('should strip ANSI sequences after joining output chunks', () => { + const logHelper = createLogHelper(); + + logHelper.addLog('watching for \u001B[3'); + logHelper.addLog('9mchanges...\n'); + logHelper.expectLogTimes('watching for changes...', 1); +});