concurrently
Version:
Run commands concurrently
508 lines (507 loc) • 25.1 kB
JavaScript
import { subscribeSpyTo } from '@hirez_io/observer-spy';
import chalk from 'chalk';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { FakeCommand } from './__fixtures__/fake-command.js';
import { Logger } from './logger.js';
beforeEach(() => {
// Force Chalk to use colors, otherwise tests may pass when they were supposed to be failing.
chalk.level = 3;
});
const createLogger = (...options) => {
const logger = new Logger(...options);
vi.spyOn(logger, 'log');
const spy = subscribeSpyTo(logger.output);
return { logger, spy };
};
describe('#log()', () => {
it('emits prefix + text in the output stream', () => {
const { logger, spy } = createLogger({});
logger.log('foo', 'bar');
const values = spy.getValues();
expect(values).toHaveLength(2);
expect(values[0]).toEqual({ command: undefined, text: 'foo' });
expect(values[1]).toEqual({ command: undefined, text: 'bar' });
});
it('emits multiple lines of text with prefix on each', () => {
const { logger, spy } = createLogger({});
logger.log('foo', 'bar\nbaz\n');
const values = spy.getValues();
expect(values).toHaveLength(2);
expect(values[0]).toEqual({ command: undefined, text: 'foo' });
expect(values[1]).toEqual({ command: undefined, text: 'bar\nfoobaz\n' });
});
it('does not emit prefix if previous call from same command did not finish with a LF', () => {
const { logger, spy } = createLogger({});
const command = new FakeCommand();
logger.log('foo', 'bar', command);
logger.log('foo', 'baz', command);
expect(spy.getValuesLength()).toBe(3);
expect(spy.getLastValue()).toEqual({ command, text: 'baz' });
});
it('emits LF and prefix if previous call is from different command and did not finish with a LF', () => {
const { logger, spy } = createLogger({});
const command1 = new FakeCommand();
logger.log('foo', 'bar', command1);
const command2 = new FakeCommand();
logger.log('foo', 'baz', command2);
const values = spy.getValues();
expect(values).toHaveLength(5);
expect(values).toContainEqual({ command: command1, text: '\n' });
expect(values).toContainEqual({ command: command2, text: 'foo' });
expect(values).toContainEqual({ command: command2, text: 'baz' });
});
it('does not emit prefix nor handle text if logger is in raw mode', () => {
const { logger, spy } = createLogger({ raw: true });
logger.log('foo', 'bar\nbaz\n');
const values = spy.getValues();
expect(values).toHaveLength(1);
expect(values[0]).toEqual({ command: undefined, text: 'bar\nbaz\n' });
});
});
describe('#logGlobalEvent()', () => {
it('does nothing if in raw mode', () => {
const { logger } = createLogger({ raw: true });
logger.logGlobalEvent('foo');
expect(logger.log).not.toHaveBeenCalled();
});
it('logs in gray dim style with arrow prefix', () => {
const { logger } = createLogger({});
logger.logGlobalEvent('foo');
expect(logger.log).toHaveBeenCalledWith(`${chalk.reset('-->')} `, `${chalk.reset('foo')}\n`);
});
});
describe('#logCommandText()', () => {
it('logs with name if no prefixFormat is set', () => {
const { logger } = createLogger({});
const cmd = new FakeCommand('bla');
logger.logCommandText('foo', cmd);
expect(logger.log).toHaveBeenCalledWith(`${chalk.reset('[bla]')} `, 'foo', cmd);
});
it('logs with index if no prefixFormat is set, and command has no name', () => {
const { logger } = createLogger({});
const cmd = new FakeCommand('', undefined, 2);
logger.logCommandText('foo', cmd);
expect(logger.log).toHaveBeenCalledWith(`${chalk.reset('[2]')} `, 'foo', cmd);
});
it('logs with prefixFormat set to pid', () => {
const { logger } = createLogger({ prefixFormat: 'pid' });
const cmd = new FakeCommand();
cmd.pid = 123;
logger.logCommandText('foo', cmd);
expect(logger.log).toHaveBeenCalledWith(`${chalk.reset('[123]')} `, 'foo', cmd);
});
it('logs with prefixFormat set to name', () => {
const { logger } = createLogger({ prefixFormat: 'name' });
const cmd = new FakeCommand('bar');
logger.logCommandText('foo', cmd);
expect(logger.log).toHaveBeenCalledWith(`${chalk.reset('[bar]')} `, 'foo', cmd);
});
it('logs with prefixFormat set to index', () => {
const { logger } = createLogger({ prefixFormat: 'index' });
const cmd = new FakeCommand(undefined, undefined, 3);
logger.logCommandText('foo', cmd);
expect(logger.log).toHaveBeenCalledWith(`${chalk.reset('[3]')} `, 'foo', cmd);
});
it('logs with prefixFormat set to time (with timestampFormat)', () => {
const { logger } = createLogger({ prefixFormat: 'time', timestampFormat: 'yyyy' });
const cmd = new FakeCommand();
logger.logCommandText('foo', cmd);
const year = new Date().getFullYear();
expect(logger.log).toHaveBeenCalledWith(`${chalk.reset(`[${year}]`)} `, 'foo', cmd);
});
it('logs with templated prefixFormat', () => {
const { logger } = createLogger({ prefixFormat: '{index}-{name}' });
const cmd = new FakeCommand('bar');
logger.logCommandText('foo', cmd);
expect(logger.log).toHaveBeenCalledWith(`${chalk.reset('0-bar')} `, 'foo', cmd);
});
it('does not strip spaces from beginning or end of prefixFormat', () => {
const { logger } = createLogger({ prefixFormat: ' {index}-{name} ' });
const cmd = new FakeCommand('bar');
logger.logCommandText('foo', cmd);
expect(logger.log).toHaveBeenCalledWith(`${chalk.reset(' 0-bar ')} `, 'foo', cmd);
});
it('logs with no prefix', () => {
const { logger } = createLogger({ prefixFormat: 'none' });
const cmd = new FakeCommand();
logger.logCommandText('foo', cmd);
expect(logger.log).toHaveBeenCalledWith(chalk.reset(''), 'foo', cmd);
});
it('logs prefix using command line itself', () => {
const { logger } = createLogger({ prefixFormat: 'command' });
const cmd = new FakeCommand();
logger.logCommandText('foo', cmd);
expect(logger.log).toHaveBeenCalledWith(`${chalk.reset('[echo foo]')} `, 'foo', cmd);
});
it('logs prefix using command line itself, capped at commandLength bytes', () => {
const { logger } = createLogger({ prefixFormat: 'command', commandLength: 6 });
const cmd = new FakeCommand();
logger.logCommandText('foo', cmd);
expect(logger.log).toHaveBeenCalledWith(`${chalk.reset('[ec..oo]')} `, 'foo', cmd);
});
it('logs default prefixes with padding', () => {
const { logger } = createLogger({});
const cmd = new FakeCommand('foo');
logger.setPrefixLength(5);
logger.logCommandText('bar', cmd);
expect(logger.log).toHaveBeenCalledWith(`${chalk.reset('[foo ]')} `, 'bar', cmd);
});
it('logs templated prefixes with padding', () => {
const { logger } = createLogger({ prefixFormat: '{name}-{index}' });
const cmd = new FakeCommand('foo', undefined, 0);
logger.setPrefixLength(6);
logger.logCommandText('bar', cmd);
expect(logger.log).toHaveBeenCalledWith(`${chalk.reset('foo-0 ')} `, 'bar', cmd);
});
it('logs prefix using prefixColor from command', () => {
const { logger } = createLogger({});
const cmd = new FakeCommand('', undefined, 1, {
prefixColor: 'blue',
});
logger.logCommandText('foo', cmd);
expect(logger.log).toHaveBeenCalledWith(`${chalk.blue('[1]')} `, 'foo', cmd);
});
it('logs prefix using default color if prefixColor from command is not a valid color', () => {
const { logger } = createLogger({});
const cmd = new FakeCommand('', undefined, 1, {
prefixColor: 'fake.bold',
});
logger.logCommandText('foo', cmd);
expect(logger.log).toHaveBeenCalledWith(chalk.reset('[1]') + ' ', 'foo', cmd);
});
it('logs prefix in gray dim if prefixColor from command does not exist', () => {
const { logger } = createLogger({});
const cmd = new FakeCommand('', undefined, 1, {
prefixColor: 'blue.fake',
});
logger.logCommandText('foo', cmd);
expect(logger.log).toHaveBeenCalledWith(`${chalk.reset('[1]')} `, 'foo', cmd);
});
it('logs prefix using prefixColor from command if prefixColor is a hex value', () => {
const { logger } = createLogger({});
const prefixColor = '#32bd8a';
const cmd = new FakeCommand('', undefined, 1, {
prefixColor,
});
logger.logCommandText('foo', cmd);
expect(logger.log).toHaveBeenCalledWith(`${chalk.hex(prefixColor)('[1]')} `, 'foo', cmd);
});
it('logs prefix using prefixColor from command if prefixColor is a hex value with modifiers', () => {
const { logger } = createLogger({});
const prefixColor = '#32bd8a.inverse';
const cmd = new FakeCommand('', undefined, 1, {
prefixColor,
});
logger.logCommandText('foo', cmd);
expect(logger.log).toHaveBeenCalledWith(`${chalk.hex(prefixColor).inverse('[1]')} `, 'foo', cmd);
});
it('logs prefix using prefixColor from command if prefixColor is a bg hex value (short form)', () => {
const { logger } = createLogger({});
const cmd = new FakeCommand('', undefined, 1, {
prefixColor: 'bg#32bd8a',
});
logger.logCommandText('foo', cmd);
expect(logger.log).toHaveBeenCalledWith(`${chalk.bgHex('#32bd8a')('[1]')} `, 'foo', cmd);
});
it('logs prefix using prefixColor from command if prefixColor is a bg hex value with modifiers (short form)', () => {
const { logger } = createLogger({});
const cmd = new FakeCommand('', undefined, 1, {
prefixColor: 'bg#32bd8a.bold',
});
logger.logCommandText('foo', cmd);
expect(logger.log).toHaveBeenCalledWith(`${chalk.bgHex('#32bd8a').bold('[1]')} `, 'foo', cmd);
});
it('handles 3-digit hex codes for bg hex (short form)', () => {
const { logger } = createLogger({});
const cmd = new FakeCommand('', undefined, 1, {
prefixColor: 'bg#f00',
});
logger.logCommandText('foo', cmd);
expect(logger.log).toHaveBeenCalledWith(`${chalk.bgHex('#f00')('[1]')} `, 'foo', cmd);
});
it('logs prefix using prefixColor from command if prefixColor is a bgHex() value (explicit form)', () => {
const { logger } = createLogger({});
const cmd = new FakeCommand('', undefined, 1, {
prefixColor: 'bgHex(#ff5500)',
});
logger.logCommandText('foo', cmd);
expect(logger.log).toHaveBeenCalledWith(`${chalk.bgHex('#ff5500')('[1]')} `, 'foo', cmd);
});
it('logs prefix using prefixColor from command if prefixColor is a bgHex() value with modifiers (explicit form)', () => {
const { logger } = createLogger({});
const cmd = new FakeCommand('', undefined, 1, {
prefixColor: 'bgHex(#ff5500).dim',
});
logger.logCommandText('foo', cmd);
expect(logger.log).toHaveBeenCalledWith(`${chalk.bgHex('#ff5500').dim('[1]')} `, 'foo', cmd);
});
it('handles 3-digit hex codes for bgHex() (explicit form)', () => {
const { logger } = createLogger({});
const cmd = new FakeCommand('', undefined, 1, {
prefixColor: 'bgHex(#0f0)',
});
logger.logCommandText('foo', cmd);
expect(logger.log).toHaveBeenCalledWith(`${chalk.bgHex('#0f0')('[1]')} `, 'foo', cmd);
});
it('falls back to default color for malformed bgHex() syntax', () => {
const { logger } = createLogger({});
const cmd = new FakeCommand('', undefined, 1, {
prefixColor: 'bgHex(invalid)',
});
logger.logCommandText('foo', cmd);
expect(logger.log).toHaveBeenCalledWith(`${chalk.reset('[1]')} `, 'foo', cmd);
});
it('logs prefix with chained fgColor.bgHex().modifier pattern', () => {
const { logger } = createLogger({});
const cmd = new FakeCommand('', undefined, 1, {
prefixColor: 'black.bgHex(#533AFD).dim',
});
logger.logCommandText('foo', cmd);
expect(logger.log).toHaveBeenCalledWith(`${chalk.black.bgHex('#533AFD').dim('[1]')} `, 'foo', cmd);
});
it('logs prefix with chained fgColor.bg#HEXCODE.modifier pattern', () => {
const { logger } = createLogger({});
const cmd = new FakeCommand('', undefined, 1, {
prefixColor: 'black.bg#FF0000.bold',
});
logger.logCommandText('foo', cmd);
expect(logger.log).toHaveBeenCalledWith(`${chalk.black.bgHex('#FF0000').bold('[1]')} `, 'foo', cmd);
});
it('logs prefix with chained #HEXCODE.bgNamed.modifier pattern', () => {
const { logger } = createLogger({});
const cmd = new FakeCommand('', undefined, 1, {
prefixColor: '#FF0000.bgBlue.dim',
});
logger.logCommandText('foo', cmd);
expect(logger.log).toHaveBeenCalledWith(`${chalk.hex('#FF0000').bgBlue.dim('[1]')} `, 'foo', cmd);
});
it('logs prefix using rgb() color function', () => {
const { logger } = createLogger({});
const cmd = new FakeCommand('', undefined, 1, {
prefixColor: 'rgb(255,136,0).bold',
});
logger.logCommandText('foo', cmd);
expect(logger.log).toHaveBeenCalledWith(`${chalk.rgb(255, 136, 0).bold('[1]')} `, 'foo', cmd);
});
it('logs prefix using bgRgb() color function', () => {
const { logger } = createLogger({});
const cmd = new FakeCommand('', undefined, 1, {
prefixColor: 'black.bgRgb(100,100,255)',
});
logger.logCommandText('foo', cmd);
expect(logger.log).toHaveBeenCalledWith(`${chalk.black.bgRgb(100, 100, 255)('[1]')} `, 'foo', cmd);
});
it('logs prefix using ansi256() color function', () => {
const { logger } = createLogger({});
const cmd = new FakeCommand('', undefined, 1, {
prefixColor: 'ansi256(199)',
});
logger.logCommandText('foo', cmd);
expect(logger.log).toHaveBeenCalledWith(`${chalk.ansi256(199)('[1]')} `, 'foo', cmd);
});
it('logs prefix using bgAnsi256() color function', () => {
const { logger } = createLogger({});
const cmd = new FakeCommand('', undefined, 1, {
prefixColor: 'ansi256(199).bgAnsi256(50)',
});
logger.logCommandText('foo', cmd);
expect(logger.log).toHaveBeenCalledWith(`${chalk.ansi256(199).bgAnsi256(50)('[1]')} `, 'foo', cmd);
});
it('logs prefix using hex() explicit function', () => {
const { logger } = createLogger({});
const cmd = new FakeCommand('', undefined, 1, {
prefixColor: 'hex(#ff5500)',
});
logger.logCommandText('foo', cmd);
expect(logger.log).toHaveBeenCalledWith(`${chalk.hex('#ff5500')('[1]')} `, 'foo', cmd);
});
it('falls back to default color for malformed hex() syntax', () => {
const { logger } = createLogger({});
const cmd = new FakeCommand('', undefined, 1, {
prefixColor: 'hex(invalid)',
});
logger.logCommandText('foo', cmd);
expect(logger.log).toHaveBeenCalledWith(`${chalk.reset('[1]')} `, 'foo', cmd);
});
it('falls back to default color for unknown function name', () => {
const { logger } = createLogger({});
const cmd = new FakeCommand('', undefined, 1, {
prefixColor: 'unknownFunc(123)',
});
logger.logCommandText('foo', cmd);
expect(logger.log).toHaveBeenCalledWith(`${chalk.reset('[1]')} `, 'foo', cmd);
});
it('does nothing if command is hidden by name', () => {
const { logger } = createLogger({ hide: ['abc'] });
const cmd = new FakeCommand('abc');
logger.logCommandText('foo', cmd);
expect(logger.log).not.toHaveBeenCalled();
});
it('does nothing if command is hidden by index', () => {
const { logger } = createLogger({ hide: [3] });
const cmd = new FakeCommand('', undefined, 3);
logger.logCommandText('foo', cmd);
expect(logger.log).not.toHaveBeenCalled();
});
});
describe('#logCommandText() with color markers', () => {
it('colors only the text inside {color}...{/color} within a template prefix', () => {
const { logger } = createLogger({ prefixFormat: '[{color}{name}{/color}]' });
const cmd = new FakeCommand('bar', undefined, 1, { prefixColor: 'blue' });
logger.logCommandText('foo', cmd);
expect(logger.log).toHaveBeenCalledWith(`[${chalk.blue('bar')}] `, 'foo', cmd);
});
it('supports multiple {color}...{/color} pairs in one template', () => {
const { logger } = createLogger({
prefixFormat: '{color}[{/color}{name}{color}]{/color}',
});
const cmd = new FakeCommand('bar', undefined, 1, { prefixColor: 'blue' });
logger.logCommandText('foo', cmd);
expect(logger.log).toHaveBeenCalledWith(`${chalk.blue('[')}bar${chalk.blue(']')} `, 'foo', cmd);
});
it('auto-closes an unclosed {color} so the tail stays colored', () => {
const { logger } = createLogger({ prefixFormat: '[{color}{name}]' });
const cmd = new FakeCommand('bar', undefined, 1, { prefixColor: 'blue' });
logger.logCommandText('foo', cmd);
expect(logger.log).toHaveBeenCalledWith(`[${chalk.blue('bar]')} `, 'foo', cmd);
});
it('auto-opens a bare {/color} so the head stays colored', () => {
const { logger } = createLogger({ prefixFormat: '{name}{/color}]' });
const cmd = new FakeCommand('bar', undefined, 1, { prefixColor: 'blue' });
logger.logCommandText('foo', cmd);
expect(logger.log).toHaveBeenCalledWith(`${chalk.blue('bar')}] `, 'foo', cmd);
});
it('templates without markers stay fully colored (backward compat)', () => {
const { logger } = createLogger({ prefixFormat: '{name}-{index}' });
const cmd = new FakeCommand('bar', undefined, 1, { prefixColor: 'blue' });
logger.logCommandText('foo', cmd);
expect(logger.log).toHaveBeenCalledWith(`${chalk.blue('bar-1')} `, 'foo', cmd);
});
it('pads templated prefix based on visible length, ignoring marker tokens', () => {
const { logger } = createLogger({ prefixFormat: '{color}{name}{/color}' });
const cmd = new FakeCommand('foo', undefined, 0, { prefixColor: 'blue' });
logger.setPrefixLength(6);
logger.logCommandText('bar', cmd);
expect(logger.log).toHaveBeenCalledWith(`${chalk.blue('foo')} `, 'bar', cmd);
});
it('strips markers and emits no ANSI escapes when colors are globally off', () => {
const { logger } = createLogger({ prefixFormat: '[{color}{name}{/color}]' });
logger.toggleColors(false);
const cmd = new FakeCommand('bar', undefined, 1, { prefixColor: 'blue' });
logger.logCommandText('foo', cmd);
expect(logger.log).toHaveBeenCalledWith('[bar] ', 'foo', cmd);
});
});
describe('#logCommandEvent()', () => {
it('does nothing if in raw mode', () => {
const { logger } = createLogger({ raw: true });
logger.logCommandEvent('foo', new FakeCommand());
expect(logger.log).not.toHaveBeenCalled();
});
it('does nothing if command is hidden by name', () => {
const { logger } = createLogger({ hide: ['abc'] });
const cmd = new FakeCommand('abc');
logger.logCommandEvent('foo', cmd);
expect(logger.log).not.toHaveBeenCalled();
});
it('does nothing if command is hidden by index', () => {
const { logger } = createLogger({ hide: [3] });
const cmd = new FakeCommand('', undefined, 3);
logger.logCommandEvent('foo', cmd);
expect(logger.log).not.toHaveBeenCalled();
});
it('logs text in gray dim', () => {
const { logger } = createLogger({});
const cmd = new FakeCommand('', undefined, 1);
logger.logCommandEvent('foo', cmd);
expect(logger.log).toHaveBeenCalledWith(`${chalk.reset('[1]')} `, `${chalk.reset('foo')}\n`, cmd);
});
it('prepends a LF if previous command write did not end with a LF', () => {
const { logger } = createLogger({});
const cmd = new FakeCommand('', undefined, 1);
logger.logCommandText('text', cmd);
logger.logCommandEvent('event', cmd);
expect(logger.log).toHaveBeenCalledWith(`${chalk.reset('[1]')} `, `\n${chalk.reset('event')}\n`, cmd);
});
});
describe('#logTable()', () => {
it('does not log anything in raw mode', () => {
const { logger } = createLogger({ raw: true });
logger.logTable([{ foo: 1, bar: 2 }]);
expect(logger.log).not.toHaveBeenCalled();
});
it('does not log anything if value is not an array', () => {
const { logger } = createLogger({});
logger.logTable({});
logger.logTable(null);
logger.logTable(0);
logger.logTable('');
expect(logger.log).not.toHaveBeenCalled();
});
it('does not log anything if array is empty', () => {
const { logger } = createLogger({});
logger.logTable([]);
expect(logger.log).not.toHaveBeenCalled();
});
it('does not log anything if array items have no properties', () => {
const { logger } = createLogger({});
logger.logTable([{}]);
expect(logger.log).not.toHaveBeenCalled();
});
it("logs a header for each item's properties", () => {
const { logger } = createLogger({});
logger.logTable([{ foo: 1, bar: 2 }]);
expect(logger.log).toHaveBeenCalledWith(`${chalk.reset('-->')} `, `${chalk.reset('│ foo │ bar │')}\n`);
});
it("logs padded headers according to longest column's value", () => {
const { logger } = createLogger({});
logger.logTable([{ a: 'foo', b: 'barbaz' }]);
expect(logger.log).toHaveBeenCalledWith(`${chalk.reset('-->')} `, `${chalk.reset('│ a │ b │')}\n`);
});
it("logs each items's values", () => {
const { logger } = createLogger({});
logger.logTable([{ foo: 123 }, { foo: 456 }]);
expect(logger.log).toHaveBeenCalledWith(`${chalk.reset('-->')} `, `${chalk.reset('│ 123 │')}\n`);
expect(logger.log).toHaveBeenCalledWith(`${chalk.reset('-->')} `, `${chalk.reset('│ 456 │')}\n`);
});
it("logs each items's values with empty column", () => {
const { logger } = createLogger({});
logger.logTable([{ foo: 123 }, { foo: null }]);
expect(logger.log).toHaveBeenCalledWith(`${chalk.reset('-->')} `, `${chalk.reset('│ 123 │')}\n`);
expect(logger.log).toHaveBeenCalledWith(`${chalk.reset('-->')} `, `${chalk.reset('│ │')}\n`);
});
it("logs each items's values padded according to longest column's value", () => {
const { logger } = createLogger({});
logger.logTable([{ foo: 1 }, { foo: 123 }]);
expect(logger.log).toHaveBeenCalledWith(`${chalk.reset('-->')} `, `${chalk.reset('│ 1 │')}\n`);
});
it('logs items with different properties in each', () => {
const { logger } = createLogger({});
logger.logTable([{ foo: 1 }, { bar: 2 }]);
expect(logger.log).toHaveBeenCalledWith(`${chalk.reset('-->')} `, `${chalk.reset('│ foo │ bar │')}\n`);
expect(logger.log).toHaveBeenCalledWith(`${chalk.reset('-->')} `, `${chalk.reset('│ 1 │ │')}\n`);
expect(logger.log).toHaveBeenCalledWith(`${chalk.reset('-->')} `, `${chalk.reset('│ │ 2 │')}\n`);
});
});
describe('#toggleColors()', () => {
it('uses supported color level when on', () => {
const { logger, spy } = createLogger({});
logger.toggleColors(true);
const command1 = new FakeCommand('foo', 'command', 0, { prefixColor: 'red' });
logger.logCommandText('bar', command1);
logger.logGlobalEvent('baz');
const texts = spy.getValues().map((value) => value.text);
expect(texts).toContain(`${chalk.red('[foo]')} `);
expect(texts).toContain(`${chalk.reset('-->')} `);
});
it('uses no colors when off', () => {
const { logger, spy } = createLogger({});
logger.toggleColors(false);
const command1 = new FakeCommand('foo', 'command', 0, { prefixColor: 'red' });
logger.logCommandText('bar', command1);
logger.logGlobalEvent('baz');
const texts = spy.getValues().map((value) => value.text);
expect(texts).toContain('[foo] ');
expect(texts).toContain('--> ');
});
});