-
Notifications
You must be signed in to change notification settings - Fork 102
Description
Launching the tests within a VSCode Javascript Debug Terminal or probably any debug process fails all CLI tests. The node process uncontrollably writes the following lines for every attached VM, breaking the strategy of console snapshot assertions...
Debugger attached.␊
Waiting for the debugger to disconnect...␊
See upstream nodejs/node#34799
A strategy that allows narrower test assertions (rather than the whole snapshot of the whole CLI output) would enable people to interactively debug tests at the same time as observing actual test failures. However, I don't know if that is compatible with the testing framework used here.
A strategy that worked for us for CLI testing in jest/vitest was not to rely on a console testing framework, but explicitly capture stderr and stdout and use e.g. expect.stringMatching() or other more 'forgiving' tests of console output, that allows there to be variable output which doesn't break the test assertion. This also makes tests more maintainable generally since unrelated changes (e.g. changing some greeting text) won't break every test - only some actual test of the greeting text.
Interception example
// using test utility
test("Exits with error when invoking unknown command", async () => {
const { stdout, stderr } = process;
using callsByStream = interceptStreams({ stdout, stderr });
// mock the CLI command
using processMock = mockCommandLineProcess(
`npx @roku-web-starter/cli this-is-not-a-command`,
);
// run the CLI tool
await executeCommand();
expect(
[...callsByStream.stdout, ...callsByStream.stderr].some((message) =>
message.includes(`Unknown command`),
),
).toBe(true);
const [firstExitArgs] = processMock.exit.mock.calls;
expect(firstExitArgs).toMatchObject([1]);
});// test utility
type WriteMethod = NodeJS.WriteStream["write"];
export function interceptStreams<StreamName extends string>(
streams: Record<StreamName, NodeJS.WriteStream>,
passthru = false,
) {
const streamEntries = Object.entries(streams) as [
StreamName,
NodeJS.WriteStream,
][];
const writeByStream = {} as Record<StreamName, WriteMethod>;
const callsByStream = Object.fromEntries(
Object.keys(streams).map((name) => [name, []]),
) as unknown as Record<StreamName, string[]>;
// substitute stream write method
for (const [name, stream] of streamEntries) {
// prepare interception - deliberately stores unbound method
// eslint-disable-next-line @typescript-eslint/unbound-method
const { write } = stream;
const calls: string[] = [];
// keep per-stream records
writeByStream[name] = write;
callsByStream[name] = calls;
// intercept
const interceptor = ((...args: Parameters<typeof write>) => {
const [bufferOrString] = args;
calls.push(bufferOrString.toString());
if (passthru) {
return write.call(stream, ...args);
}
return true;
}) as WriteMethod;
stream.write = interceptor;
}
const stopInterception = () => {
// restore stream write methods
for (const [name, stream] of streamEntries) {
stream.write = writeByStream[name];
}
};
return {
...callsByStream,
[Symbol.dispose]: stopInterception,
};
}