@eclipse-emfcloud/model-manager
Version:
Command-based model editing with undo/redo.
750 lines • 34.1 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 assert_1 = require("assert");
const chai_1 = __importStar(require("chai"));
const fast_json_patch_1 = require("fast-json-patch");
const lodash_1 = require("lodash");
const sinon_1 = __importDefault(require("sinon"));
const sinon_chai_1 = __importDefault(require("sinon-chai"));
const core_1 = require("../../core");
const patch_1 = require("../../patch");
const core_model_manager_impl_1 = require("../core-model-manager-impl");
chai_1.default.use(sinon_chai_1.default);
const editingContext = (id) => id;
describe('CoreModelManagerImpl', () => {
let modelManager;
const key1 = 'model1';
const key2 = 'model2';
const model1 = { foo1: 'foo1', bar1: 'bar1' };
const model2 = { foo2: 'foo2', bar2: 'bar2' };
let command1;
let command2;
let context1;
let context2;
let commandStack;
const getModel1 = () => {
const result = modelManager.getModel(key1);
(0, chai_1.expect)(result).to.exist;
return result;
};
const getModel2 = () => {
const result = modelManager.getModel(key2);
(0, chai_1.expect)(result).to.exist;
return result;
};
beforeEach(() => {
modelManager = new core_model_manager_impl_1.CoreModelManagerImpl();
command1 = new TestCommand('command1', key1);
command2 = new TestCommand('command2', key2);
context1 = editingContext('context1');
context2 = editingContext('context2');
commandStack = modelManager.getCommandStack();
});
describe('register & retrieve & remove models', () => {
it('set & get models', () => {
modelManager.setModel(key1, model1);
modelManager.setModel(key2, model2);
const m1 = modelManager.getModel(key1);
(0, chai_1.expect)(m1, 'model1 has not been set correctly').to.deep.equal(model1);
const m2 = modelManager.getModel(key2);
(0, chai_1.expect)(m2, 'model2 has not been set correctly').to.deep.equal(model2);
});
it('register a model to an already existing key', () => {
try {
modelManager.setModel(key1, model1);
modelManager.setModel(key1, model2);
}
catch (_err) {
//success
return;
}
(0, assert_1.fail)('should have thrown an error as key1 already exists');
});
it('remove a model', () => {
modelManager.setModel(key1, model1);
modelManager.setModel(key2, model2);
const m1 = modelManager.removeModel(key1);
(0, chai_1.expect)(m1, 'removed model is not the expected one').to.deep.equal(model1);
(0, chai_1.expect)(modelManager.getModel(key1), 'removed model should not be present anymore in the modelManager').to.be.undefined;
(0, chai_1.expect)(modelManager.getModel(key2), 'removing a model should not changed other registered models').to.deep.equal(model2);
});
it('remove a model not registered', () => {
modelManager.setModel(key1, model1);
const m2 = modelManager.removeModel(key2);
(0, chai_1.expect)(m2, 'no model should be removed').to.be.undefined;
});
it('get a model ID', () => {
modelManager.setModel(key1, model1);
const _model = modelManager.getModel(key1);
(0, chai_1.expect)(_model).to.exist;
const firstEdition = _model;
(0, chai_1.expect)(modelManager.getModelId(firstEdition)).to.be.equal(key1);
modelManager.getCommandStack().execute((0, patch_1.createModelUpdaterCommand)('patch', key1, (model) => {
model.bar1 = 'New value';
}), context1);
// Historical editions of models are remembered until they're GCed
(0, chai_1.expect)(modelManager.getModelId(firstEdition)).to.be.equal(key1);
});
it('get model IDs', () => {
(0, chai_1.expect)(modelManager.getModelIds(), 'should not have model IDs').to.eql([]);
modelManager.setModel(key1, model1);
(0, chai_1.expect)(modelManager.getModelIds(), 'should just have model ID 1')
.to.include(key1)
.and.have.length(1);
modelManager.setModel(key2, model2);
(0, chai_1.expect)(modelManager.getModelIds(), 'should also have model ID 2')
.to.include(key2)
.and.have.length(2);
modelManager.removeModel(key1);
(0, chai_1.expect)(modelManager.getModelIds(), 'should just have model ID 2')
.to.include(key2)
.and.have.length(1);
});
});
describe('manipulating the commandStack', () => {
it('get commandStack for editing the models', () => {
modelManager.setModel(key1, model1);
let commandStack = modelManager.getCommandStack();
(0, chai_1.expect)(commandStack, 'the commandStack should be defined').to.not.be
.undefined;
modelManager.removeModel(key1);
commandStack = modelManager.getCommandStack();
(0, chai_1.expect)(commandStack, 'the commandStack should still be defined even when all models are removed').to.not.be.undefined;
});
it('get commandStack for editing the models when no registered model', () => {
const commandStack = modelManager.getCommandStack();
(0, chai_1.expect)(commandStack, 'the commandStack should be defined even when no model is registered').to.not.be.undefined;
});
});
describe('subscription to model changes', () => {
it('subscribe to an existing model changes', async () => {
let counter = 0;
modelManager.setModel(key1, model1);
modelManager.setModel(key2, model2);
const sub1 = modelManager.subscribe(key1);
const sub2 = modelManager.subscribe(key1);
const sub3 = modelManager.subscribe(key2);
sub1.onModelChanged = (key, model, delta) => {
(0, chai_1.expect)(key).to.equal(key1);
(0, chai_1.expect)(model).to.be.deep.equal(model1);
(0, chai_1.expect)(model.command1).to.be.equal('test-value');
(0, chai_1.expect)(delta).to.be.like([
{ op: 'add', path: 'command1', value: 'test-value' },
]);
counter++;
};
sub2.onModelChanged = (key, model, delta) => {
(0, chai_1.expect)(key).to.equal(key1);
(0, chai_1.expect)(model).to.be.deep.equal(model1);
(0, chai_1.expect)(model.command1).to.be.equal('test-value');
(0, chai_1.expect)(delta).to.be.like([
{ op: 'add', path: 'command1', value: 'test-value' },
]);
counter++;
};
sub3.onModelChanged = sinon_1.default.spy();
await commandStack.execute(command1, context1);
(0, chai_1.expect)(counter).to.be.equal(2);
(0, chai_1.expect)(sub3.onModelChanged).to.be.not.called;
await commandStack.execute(command2, context2);
(0, chai_1.expect)(sub3.onModelChanged).to.be.calledOnce;
});
it('unsubscribe to an existing model changes', async () => {
modelManager.setModel(key1, model1);
const sub1 = modelManager.subscribe(key1);
sub1.onModelChanged = sinon_1.default.spy();
const sub2 = modelManager.subscribe(key1);
sub2.onModelChanged = sinon_1.default.spy();
const sub3 = modelManager.subscribe(key2);
sub3.onModelChanged = sinon_1.default.spy();
sub1.close();
sub2.close();
sub3.close();
await commandStack.execute(command1, context1);
(0, chai_1.expect)(sub1.onModelChanged).to.be.not.called;
(0, chai_1.expect)(sub2.onModelChanged).to.be.not.called;
(0, chai_1.expect)(sub3.onModelChanged).to.be.not.called;
});
it('execute a command on closed subscriber for a specific model', async () => {
modelManager.setModel(key1, model1);
modelManager.setModel(key2, model1);
modelManager.subscribe(key1);
const sub2 = modelManager.subscribe(key2);
sub2.onModelChanged = sinon_1.default.spy();
sub2.close();
await commandStack.execute(command1, context1);
(0, chai_1.expect)(sub2.onModelChanged).to.not.be.called;
});
it('execute a command on closed subscriber for all models', async () => {
modelManager.setModel(key1, model1);
modelManager.setModel(key2, model1);
modelManager.subscribe();
const sub2 = modelManager.subscribe();
sub2.onModelChanged = sinon_1.default.spy();
sub2.close();
await commandStack.execute(command1, context1);
(0, chai_1.expect)(sub2.onModelChanged).to.not.be.called;
});
it('unsubscribe twice to same modelId', async () => {
modelManager.setModel(key1, model1);
const sub = modelManager.subscribe(key1);
sub.onModelChanged = sinon_1.default.spy();
sub.close();
sub.close();
await commandStack.execute(command1, context1);
(0, chai_1.expect)(sub.onModelChanged).to.not.be.called;
});
it('execute a command with undefined result w/o notification', async () => {
modelManager.setModel(key1, model1);
const commandUndefined = new TestCommandUndefined('undefined', key1);
const sub = modelManager.subscribe(key1);
sub.onModelChanged = sinon_1.default.spy();
await commandStack.execute(commandUndefined, context1);
await commandStack.undo(context1);
await commandStack.redo(context1);
(0, chai_1.expect)(sub.onModelChanged).to.not.be.called;
const commandUndefined2 = new TestCommandUndefined('undefined2', key1);
await commandStack.executeAndAppend(context1, commandUndefined2);
const sub2 = modelManager.subscribe(key1);
sub2.onModelChanged = sinon_1.default.spy();
(0, chai_1.expect)(sub2.onModelChanged).to.not.be.called;
});
it('subscribe to a model changes before having this model registered', async () => {
const sub = modelManager.subscribe(key2);
sub.onModelChanged = sinon_1.default.spy();
modelManager.setModel(key2, model2);
await commandStack.execute(command2, context2);
(0, chai_1.expect)(sub.onModelChanged).to.be.calledOnce;
});
it('subscribe to all existing model changes', async () => {
modelManager.setModel(key1, model1);
modelManager.setModel(key2, model2);
const sub = modelManager.subscribe();
sub.onModelChanged = sinon_1.default.spy();
await commandStack.execute(command1, context1);
(0, chai_1.expect)(sub.onModelChanged).to.be.calledOnce;
});
it('unsubscribe to all model changes w/o having any model registered', async () => {
const sub1 = modelManager.subscribe();
const sub2 = modelManager.subscribe();
sub1.onModelChanged = sinon_1.default.spy();
sub2.onModelChanged = sinon_1.default.spy();
sub1.close();
sub1.close();
sub2.close();
modelManager.setModel(key1, model1);
await commandStack.execute(command1, context1);
(0, chai_1.expect)(sub1.onModelChanged).to.not.be.called;
(0, chai_1.expect)(sub2.onModelChanged).to.not.be.called;
});
it('subscriber not notified when execute provides no delta', async () => {
modelManager.setModel(key1, model1);
const sub = modelManager.subscribe(key1);
sub.onModelChanged = sinon_1.default.stub();
// Instrument the command to return no delta
command1.execute = () => undefined;
await commandStack.execute(command1, context1);
(0, chai_1.expect)(sub.onModelChanged).not.to.be.called;
});
it('subscriber not notified when undo provides no delta', async () => {
modelManager.setModel(key1, model1);
await commandStack.execute(command1, context1);
const sub = modelManager.subscribe(key1);
sub.onModelChanged = sinon_1.default.stub();
// Instrument the command to return no delta on undo
command1.undo = () => undefined;
await commandStack.undo(context1);
(0, chai_1.expect)(sub.onModelChanged).not.to.be.called;
});
it('subscriber not notified when redo provides no delta', async () => {
modelManager.setModel(key1, model1);
await commandStack.execute(command1, context1);
await commandStack.undo(context1);
const sub = modelManager.subscribe(key1);
sub.onModelChanged = sinon_1.default.stub();
// Instrument the command to return no delta on redo
command1.redo = () => undefined;
await commandStack.redo(context1);
(0, chai_1.expect)(sub.onModelChanged).not.to.be.called;
});
it('subscriber without callback does not break notifications', async () => {
modelManager.setModel(key1, model1);
const blankSub = modelManager.subscribe(key1);
blankSub.onModelChanged = undefined;
const sub = modelManager.subscribe(key1);
sub.onModelChanged = sinon_1.default.spy();
await commandStack.execute(command1, context1);
(0, chai_1.expect)(sub.onModelChanged).to.be.called;
});
it('no subscription call-backs for unmanaged model', async () => {
// Instrument a compound command to invent a delta on an unmanaged model
const compoundCommand = (0, core_1.append)(command1, command2);
// Sinon doesn't provide the wrapped method for async stub
const wrappedMethod = compoundCommand.execute;
sinon_1.default.stub(compoundCommand, 'execute').callsFake(async (...args) => {
const result = (await wrappedMethod.apply(compoundCommand, args));
result.set(new TestCommand('interloper', 'interloper'),
// May as well be a nonsense "delta"
[{ op: 'test', path: '/modelIs', value: 'interloper' }]);
return result;
});
modelManager.setModel(key1, model1);
modelManager.setModel(key2, model2);
const sub = modelManager.subscribe();
sub.onModelChanged = sinon_1.default.spy();
await commandStack.execute(compoundCommand, context1);
(0, chai_1.expect)(sub.onModelChanged).to.be.called;
(0, chai_1.expect)(sub.onModelChanged).not.to.be.calledWithMatch('interloper', sinon_1.default.match.any, sinon_1.default.match.any);
});
});
describe('Concurrency scenarios', () => {
beforeEach(() => {
modelManager.setModel(key1, (0, lodash_1.cloneDeep)(model1));
modelManager.setModel(key2, (0, lodash_1.cloneDeep)(model2));
});
it('single sequential model undo/redo', async () => {
await commandStack.execute(new AsyncPatchCommand('1', key1, [
{ op: 'test', path: '/bar1', value: 'bar1' },
{ op: 'replace', path: '/bar1', value: 'New Value' },
]), context1);
await commandStack.execute(new AsyncPatchCommand('2', key1, [
{ op: 'test', path: '/bar1', value: 'New Value' },
{ op: 'replace', path: '/bar1', value: '42' },
]), context1);
(0, chai_1.expect)(getModel1()).to.be.like({
foo1: 'foo1',
bar1: '42',
});
await commandStack.undo(context1);
(0, chai_1.expect)(getModel1()).to.be.like({
foo1: 'foo1',
bar1: 'New Value',
});
await commandStack.undo(context1);
(0, chai_1.expect)(getModel1()).to.be.like({
foo1: 'foo1',
bar1: 'bar1',
});
await commandStack.redo(context1);
(0, chai_1.expect)(getModel1()).to.be.like({
foo1: 'foo1',
bar1: 'New Value',
});
await commandStack.redo(context1);
(0, chai_1.expect)(getModel1()).to.be.like({
foo1: 'foo1',
bar1: '42',
});
});
it('single stacked model undo/redo', async () => {
commandStack.execute(new AsyncPatchCommand('1', key2, [
{ op: 'test', path: '/bar2', value: 'bar2' },
{ op: 'replace', path: '/bar2', value: 'New Value' },
]), context1);
await commandStack.execute(new AsyncPatchCommand('2', key2, [
{ op: 'test', path: '/bar2', value: 'New Value' },
{ op: 'replace', path: '/bar2', value: '42' },
]), context1);
(0, chai_1.expect)(getModel2()).to.be.like({
foo2: 'foo2',
bar2: '42',
});
await commandStack.undo(context1);
await commandStack.undo(context1);
(0, chai_1.expect)(getModel2()).to.be.like({
foo2: 'foo2',
bar2: 'bar2',
});
await commandStack.redo(context1);
await commandStack.redo(context1);
(0, chai_1.expect)(getModel2()).to.be.like({
foo2: 'foo2',
bar2: '42',
});
});
it('multiple sequential model undo/redo', async () => {
await commandStack.execute((0, core_1.append)(new AsyncPatchCommand('1a', key1, [
{ op: 'test', path: '/bar1', value: 'bar1' },
{ op: 'replace', path: '/bar1', value: 'New Value' },
]), new AsyncPatchCommand('1b', key2, [
{ op: 'test', path: '/bar2', value: 'bar2' },
{ op: 'replace', path: '/bar2', value: 'New Value 2' },
])), context1);
await commandStack.execute((0, core_1.append)(new AsyncPatchCommand('2a', key1, [
{ op: 'test', path: '/bar1', value: 'New Value' },
{ op: 'replace', path: '/bar1', value: '42' },
]), new AsyncPatchCommand('2b', key2, [
{ op: 'test', path: '/bar2', value: 'New Value 2' },
{ op: 'replace', path: '/bar2', value: '19' },
])), context1);
(0, chai_1.expect)(getModel1()).to.be.like({
foo1: 'foo1',
bar1: '42',
});
(0, chai_1.expect)(getModel2()).to.be.like({
foo2: 'foo2',
bar2: '19',
});
await commandStack.undo(context1);
(0, chai_1.expect)(getModel1()).to.be.like({
foo1: 'foo1',
bar1: 'New Value',
});
(0, chai_1.expect)(getModel2()).to.be.like({
foo2: 'foo2',
bar2: 'New Value 2',
});
await commandStack.undo(context1);
(0, chai_1.expect)(getModel1()).to.be.like({
foo1: 'foo1',
bar1: 'bar1',
});
(0, chai_1.expect)(getModel2()).to.be.like({
foo2: 'foo2',
bar2: 'bar2',
});
await commandStack.redo(context1);
(0, chai_1.expect)(getModel1()).to.be.like({
foo1: 'foo1',
bar1: 'New Value',
});
(0, chai_1.expect)(getModel2()).to.be.like({
foo2: 'foo2',
bar2: 'New Value 2',
});
await commandStack.redo(context1);
(0, chai_1.expect)(getModel1()).to.be.like({
foo1: 'foo1',
bar1: '42',
});
(0, chai_1.expect)(getModel2()).to.be.like({
foo2: 'foo2',
bar2: '19',
});
});
it('multiple stacked model undo/redo', async () => {
commandStack.execute((0, core_1.append)(new AsyncPatchCommand('1a', key1, [
{ op: 'test', path: '/bar1', value: 'bar1' },
{ op: 'replace', path: '/bar1', value: 'New Value' },
]), new AsyncPatchCommand('1b', key2, [
{ op: 'test', path: '/bar2', value: 'bar2' },
{ op: 'replace', path: '/bar2', value: 'New Value 2' },
])), context1);
await commandStack.execute((0, core_1.append)(new AsyncPatchCommand('2a', key1, [
{ op: 'test', path: '/bar1', value: 'New Value' },
{ op: 'replace', path: '/bar1', value: '42' },
]), new AsyncPatchCommand('2b', key2, [
{ op: 'test', path: '/bar2', value: 'New Value 2' },
{ op: 'replace', path: '/bar2', value: '19' },
])), context1);
(0, chai_1.expect)(getModel1()).to.be.like({
foo1: 'foo1',
bar1: '42',
});
(0, chai_1.expect)(getModel2()).to.be.like({
foo2: 'foo2',
bar2: '19',
});
await commandStack.undo(context1);
await commandStack.undo(context1);
(0, chai_1.expect)(getModel1()).to.be.like({
foo1: 'foo1',
bar1: 'bar1',
});
(0, chai_1.expect)(getModel2()).to.be.like({
foo2: 'foo2',
bar2: 'bar2',
});
await commandStack.redo(context1);
await commandStack.redo(context1);
(0, chai_1.expect)(getModel1()).to.be.like({
foo1: 'foo1',
bar1: '42',
});
(0, chai_1.expect)(getModel2()).to.be.like({
foo2: 'foo2',
bar2: '19',
});
});
});
describe('Edge Cases', () => {
let workingCopyManager;
beforeEach(() => {
workingCopyManager = modelManager._modelStore;
});
it('attempt to open working copy manager already open', () => {
workingCopyManager.open(['testId']);
(0, chai_1.expect)(workingCopyManager.isOpen(['testId'])).to.be.true;
(0, chai_1.expect)(() => workingCopyManager.open(['testId'])).to.throw('Already open');
});
it('attempt to get a working copy when not open', () => {
(0, chai_1.expect)(workingCopyManager.isOpen(['testId'])).to.be.false;
(0, chai_1.expect)(() => workingCopyManager.getWorkingCopy('foo')).to.throw('Not open');
});
it('attempt to get a working copy for nonexistent model', () => {
workingCopyManager.open(['testId']);
(0, chai_1.expect)(() => workingCopyManager.getWorkingCopy('none such')).to.throw('Not open');
});
it('attempt to get a working copy for nonexistent model', () => {
workingCopyManager.open(['nonesuch']);
try {
(0, chai_1.expect)(workingCopyManager.getWorkingCopy('nonesuch')).to.be.undefined;
}
finally {
workingCopyManager.cancel(['nonesuch']);
}
});
it('commit a delta for non-open model', () => {
(0, chai_1.expect)(workingCopyManager.isOpen(['nonesuch'])).to.be.false;
workingCopyManager.open([key1]);
(0, chai_1.expect)(workingCopyManager.isOpen([key1])).to.be.true;
workingCopyManager.getWorkingCopy(key1);
const results = new Map();
results.set(new TestCommandUndefined('Working Copies Test', 'nonesuch'), []);
try {
workingCopyManager.commit(results, ['nonesuch']);
(0, chai_1.expect)(workingCopyManager.isOpen(['nonesuch'])).to.be.false;
(0, chai_1.expect)(workingCopyManager.isOpen([key1])).to.be.true;
}
finally {
workingCopyManager.cancel([key1]);
}
});
it('no-op deferred command', async () => {
modelManager.setModel(key1, model1);
modelManager.setModel(key2, model2);
// a deferred command that specifies 2 models, but only uses one.
const deferredCommand = (0, core_1.createDeferredCompoundCommand)('test command', [key1, key2], () => {
return [new TestCommand('test', key1)];
});
await commandStack.execute(deferredCommand, context1);
(0, chai_1.expect)(workingCopyManager.isOpen([key1])).to.be.false;
(0, chai_1.expect)(workingCopyManager.isOpen([key2])).to.be.false;
});
describe('concurrent working copy sets', () => {
const key3 = 'model3';
const key4 = 'model4';
const model3 = { foo3: 'foo3', bar3: 'bar3' };
const model4 = { foo4: 'foo4', bar4: 'bar4' };
beforeEach(() => {
[
[key1, model1],
[key2, model2],
[key3, model3],
[key4, model4],
].forEach(([key, model]) => modelManager.setModel(key, model));
});
afterEach(() => {
const modelIds = [key4, key3, key2, key1];
workingCopyManager.cancel(modelIds);
modelIds.forEach(modelManager.removeModel.bind(modelManager));
});
it('non-conflicting open', () => {
workingCopyManager.open([key1, key3]);
(0, chai_1.expect)(workingCopyManager.isOpen([key1, key3])).to.be.true;
(0, chai_1.expect)(workingCopyManager.isOpen([key1, key2, key3, key4])).to.be.true;
(0, chai_1.expect)(workingCopyManager.isOpen([key2, key4])).to.be.false;
workingCopyManager.open([key2, key4]);
(0, chai_1.expect)(workingCopyManager.isOpen([key2, key4])).to.be.true;
(0, chai_1.expect)(workingCopyManager.isOpen([key1, key2, key3, key4])).to.be.true;
});
it('non-conflicting working copy cancel', () => {
workingCopyManager.open([key1, key3]);
workingCopyManager.open([key2, key4]);
(0, chai_1.expect)(workingCopyManager.getWorkingCopy(key1)).to.be.like(model1);
(0, chai_1.expect)(workingCopyManager.getWorkingCopy(key2)).to.be.like(model2);
(0, chai_1.expect)(workingCopyManager.getWorkingCopy(key3)).to.be.like(model3);
(0, chai_1.expect)(workingCopyManager.getWorkingCopy(key4)).to.be.like(model4);
workingCopyManager.cancel([key2, key4]);
(0, chai_1.expect)(workingCopyManager.getWorkingCopy(key1)).to.be.like(model1);
(0, chai_1.expect)(workingCopyManager.getWorkingCopy(key3)).to.be.like(model3);
(0, chai_1.expect)(() => workingCopyManager.getWorkingCopy(key2)).to.throw();
(0, chai_1.expect)(() => workingCopyManager.getWorkingCopy(key4)).to.throw();
});
it('working copy conflict', () => {
workingCopyManager.open([key1, key3]);
(0, chai_1.expect)(workingCopyManager.getWorkingCopy(key1)).to.be.like(model1);
(0, chai_1.expect)(workingCopyManager.getWorkingCopy(key3)).to.be.like(model3);
(0, chai_1.expect)(() => workingCopyManager.open([key2, key3])).to.throw();
(0, chai_1.expect)(workingCopyManager.isOpen([key1, key3])).to.be.true;
(0, chai_1.expect)(workingCopyManager.isOpen([key2, key3])).to.be.true; // It's "some" semantics, not "every"
(0, chai_1.expect)(workingCopyManager.isOpen([key2])).to.be.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(model) {
if (!this.canExecute()) {
throw new Error('cannot execute');
}
this.wasExecuted = true;
this.wasUndone = false;
this.wasRedone = false;
Object.defineProperty(model, this.label, {
value: 'test-value',
writable: true,
});
return [{ op: 'add', path: this.label, value: 'test-value' }];
}
undo(model) {
if (!this.canUndo()) {
throw new Error('cannot undo');
}
this.wasExecuted = false;
this.wasUndone = true;
this.wasRedone = false;
Reflect.deleteProperty(model, this.label);
return [{ op: 'remove', path: this.label }];
}
redo(model) {
if (!this.canRedo()) {
throw new Error('cannot redo');
}
this.wasExecuted = false;
this.wasUndone = false;
this.wasRedone = true;
Object.defineProperty(model, this.label, {
value: 'test-value',
writable: true,
});
return [{ op: 'add', path: this.label, value: 'test-value' }];
}
}
class TestCommandUndefined {
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 undefined;
}
undo() {
if (!this.canUndo()) {
throw new Error('cannot undo');
}
this.wasExecuted = false;
this.wasUndone = true;
this.wasRedone = false;
return undefined;
}
redo() {
if (!this.canRedo()) {
throw new Error('cannot redo');
}
this.wasExecuted = false;
this.wasUndone = false;
this.wasRedone = true;
return undefined;
}
}
class AsyncPatchCommand {
constructor(label, modelId, patch) {
this.label = label;
this.modelId = modelId;
this.patch = patch;
}
async canExecute() {
return this.canApplyAndReverse();
}
async canUndo() {
return this.canApplyAndReverse();
}
async canRedo() {
return this.canApplyAndReverse();
}
async canApplyAndReverse() {
return this.patch.length > 0;
}
execute(model) {
return this.applyAndReverse(model);
}
undo(model) {
return this.applyAndReverse(model);
}
redo(model) {
return this.applyAndReverse(model);
}
async applyAndReverse(model) {
const baseline = (0, lodash_1.cloneDeep)(model);
(0, fast_json_patch_1.applyPatch)(model, this.patch);
const result = (0, fast_json_patch_1.compare)(baseline, model, true);
this.patch = (0, fast_json_patch_1.compare)(model, baseline);
return result;
}
}
//# sourceMappingURL=core-model-manager-impl.spec.js.map