Skip to content
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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.
Expand Down
40 changes: 38 additions & 2 deletions src/proxyConsole.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const matchPattern = (
export const createLogHelper = () => {
const logs: string[] = [];
const originalLogs: string[] = [];
let rawOutput = '';

const logPatterns = new Set<{
pattern: LogPattern;
Expand All @@ -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)) {
Expand Down Expand Up @@ -112,13 +115,46 @@ 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,
addLog,
clearLogs,
expectLog,
expectNoLog,
expectLogTimes,
};
};

Expand Down Expand Up @@ -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 });
};
}

Expand Down
31 changes: 30 additions & 1 deletion tests/proxyConsole.test.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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([]);
Expand Down Expand Up @@ -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);
});