UNPKG

@eclipse-emfcloud/model-manager

Version:

Command-based model editing with undo/redo.

750 lines 34.1 kB
"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