@eclipse-emfcloud/model-manager
Version:
Command-based model editing with undo/redo.
777 lines • 34.3 kB
JavaScript
"use strict";
// *****************************************************************************
// Copyright (C) 2023-2024 STMicroelectronics.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: MIT License which is
// available at https://opensource.org/licenses/MIT.
//
// SPDX-License-Identifier: EPL-2.0 OR MIT
// *****************************************************************************
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const chai_1 = __importStar(require("chai"));
const chai_as_promised_1 = __importDefault(require("chai-as-promised"));
const chai_like_1 = __importDefault(require("chai-like"));
const sinon_1 = __importDefault(require("sinon"));
const sinon_chai_1 = __importDefault(require("sinon-chai"));
const command_1 = require("../command");
chai_1.default.use(chai_like_1.default);
chai_1.default.use(chai_as_promised_1.default);
chai_1.default.use(sinon_chai_1.default);
describe('Command-related functions', () => {
describe('isCompoundCommand', () => {
it('detects a CompoundCommand', async () => {
const compound = new command_1.CompoundCommandImpl('test');
const isCompound = (0, command_1.isCompoundCommand)(compound);
(0, chai_1.expect)(isCompound, 'compound command not detected').to.be.true;
});
it('detects a simple (non-compound) Command', async () => {
const leaf = new TestCommand('test');
const isCompound = (0, command_1.isCompoundCommand)(leaf);
(0, chai_1.expect)(isCompound, 'simple command not detected').to.be.false;
});
});
describe('append', () => {
it('base is already a CompoundCommand', async () => {
const compound = new command_1.CompoundCommandImpl('test');
const appended = new TestCommand('appended');
const result = (0, command_1.append)(compound, appended);
(0, chai_1.expect)(result, 'compound command not appended').to.be.equal(compound);
(0, chai_1.expect)(result).to.be.like({ _commands: [appended] });
});
it('base is not a CompoundCommand', async () => {
const base = new TestCommand('test');
const appended = new TestCommand('appended');
const result = (0, command_1.append)(base, appended);
(0, chai_1.expect)(result, 'commands not compounded').to.be.instanceOf(command_1.CompoundCommandImpl);
(0, chai_1.expect)(result).to.be.like({ _commands: [base, appended] });
});
it('is given nothing to append', async () => {
const base = new TestCommand('test');
const result = (0, command_1.append)(base);
(0, chai_1.expect)(result, 'command was unnecessarily replaced').to.be.equal(base);
});
});
describe('groupByModelId', () => {
it('group compound command results by model', async () => {
const models = new Map([
['model1', { id: 'model1' }],
['model2', { id: 'model2' }],
]);
const command1 = new TestCommand('testProp1', 'model1');
const command2 = new TestCommand('testProp2', 'model1');
const command3 = new TestCommand('test', 'model2');
const compoundModel1 = new command_1.CompoundCommandImpl('test', command1, command2);
const compoundModel2 = new command_1.CompoundCommandImpl('test', command3);
const topCompound = new command_1.CompoundCommandImpl('test', compoundModel1, compoundModel2);
const result = await topCompound.execute(models.get.bind(models));
(0, chai_1.expect)(result).to.not.be.undefined;
if (result !== undefined) {
const groupedResult = (0, command_1.groupByModelId)(result);
(0, chai_1.expect)(groupedResult.size).to.equal(2);
}
});
});
describe('isSimpleCommandWithResult', () => {
it('is not', () => {
const command = new TestCommand('test', 'model1');
const isIt = (0, command_1.isSimpleCommandWithResult)(command);
(0, chai_1.expect)(isIt).to.be.false;
});
it('is', () => {
const command = new TestCommandWithResult('test', 'model1');
const isIt = (0, command_1.isSimpleCommandWithResult)(command);
(0, chai_1.expect)(isIt).to.be.true;
});
});
describe('unwrapReturnResult', () => {
it('pending', () => {
const command = new TestCommandWithResult('test', 'model1');
const unwrapped = (0, command_1.unwrapReturnResult)(command);
(0, chai_1.expect)(unwrapped).to.be.undefined;
});
it('failed', () => {
const command = Object.assign(new TestCommand('test', 'model1'), {
result: {
status: 'failed',
error: new Error('💣'),
},
});
(0, chai_1.expect)(() => (0, command_1.unwrapReturnResult)(command)).to.throw('💣');
});
it('failed without detail', () => {
const command = Object.assign(new TestCommand('test', 'model1'), {
result: {
status: 'failed',
},
});
(0, chai_1.expect)(() => (0, command_1.unwrapReturnResult)(command)).to.throw();
});
it('ready', () => {
const command = Object.assign(new TestCommand('test', 'model1'), {
result: {
status: 'ready',
value: 'Hello, world!',
},
});
const unwrapped = (0, command_1.unwrapReturnResult)(command);
(0, chai_1.expect)(unwrapped).to.be.eq('Hello, world!');
});
});
// This test ensures a wider test coverage, by crafting test cases that
// are not expected to happen in a more realistic scenario
it('additional corner cases', () => {
const command1 = new TestCommand('testProp2', 'model1');
const command2 = new TestCommand('test', 'model2');
const compound = new command_1.CompoundCommandImpl('test', command1, command2);
const result = new Map();
result.set(command1, undefined);
result.set(command2, []);
result.set(compound, []);
const groupedResult = (0, command_1.groupByModelId)(result);
(0, chai_1.expect)(groupedResult.size).to.equal(1);
(0, chai_1.expect)(groupedResult.get('model1')).to.be.undefined;
(0, chai_1.expect)(groupedResult.get('model2')).to.be.an('array').that.is.empty;
});
});
describe('CompoundCommandImpl', () => {
let getModel;
let commands;
let compound;
const forceAppend = (compound, ...commands) => {
// Cannot append via the API, so shoehorn it
compound._commands.push(...commands);
};
beforeEach(() => {
getModel = () => ({});
commands = [new AsyncTestCommand('a'), new AsyncTestCommand('b')];
compound = new command_1.CompoundCommandImpl('test', ...commands);
});
it('has a label', () => {
(0, chai_1.expect)(compound.label, 'wrong label').to.be.eq('test');
});
describe('execute', () => {
let sandbox;
beforeEach(() => {
sandbox = sinon_1.default.createSandbox();
});
afterEach(() => {
sandbox.restore();
});
it('executes the subcommands', async () => {
await compound.execute(getModel);
(0, chai_1.expect)(commands, 'commands not executed').to.be.like([
{ wasExecuted: true },
{ wasExecuted: true },
]);
});
it('collates the deltas', async () => {
const delta = await compound.execute(getModel);
(0, chai_1.expect)(delta, 'no delta returned').to.exist;
(0, chai_1.expect)(delta?.get(commands[0]), 'wrong result for command a').to.be.like([
{ op: 'add', path: 'a' },
]);
(0, chai_1.expect)(delta?.get(commands[1]), 'wrong result for command b').to.be.like([
{ op: 'add', path: 'b' },
]);
});
it('rewinds subcommands on failure', async () => {
const third = new AsyncTestCommand('c');
commands.push(third);
compound.append(third);
third.failOn('execute');
await (0, chai_1.expect)(compound.execute(getModel)).to.eventually.have.rejected;
(0, chai_1.expect)(commands, 'commands not rewound').to.be.like([
{ wasUndone: true, failed: false },
{ wasUndone: true, failed: false },
{ wasExecuted: false, failed: true },
]);
});
it('best-effort rewinds in case of rewind failure', async () => {
const third = new AsyncTestCommand('c');
commands.push(third);
compound.append(third);
third.failOn('execute');
commands[1].failOn('undo');
const consoleError = sandbox.stub(console, 'error');
await (0, chai_1.expect)(compound.execute(getModel)).to.eventually.have.rejected;
(0, chai_1.expect)(consoleError).to.have.been.calledWithMatch('model-manager/command:', 'Error in recovery of failed execute.');
(0, chai_1.expect)(commands, 'commands not rewound').to.be.like([
{ wasUndone: true, failed: false },
{ wasUndone: false, failed: true },
{ wasExecuted: false, failed: true },
]);
});
describe('asserts the preconditions', () => {
it('is non-executable by itself', async () => {
// Make it non-executable
await compound.execute(getModel);
await (0, chai_1.expect)(compound.execute(getModel), 'compound should not have executed').to.eventually.be.rejected;
});
it('is non-executable by a nested command', async () => {
// Make it non-executable
await commands[1].execute(getModel);
await (0, chai_1.expect)(compound.execute(getModel), 'compound should not have executed').to.eventually.be.rejected;
});
});
});
describe('undo', () => {
beforeEach(() => {
return compound.execute(getModel);
});
it('undoes the subcommands', async () => {
await compound.undo(getModel);
(0, chai_1.expect)(commands, 'commands not undone').to.be.like([
{ wasUndone: true },
{ wasUndone: true },
]);
});
it('collates the deltas', async () => {
const delta = await compound.undo(getModel);
(0, chai_1.expect)(delta, 'no delta returned').to.exist;
(0, chai_1.expect)(delta?.get(commands[0]), 'wrong result for command a').to.be.like([
{ op: 'remove', path: 'a' },
]);
(0, chai_1.expect)(delta?.get(commands[1]), 'wrong result for command b').to.be.like([
{ op: 'remove', path: 'b' },
]);
});
it('rewinds subcommands on failure', async () => {
const third = new AsyncTestCommand('c');
await third.execute();
commands.push(third);
forceAppend(compound, third);
commands[0].failOn('undo');
await (0, chai_1.expect)(compound.undo(getModel)).to.eventually.have.rejected;
(0, chai_1.expect)(commands, 'commands not rewound').to.be.like([
{ wasUndone: false, failed: true },
{ wasRedone: true, failed: false },
{ wasRedone: true, failed: false },
]);
});
describe('asserts the preconditions', () => {
it('is non-undoable by itself', async () => {
// Make it non-undoable
await compound.undo(getModel);
await (0, chai_1.expect)(compound.undo(getModel), 'compound should not have undone')
.to.eventually.be.rejected;
});
it('is non-undoable by a nested command', async () => {
// Make it non-undoable
await commands[0].undo(getModel);
await (0, chai_1.expect)(compound.undo(getModel), 'compound should not have undone')
.to.eventually.be.rejected;
});
});
});
describe('redo', () => {
beforeEach(async () => {
await compound.execute(getModel);
await compound.undo(getModel);
});
it('redoes the subcommands', async () => {
await compound.redo(getModel);
(0, chai_1.expect)(commands, 'commands not redone').to.be.like([
{ wasRedone: true },
{ wasRedone: true },
]);
});
it('collates the deltas', async () => {
const delta = await compound.redo(getModel);
(0, chai_1.expect)(delta, 'no delta returned').to.exist;
(0, chai_1.expect)(delta?.get(commands[0]), 'wrong result for command a').to.be.like([
{ op: 'add', path: 'a' },
]);
(0, chai_1.expect)(delta?.get(commands[1]), 'wrong result for command b').to.be.like([
{ op: 'add', path: 'b' },
]);
});
describe('asserts the preconditions', () => {
it('is non-redoable by itself', async () => {
// Make it non-redoable
await compound.redo(getModel);
await (0, chai_1.expect)(compound.redo(getModel), 'compound should not have redone')
.to.eventually.be.rejected;
});
it('is non-redoable by a nested command', async () => {
// Make it non-redoable
await commands[1].redo(getModel);
await (0, chai_1.expect)(compound.redo(getModel), 'compound should not have redone')
.to.eventually.be.rejected;
});
});
});
describe('canExecute', () => {
let sandbox;
beforeEach(() => {
sandbox = sinon_1.default.createSandbox();
});
afterEach(() => {
sandbox.restore();
});
it('is true', () => {
return (0, chai_1.expect)(compound.canExecute(getModel), 'compound not executable').to
.eventually.be.true;
});
it('is true with logs by reason of being an empty compound', async () => {
const spyConsoleDebug = sandbox.spy(console, 'debug');
const empty = new command_1.CompoundCommandImpl('empty');
const result = await empty.canExecute(getModel);
(0, chai_1.expect)(result, 'empty compound is executable').to.be.true;
(0, chai_1.expect)(spyConsoleDebug).to.have.been.calledWithMatch('model-manager/command:', `No constituent command for ${empty.label}.`);
});
it('is false by reason of itself', async () => {
await compound.execute(getModel); // Make it non-executable by executing it
return (0, chai_1.expect)(compound.canExecute(getModel), 'compound is executable').to
.eventually.be.false;
});
describe('asynchronous commands', () => {
it('is false by reason of a nested command', async () => {
await commands[1].execute(getModel); // Make one non-executable by executing it
return (0, chai_1.expect)(compound.canExecute(getModel), 'compound is executable')
.to.eventually.be.false;
});
});
describe('synchronous commands', () => {
beforeEach(() => {
commands = [new TestCommand('a'), new TestCommand('b')];
compound = new command_1.CompoundCommandImpl('test', ...commands);
});
it('is false by reason of a nested command', () => {
commands[1].execute(getModel); // Make one non-executable by executing it
return (0, chai_1.expect)(compound.canExecute(getModel), 'compound is executable')
.to.eventually.be.false;
});
});
});
describe('canUndo', () => {
let sandbox;
beforeEach(() => {
sandbox = sinon_1.default.createSandbox();
return compound.execute(getModel);
});
afterEach(() => {
sandbox.restore();
});
it('is true', () => {
return (0, chai_1.expect)(compound.canUndo(getModel), 'compound not undoable').to
.eventually.be.true;
});
it('is false by reason of itself', async () => {
// Make it non-undoable by undoing it
await compound.undo(getModel);
return (0, chai_1.expect)(compound.canUndo(getModel), 'compound is undoable').to
.eventually.be.false;
});
describe('asynchronous commands', () => {
it('is false by reason of a nested command', async () => {
// Make one non-undoable by undoing it
await commands[0].undo(getModel);
return (0, chai_1.expect)(compound.canUndo(getModel), 'compound is undoable').to
.eventually.be.false;
});
});
describe('synchronous commands', () => {
beforeEach(() => {
commands = [new TestCommand('a'), new TestCommand('b')];
compound = new command_1.CompoundCommandImpl('test', ...commands);
return compound.execute(getModel);
});
it('is false by reason of a nested command', () => {
commands[0].undo(getModel); // Make one non-undoable by executing it
return (0, chai_1.expect)(compound.canUndo(getModel), 'compound is undoable').to
.eventually.be.false;
});
});
describe('no sub commands', () => {
beforeEach(() => {
sandbox = sinon_1.default.createSandbox();
compound = new command_1.CompoundCommandImpl('test');
return compound.execute(getModel);
});
afterEach(() => {
sandbox.restore();
});
it('is true even though no command', async () => {
const spyConsoleDebug = sandbox.spy(console, 'debug');
const result = await compound.canUndo(getModel);
(0, chai_1.expect)(result, 'compound is not undoable').to.be.true;
(0, chai_1.expect)(spyConsoleDebug).to.have.been.calledWithMatch('model-manager/command:', `No constituent command for ${compound.label}.`);
});
});
});
describe('canRedo', () => {
let sandbox;
beforeEach(async () => {
sandbox = sinon_1.default.createSandbox();
await compound.execute(getModel);
await compound.undo(getModel);
});
afterEach(() => {
sandbox.restore();
});
it('is true', () => {
return (0, chai_1.expect)(compound.canRedo(getModel), 'compound not redoable').to
.eventually.be.true;
});
it('is false by reason of itself', async () => {
// Make it non-undoable by redoing it
await compound.redo(getModel);
return (0, chai_1.expect)(compound.canRedo(getModel), 'compound is redoable').to
.eventually.be.false;
});
describe('asynchronous commands', () => {
it('is false by reason of a nested command', async () => {
// Make one non-undoable by redoing it
await commands[1].redo(getModel);
return (0, chai_1.expect)(compound.canRedo(getModel), 'compound is redoable').to
.eventually.be.false;
});
});
describe('synchronous commands', () => {
beforeEach(async () => {
commands = [new TestCommand('a'), new TestCommand('b')];
compound = new command_1.CompoundCommandImpl('test', ...commands);
await compound.execute(getModel);
await compound.undo(getModel);
});
it('is false by reason of a nested command', () => {
// Make one non-redoable by redoing it
commands[1].redo(getModel);
return (0, chai_1.expect)(compound.canRedo(getModel), 'compound is redoable').to
.eventually.be.false;
});
});
describe('no sub commands', () => {
beforeEach(async () => {
compound = new command_1.CompoundCommandImpl('test');
await compound.execute(getModel);
await compound.undo(getModel);
});
it('is true even though no command', async () => {
const spyConsoleDebug = sandbox.spy(console, 'debug');
const result = await compound.canRedo(getModel);
(0, chai_1.expect)(result, 'compound is not redoable').to.be.true;
(0, chai_1.expect)(spyConsoleDebug).to.have.been.calledWithMatch('model-manager/command:', `No constituent command for ${compound.label}.`);
});
});
});
describe('append', () => {
let newCommand;
beforeEach(() => {
newCommand = new AsyncTestCommand('c');
});
it('is appendable', () => {
compound.append(newCommand);
(0, chai_1.expect)(compound, 'new command not appended').to.be.like({
_commands: [{ label: 'a' }, { label: 'b' }, { label: 'c' }],
});
});
it('is frozen', async () => {
await compound.execute(getModel);
(0, chai_1.expect)(() => compound.append(newCommand), 'should have thrown precondition error').to.throw();
});
});
describe('getCommands', () => {
it('returns the commands', () => {
const list = compound.getCommands();
(0, chai_1.expect)(list, 'wrong commands returned').to.be.like([
{ label: 'a' },
{ label: 'b' },
]);
});
it('returns a safe copy', () => {
const list = compound.getCommands();
list.splice(0, 1);
(0, chai_1.expect)(compound, 'command removed from the compound').to.be.like({
_commands: [{ label: 'a' }, { label: 'b' }],
});
});
});
describe('nested compounds', () => {
let nested;
let nestedCompound;
let sandbox;
const obtrudeExecute = (cmd) => {
const stub = sandbox.stub(cmd, 'execute').callsFake((...args) => {
stub.wrappedMethod.apply(cmd, args);
return undefined;
});
};
beforeEach(() => {
nested = [new AsyncTestCommand('c'), new AsyncTestCommand('d')];
nestedCompound = new command_1.CompoundCommandImpl('nested', ...nested);
compound.append(nestedCompound);
sandbox = sinon_1.default.createSandbox();
});
afterEach(() => {
sandbox.restore();
});
it('collates the nested deltas', async () => {
const delta = await compound.execute(getModel);
(0, chai_1.expect)(delta, 'no delta returned').to.exist;
(0, chai_1.expect)(delta?.size, 'wrong number of deltas returned').to.be.eq(4);
(0, chai_1.expect)(delta?.get(nested[0])).to.be.like([{ op: 'add', path: 'c' }]);
(0, chai_1.expect)(delta?.get(nested[1])).to.be.like([{ op: 'add', path: 'd' }]);
(0, chai_1.expect)(delta?.has(commands[0])).to.be.true;
(0, chai_1.expect)(delta?.has(commands[1])).to.be.true;
});
it('returns partial results', async () => {
obtrudeExecute(nested[0]);
commands.forEach((cmd) => sandbox.spy(cmd, 'execute'));
const delta = await compound.execute(getModel);
(0, chai_1.expect)(delta, 'a partial delta was not returned').to.exist;
(0, chai_1.expect)(delta?.get(nested[1])).to.be.like([{ op: 'add', path: 'd' }]);
(0, chai_1.expect)(Array.from(delta?.keys() ?? [])).to.include.members(commands);
commands.forEach((cmd) => (0, chai_1.expect)(cmd.execute).to.have.been.called);
});
it('returns undefined if no results', async () => {
nested.forEach(obtrudeExecute);
commands.forEach(obtrudeExecute);
const delta = await compound.execute(getModel);
(0, chai_1.expect)(delta, 'a delta was returned').not.to.exist;
nested.forEach((cmd) => (0, chai_1.expect)(cmd.execute).to.have.been.called);
commands.forEach((cmd) => (0, chai_1.expect)(cmd.execute).to.have.been.called);
});
});
describe('iteration', () => {
it('empty compound', () => {
const labels = new command_1.CompoundCommandImpl('test').map((cmd) => cmd.label);
(0, chai_1.expect)(labels).to.eql([]);
});
it('flat compound', () => {
const compound = new command_1.CompoundCommandImpl('test', new TestCommand('a'), new TestCommand('b'));
const labels = compound.map((cmd) => cmd.label);
(0, chai_1.expect)(labels).to.eql(['a', 'b']);
});
it('nested compound', () => {
const compound = new command_1.CompoundCommandImpl('test', new command_1.CompoundCommandImpl('one', new TestCommand('a'), new TestCommand('b')), new TestCommand('c'), new command_1.CompoundCommandImpl('two', new TestCommand('d'), new command_1.CompoundCommandImpl('three', new TestCommand('e'), new TestCommand('f')), new TestCommand('g')));
const labels = compound.map((cmd) => cmd.label);
(0, chai_1.expect)(labels).to.eql(['a', 'b', 'c', 'd', 'e', 'f', 'g']);
});
});
describe('Compounds of compounds', () => {
beforeEach(async () => {
commands = [
new AsyncTestCommand('a', 'a'),
new TestCommand('b'),
new AsyncTestCommand('c', 'c'),
];
compound = new command_1.CompoundCommandImpl('outer', commands[0], new command_1.CompoundCommandImpl('inner', ...commands.slice(1)));
});
it('execute', async () => {
await compound.execute(getModel);
(0, chai_1.expect)(commands, 'commands not executed').to.be.like([
{ wasExecuted: true },
{ wasExecuted: true },
{ wasExecuted: true },
]);
});
it('undo', async () => {
await compound.execute(getModel);
await compound.undo(getModel);
(0, chai_1.expect)(commands, 'commands not undone').to.be.like([
{ wasUndone: true },
{ wasUndone: true },
{ wasUndone: true },
]);
});
it('redo', async () => {
await compound.execute(getModel);
await compound.undo(getModel);
await compound.redo(getModel);
(0, chai_1.expect)(commands, 'commands not redone').to.be.like([
{ wasRedone: true },
{ wasRedone: true },
{ wasRedone: true },
]);
});
it('rewinds subcommands on failure', async () => {
await compound.execute(getModel);
const fourth = new AsyncTestCommand('d');
await fourth.execute();
commands.push(fourth);
forceAppend(compound, fourth);
commands[0].failOn('undo');
await (0, chai_1.expect)(compound.undo(getModel)).to.eventually.have.rejected;
(0, chai_1.expect)(commands, 'commands not rewound').to.be.like([
{ wasUndone: false, failed: true },
{ wasRedone: true },
{ wasRedone: true },
{ wasRedone: true },
]);
});
it('model AWOL on execute', async () => {
let countdown = 2;
getModel = (modelId) => {
if (modelId === 'c' && countdown-- <= 0) {
return undefined;
}
return {};
};
await (0, chai_1.expect)(compound.execute(getModel)).to.eventually.have.rejectedWith('No model on which to execute');
});
it('model AWOL on rewind', async () => {
const bomb = new AsyncTestCommand('Boom!');
bomb.failOn('execute');
commands.push(bomb);
forceAppend(compound, bomb);
let countdown = 2;
getModel = (modelId) => {
if (modelId === 'a' && countdown-- <= 0) {
return undefined;
}
return {};
};
// The exception from rewind isn't actually surfaced
await (0, chai_1.expect)(compound.execute(getModel)).to.eventually.have.rejected;
(0, chai_1.expect)(commands[1]).to.be.like({ wasUndone: true });
(0, chai_1.expect)(commands[0]).to.be.like({ wasUndone: false });
});
});
});
class TestCommand {
constructor(label, modelId = '') {
this.label = label;
this.modelId = modelId;
this.wasExecuted = false;
this.wasUndone = false;
this.wasRedone = false;
}
canExecute() {
return !this.wasExecuted && !this.wasUndone && !this.wasRedone;
}
canUndo() {
return this.wasExecuted || this.wasRedone;
}
canRedo() {
return this.wasUndone;
}
execute() {
if (!this.canExecute()) {
throw new Error('cannot execute');
}
this.wasExecuted = true;
this.wasUndone = false;
this.wasRedone = false;
return [{ op: 'add', path: this.label, value: 'test-value' }];
}
undo() {
if (!this.canUndo()) {
throw new Error('cannot undo');
}
this.wasExecuted = false;
this.wasUndone = true;
this.wasRedone = false;
return [{ op: 'remove', path: this.label }];
}
redo() {
if (!this.canRedo()) {
throw new Error('cannot redo');
}
this.wasExecuted = false;
this.wasUndone = false;
this.wasRedone = true;
return [{ op: 'add', path: this.label, value: 'test-value' }];
}
}
class AsyncTestCommand {
constructor(label, modelId = '') {
this.label = label;
this.modelId = modelId;
this.wasExecuted = false;
this.wasUndone = false;
this.wasRedone = false;
this.failed = false;
}
failOn(op) {
this.failOnOp = op;
}
async canExecute() {
this.maybeFailOn('canExecute');
return !this.wasExecuted && !this.wasUndone && !this.wasRedone;
}
async canUndo() {
this.maybeFailOn('canUndo');
return this.wasExecuted || this.wasRedone;
}
async canRedo() {
this.maybeFailOn('canRedo');
return this.wasUndone;
}
async execute() {
if (!this.canExecute()) {
throw new Error('cannot execute');
}
this.maybeFailOn('execute');
this.wasExecuted = true;
this.wasUndone = false;
this.wasRedone = false;
return [{ op: 'add', path: this.label, value: 'test-value' }];
}
async undo() {
if (!this.canUndo()) {
throw new Error('cannot undo');
}
this.maybeFailOn('undo');
this.wasExecuted = false;
this.wasUndone = true;
this.wasRedone = false;
return [{ op: 'remove', path: this.label }];
}
async redo() {
if (!this.canRedo()) {
throw new Error('cannot redo');
}
this.maybeFailOn('redo');
this.wasExecuted = false;
this.wasUndone = false;
this.wasRedone = true;
return [{ op: 'add', path: this.label, value: 'test-value' }];
}
maybeFailOn(op) {
if (this.failOnOp === op) {
this.failed = true;
this.failOnOp = undefined;
throw new Error(`Failed on ${op} as directed.`);
}
}
}
class TestCommandWithResult extends TestCommand {
constructor() {
super(...arguments);
this.result = { status: 'pending' };
}
}
//# sourceMappingURL=command.spec.js.map