415 lines
13 KiB
TypeScript
415 lines
13 KiB
TypeScript
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
|
|
|
import type { Command } from '../command/index.js';
|
|
import { CommandManager } from '../command/index.js';
|
|
import type { BlockStdScope } from '../scope/block-std-scope.js';
|
|
|
|
type Command1 = Command<
|
|
{
|
|
command1Option?: string;
|
|
},
|
|
{
|
|
commandData1: string;
|
|
}
|
|
>;
|
|
|
|
type Command2 = Command<{ commandData1: string }, { commandData2: string }>;
|
|
|
|
describe('CommandManager', () => {
|
|
let std: BlockStdScope;
|
|
let commandManager: CommandManager;
|
|
|
|
beforeEach(() => {
|
|
// @ts-expect-error FIXME: ts error
|
|
std = {};
|
|
commandManager = new CommandManager(std);
|
|
});
|
|
|
|
test('can add and execute a command', () => {
|
|
const command1: Command1 = vi.fn((_ctx, next) => next());
|
|
const command2: Command2 = vi.fn((_ctx, _next) => {});
|
|
|
|
const [success1] = commandManager.chain().pipe(command1, {}).run();
|
|
const [success2] = commandManager
|
|
.chain()
|
|
.pipe(command1, {})
|
|
.pipe(command2)
|
|
.run();
|
|
|
|
expect(command1).toHaveBeenCalled();
|
|
expect(command2).toHaveBeenCalled();
|
|
expect(success1).toBeTruthy();
|
|
expect(success2).toBeFalsy();
|
|
});
|
|
|
|
test('can chain multiple commands', () => {
|
|
const command1: Command = vi.fn((_ctx, next) => next());
|
|
const command2: Command = vi.fn((_ctx, next) => next());
|
|
const command3: Command = vi.fn((_ctx, next) => next());
|
|
|
|
const [success] = commandManager
|
|
.chain()
|
|
.pipe(command1)
|
|
.pipe(command2)
|
|
.pipe(command3)
|
|
.run();
|
|
|
|
expect(command1).toHaveBeenCalled();
|
|
expect(command2).toHaveBeenCalled();
|
|
expect(command3).toHaveBeenCalled();
|
|
expect(success).toBeTruthy();
|
|
});
|
|
|
|
test('skip commands if there is a command failed before them (`next` not executed)', () => {
|
|
const command1: Command = vi.fn((_ctx, next) => next());
|
|
const command2: Command = vi.fn((_ctx, _next) => {});
|
|
const command3: Command = vi.fn((_ctx, next) => next());
|
|
|
|
const [success] = commandManager
|
|
.chain()
|
|
.pipe(command1)
|
|
.pipe(command2)
|
|
.pipe(command3)
|
|
.run();
|
|
|
|
expect(command1).toHaveBeenCalled();
|
|
expect(command2).toHaveBeenCalled();
|
|
expect(command3).not.toHaveBeenCalled();
|
|
expect(success).toBeFalsy();
|
|
});
|
|
|
|
test('can handle command failure', () => {
|
|
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
|
|
const command1: Command = vi.fn((_ctx, next) => next());
|
|
const command2: Command = vi.fn((_ctx, _next) => {
|
|
throw new Error('command2 failed');
|
|
});
|
|
const command3: Command = vi.fn((_ctx, next) => next());
|
|
|
|
const [success] = commandManager
|
|
.chain()
|
|
.pipe(command1)
|
|
.pipe(command2)
|
|
.pipe(command3)
|
|
.run();
|
|
|
|
expect(command1).toHaveBeenCalled();
|
|
expect(command2).toHaveBeenCalled();
|
|
expect(command3).not.toHaveBeenCalled();
|
|
expect(success).toBeFalsy();
|
|
expect(errorSpy).toHaveBeenCalledWith(new Error('command2 failed'));
|
|
});
|
|
|
|
test('can pass data to command when calling a command', () => {
|
|
const command1: Command1 = vi.fn((_ctx, next) => next());
|
|
|
|
const [success] = commandManager
|
|
.chain()
|
|
.pipe(command1, { command1Option: 'test' })
|
|
.run();
|
|
|
|
expect(command1).toHaveBeenCalledWith(
|
|
expect.objectContaining({ command1Option: 'test' }),
|
|
expect.any(Function)
|
|
);
|
|
expect(success).toBeTruthy();
|
|
});
|
|
|
|
test('can add data to the command chain with `with` method', () => {
|
|
const command1: Command<{ commandData1: string }> = vi.fn((_ctx, next) =>
|
|
next()
|
|
);
|
|
|
|
const [success, ctx] = commandManager
|
|
.chain()
|
|
.with({ commandData1: 'test' })
|
|
.pipe(command1)
|
|
.run();
|
|
|
|
expect(command1).toHaveBeenCalledWith(
|
|
expect.objectContaining({ commandData1: 'test' }),
|
|
expect.any(Function)
|
|
);
|
|
expect(success).toBeTruthy();
|
|
expect(ctx.commandData1).toBe('test');
|
|
});
|
|
|
|
test('passes and updates context across commands', () => {
|
|
const command1: Command<{}, { commandData1: string }> = vi.fn(
|
|
(_ctx, next) => next({ commandData1: '123' })
|
|
);
|
|
const command2: Command<{ commandData1: string }> = vi.fn((ctx, next) => {
|
|
expect(ctx.commandData1).toBe('123');
|
|
next({ commandData1: '456' });
|
|
});
|
|
|
|
const [success, ctx] = commandManager
|
|
.chain()
|
|
.pipe(command1)
|
|
.pipe(command2)
|
|
.run();
|
|
|
|
expect(command1).toHaveBeenCalled();
|
|
expect(command2).toHaveBeenCalled();
|
|
expect(success).toBeTruthy();
|
|
expect(ctx.commandData1).toBe('456');
|
|
});
|
|
|
|
test('should not continue with the rest of the chain if all commands in `try` fail', () => {
|
|
const command1: Command = vi.fn((_ctx, _next) => {});
|
|
const command2: Command = vi.fn((_ctx, _next) => {});
|
|
const command3: Command = vi.fn((_ctx, next) => next());
|
|
|
|
const [success] = commandManager
|
|
.chain()
|
|
.try(chain => [chain.pipe(command1), chain.pipe(command2)])
|
|
.pipe(command3)
|
|
.run();
|
|
|
|
expect(command1).toHaveBeenCalled();
|
|
expect(command2).toHaveBeenCalled();
|
|
expect(command3).not.toHaveBeenCalled();
|
|
expect(success).toBeFalsy();
|
|
});
|
|
|
|
test('should not re-execute previous commands in the chain before `try`', () => {
|
|
const command1: Command<{}, { commandData1: string }> = vi.fn(
|
|
(_ctx, next) => next({ commandData1: '123' })
|
|
);
|
|
const command2: Command = vi.fn((_ctx, _next) => {});
|
|
const command3: Command = vi.fn((_ctx, next) => next());
|
|
|
|
const [success, ctx] = commandManager
|
|
.chain()
|
|
.pipe(command1)
|
|
.try(chain => [chain.pipe(command2), chain.pipe(command3)])
|
|
.run();
|
|
|
|
expect(command1).toHaveBeenCalledTimes(1);
|
|
expect(command2).toHaveBeenCalled();
|
|
expect(command3).toHaveBeenCalled();
|
|
expect(success).toBeTruthy();
|
|
expect(ctx.commandData1).toBe('123');
|
|
});
|
|
|
|
test('should continue with the rest of the chain if one command in `try` succeeds', () => {
|
|
const command1: Command<
|
|
{},
|
|
{ commandData1?: string; commandData2?: string }
|
|
> = vi.fn((_ctx, _next) => {});
|
|
const command2: Command<
|
|
{},
|
|
{ commandData1?: string; commandData2?: string }
|
|
> = vi.fn((_ctx, next) => next({ commandData2: '123' }));
|
|
const command3: Command<{}, { commandData3: string }> = vi.fn(
|
|
(_ctx, next) => next({ commandData3: '456' })
|
|
);
|
|
|
|
const [success, ctx] = commandManager
|
|
.chain()
|
|
.try(chain => [chain.pipe(command1), chain.pipe(command2)])
|
|
.pipe(command3)
|
|
.run();
|
|
|
|
expect(command1).toHaveBeenCalled();
|
|
expect(command2).toHaveBeenCalled();
|
|
expect(command3).toHaveBeenCalled();
|
|
expect(success).toBeTruthy();
|
|
expect(ctx.commandData1).toBeUndefined();
|
|
expect(ctx.commandData2).toBe('123');
|
|
expect(ctx.commandData3).toBe('456');
|
|
});
|
|
|
|
test('should not execute any further commands in `try` after one succeeds', () => {
|
|
const command1: Command<
|
|
{},
|
|
{ commandData1?: string; commandData2?: string }
|
|
> = vi.fn((_ctx, next) => next({ commandData1: '123' }));
|
|
const command2: Command<
|
|
{},
|
|
{ commandData1?: string; commandData2?: string }
|
|
> = vi.fn((_ctx, next) => next({ commandData2: '456' }));
|
|
|
|
const [success, ctx] = commandManager
|
|
.chain()
|
|
.try(chain => [chain.pipe(command1), chain.pipe(command2)])
|
|
.run();
|
|
|
|
expect(command1).toHaveBeenCalled();
|
|
expect(command2).not.toHaveBeenCalled();
|
|
expect(success).toBeTruthy();
|
|
expect(ctx.commandData1).toBe('123');
|
|
expect(ctx.commandData2).toBeUndefined();
|
|
});
|
|
|
|
test('should pass context correctly in `try` when a command succeeds', () => {
|
|
const command1: Command = vi.fn((_ctx, next) =>
|
|
next({ commandData1: 'fromCommand1', commandData2: 'fromCommand1' })
|
|
);
|
|
const command2: Command = vi.fn((ctx, next) => {
|
|
expect(ctx.commandData1).toBe('fromCommand1');
|
|
expect(ctx.commandData2).toBe('fromCommand1');
|
|
// override commandData2
|
|
next({ commandData2: 'fromCommand2' });
|
|
});
|
|
const command3: Command = vi.fn((ctx, next) => {
|
|
expect(ctx.commandData1).toBe('fromCommand1');
|
|
expect(ctx.commandData2).toBe('fromCommand2');
|
|
next();
|
|
});
|
|
|
|
const [success] = commandManager
|
|
.chain()
|
|
.pipe(command1)
|
|
.try(chain => [chain.pipe(command2)])
|
|
.pipe(command3)
|
|
.run();
|
|
|
|
expect(command1).toHaveBeenCalled();
|
|
expect(command2).toHaveBeenCalled();
|
|
expect(command3).toHaveBeenCalled();
|
|
expect(success).toBeTruthy();
|
|
});
|
|
|
|
test('should continue with the rest of the chain if at least one command in `tryAll` succeeds', () => {
|
|
const command1: Command = vi.fn((_ctx, _next) => {});
|
|
const command2: Command = vi.fn((_ctx, next) => next());
|
|
const command3: Command = vi.fn((_ctx, next) => next());
|
|
|
|
const [success] = commandManager
|
|
.chain()
|
|
.tryAll(chain => [chain.pipe(command1), chain.pipe(command2)])
|
|
.pipe(command3)
|
|
.run();
|
|
|
|
expect(command1).toHaveBeenCalled();
|
|
expect(command2).toHaveBeenCalled();
|
|
expect(command3).toHaveBeenCalled();
|
|
expect(success).toBeTruthy();
|
|
});
|
|
|
|
test('should execute all commands in `tryAll` even if one has already succeeded', () => {
|
|
const command1: Command<
|
|
{},
|
|
{ commandData1?: string; commandData2?: string; commandData3?: string }
|
|
> = vi.fn((_ctx, next) => next({ commandData1: '123' }));
|
|
const command2: Command<
|
|
{},
|
|
{ commandData1?: string; commandData2?: string; commandData3?: string }
|
|
> = vi.fn((_ctx, next) => next({ commandData2: '456' }));
|
|
const command3: Command<
|
|
{},
|
|
{ commandData1?: string; commandData2?: string; commandData3?: string }
|
|
> = vi.fn((_ctx, next) => next({ commandData3: '789' }));
|
|
|
|
const [success, ctx] = commandManager
|
|
.chain()
|
|
.tryAll(chain => [
|
|
chain.pipe(command1),
|
|
chain.pipe(command2),
|
|
chain.pipe(command3),
|
|
])
|
|
.run();
|
|
|
|
expect(command1).toHaveBeenCalled();
|
|
expect(command2).toHaveBeenCalled();
|
|
expect(command3).toHaveBeenCalled();
|
|
expect(ctx.commandData1).toBe('123');
|
|
expect(ctx.commandData2).toBe('456');
|
|
expect(ctx.commandData3).toBe('789');
|
|
expect(success).toBeTruthy();
|
|
});
|
|
|
|
test('should not continue with the rest of the chain if all commands in `tryAll` fail', () => {
|
|
const command1: Command = vi.fn((_ctx, _next) => {});
|
|
const command2: Command = vi.fn((_ctx, _next) => {});
|
|
const command3: Command<{}, { commandData3: string }> = vi.fn(
|
|
(_ctx, next) => next({ commandData3: '123' })
|
|
);
|
|
|
|
const [success, ctx] = commandManager
|
|
.chain()
|
|
.tryAll(chain => [chain.pipe(command1), chain.pipe(command2)])
|
|
.pipe(command3)
|
|
.run();
|
|
|
|
expect(command1).toHaveBeenCalled();
|
|
expect(command2).toHaveBeenCalled();
|
|
expect(command3).not.toHaveBeenCalled();
|
|
expect(ctx.commandData3).toBeUndefined();
|
|
expect(success).toBeFalsy();
|
|
});
|
|
|
|
test('should pass context correctly in `tryAll` when at least one command succeeds', () => {
|
|
const command1: Command<{}, { commandData1: string }> = vi.fn(
|
|
(_ctx, next) => next({ commandData1: 'fromCommand1' })
|
|
);
|
|
const command2: Command<
|
|
{ commandData1: string; commandData2?: string },
|
|
{ commandData2: string; commandData3: string }
|
|
> = vi.fn((ctx, next) => {
|
|
expect(ctx.commandData1).toBe('fromCommand1');
|
|
// override commandData1
|
|
next({ commandData1: 'fromCommand2', commandData2: 'fromCommand2' });
|
|
});
|
|
const command3: Command<
|
|
{ commandData1: string; commandData2?: string },
|
|
{ commandData2: string; commandData3: string }
|
|
> = vi.fn((ctx, next) => {
|
|
expect(ctx.commandData1).toBe('fromCommand2');
|
|
expect(ctx.commandData2).toBe('fromCommand2');
|
|
next({
|
|
// override commandData2
|
|
commandData2: 'fromCommand3',
|
|
commandData3: 'fromCommand3',
|
|
});
|
|
});
|
|
const command4: Command<
|
|
{ commandData1: string; commandData2: string; commandData3: string },
|
|
{}
|
|
> = vi.fn((ctx, next) => {
|
|
expect(ctx.commandData1).toBe('fromCommand2');
|
|
expect(ctx.commandData2).toBe('fromCommand3');
|
|
expect(ctx.commandData3).toBe('fromCommand3');
|
|
next();
|
|
});
|
|
|
|
const [success, ctx] = commandManager
|
|
.chain()
|
|
.pipe(command1)
|
|
.tryAll(chain => [chain.pipe(command2), chain.pipe(command3)])
|
|
.pipe(command4)
|
|
.run();
|
|
|
|
expect(command1).toHaveBeenCalled();
|
|
expect(command2).toHaveBeenCalled();
|
|
expect(command3).toHaveBeenCalled();
|
|
expect(command4).toHaveBeenCalled();
|
|
expect(success).toBeTruthy();
|
|
expect(ctx.commandData1).toBe('fromCommand2');
|
|
expect(ctx.commandData2).toBe('fromCommand3');
|
|
expect(ctx.commandData3).toBe('fromCommand3');
|
|
});
|
|
|
|
test('should not re-execute commands before `tryAll` after executing `tryAll`', () => {
|
|
const command1: Command = vi.fn((_ctx, next) => next());
|
|
const command2: Command = vi.fn((_ctx, next) => next());
|
|
const command3: Command = vi.fn((_ctx, _next) => {});
|
|
const command4: Command = vi.fn((_ctx, next) => next());
|
|
|
|
const [success] = commandManager
|
|
.chain()
|
|
.pipe(command1)
|
|
.tryAll(chain => [chain.pipe(command2), chain.pipe(command3)])
|
|
.pipe(command4)
|
|
.run();
|
|
|
|
expect(command1).toHaveBeenCalledTimes(1);
|
|
expect(command2).toHaveBeenCalled();
|
|
expect(command3).toHaveBeenCalled();
|
|
expect(command4).toHaveBeenCalled();
|
|
expect(success).toBeTruthy();
|
|
});
|
|
});
|