UNPKG

@eclipse-emfcloud/model-manager

Version:

Command-based model editing with undo/redo.

779 lines 36.7 kB
"use strict"; 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 }); // ***************************************************************************** // 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 // ***************************************************************************** const chai_1 = __importStar(require("chai")); const chai_as_promised_1 = __importDefault(require("chai-as-promised")); const sinon_1 = __importDefault(require("sinon")); const sinon_chai_1 = __importDefault(require("sinon-chai")); const patch_command_1 = require("../patch-command"); const assert_1 = require("assert"); const chai_like_1 = __importDefault(require("chai-like")); const cloneDeep_1 = __importDefault(require("lodash/cloneDeep")); const core_1 = require("../../core"); 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('PatchCommand', () => { let sandbox; beforeEach(() => { sandbox = sinon_1.default.createSandbox(); }); afterEach(() => { sandbox.restore(); }); it('Simple execute/undo/redo', async () => { const patch = [ { op: 'replace', path: '/id', value: 'updated-test-id', }, ]; const document = (0, cloneDeep_1.default)(testDocument); const expectedDocument = (0, cloneDeep_1.default)(testDocument); expectedDocument.id = 'updated-test-id'; const command = new patch_command_1.PatchCommand('Test Command', 'document', patch); // Execute await (0, chai_1.expect)(command.canExecute(document)).to.eventually.be.true; const executeResult = await command.execute(document); (0, chai_1.expect)(document).to.deep.equal(expectedDocument); (0, chai_1.expect)(executeResult).to.not.be.undefined; if (executeResult) { // Should contain a single 'replace' operation (and maybe some optional 'test' operations) const operations = executeResult.filter((operation) => operation.op !== 'test'); (0, chai_1.expect)(operations).to.be.an('array').of.length(1); (0, chai_1.expect)(operations[0].op).to.equal('replace'); } // Undo (0, chai_1.expect)(command.canUndo(document)).to.be.true; command.undo(document); (0, chai_1.expect)(document).to.deep.equal(testDocument); // Redo (0, chai_1.expect)(command.canRedo(document)).to.be.true; command.redo(document); (0, chai_1.expect)(document).to.deep.equal(expectedDocument); }); it('Invalid patch', async () => { const invalidPatch = [ { op: 'remove', path: '/wrong-id', }, ]; const document = (0, cloneDeep_1.default)(testDocument); const expectedDocument = (0, cloneDeep_1.default)(testDocument); const invalidCommand = new patch_command_1.PatchCommand('Test Command', 'document', invalidPatch); // Currently, we don't validate commands, so canExecute is always true await (0, chai_1.expect)(invalidCommand.canExecute(document)).to.eventually.be.true; const executeResult = await invalidCommand.execute(document); (0, chai_1.expect)(document).to.deep.equal(expectedDocument); (0, chai_1.expect)(executeResult).to.not.be.undefined; (0, chai_1.expect)(executeResult).to.be.an('array').empty; }); it('Command lifecycle', async () => { const patch = [ { op: 'replace', path: '/id', value: 'updated-test-id', }, ]; const document = (0, cloneDeep_1.default)(testDocument); const command = new patch_command_1.PatchCommand('Test Command', 'document', patch); await (0, chai_1.expect)(command.canExecute(document)).to.eventually.be.true; (0, chai_1.expect)(command.canUndo(document)).to.be.false; (0, chai_1.expect)(command.canRedo(document)).to.be.false; await command.execute(document); await (0, chai_1.expect)(command.canExecute(document)).to.eventually.be.false; (0, chai_1.expect)(command.canUndo(document)).to.be.true; (0, chai_1.expect)(command.canRedo(document)).to.be.false; command.undo(document); await (0, chai_1.expect)(command.canExecute(document)).to.eventually.be.false; (0, chai_1.expect)(command.canUndo(document)).to.be.false; (0, chai_1.expect)(command.canRedo(document)).to.be.true; command.redo(document); await (0, chai_1.expect)(command.canExecute(document)).to.eventually.be.false; (0, chai_1.expect)(command.canUndo(document)).to.be.true; (0, chai_1.expect)(command.canRedo(document)).to.be.false; }); it('Command lifecycle (Invalid)', async () => { const patch = [ { op: 'replace', path: '/id', value: 'updated-test-id', }, ]; const document = (0, cloneDeep_1.default)(testDocument); const command = new patch_command_1.PatchCommand('Test Command', 'document', patch); // Undo command before it is executed: throws an error let success = false; try { command.undo(document); success = true; } catch (error) { // Okay } if (success) { (0, assert_1.fail)('Command should not be undoable'); } // Redo command before it is executed: throws an error success = false; try { command.redo(document); success = true; } catch (error) { // Okay } if (success) { (0, assert_1.fail)('Command should not be redoable'); } await command.execute(document); // Execute command twice: throws an error success = false; try { await command.execute(document); success = true; } catch (error) { // Okay } if (success) { (0, assert_1.fail)('Command should not be executable'); } // Redo command before it is undone: throws an error success = false; try { command.redo(document); success = true; } catch (error) { // Okay } if (success) { (0, assert_1.fail)('Command should not be redoable'); } command.undo(document); // Execute command twice: throws an error (even after undo) success = false; try { await command.execute(document); success = true; } catch (error) { // Okay } if (success) { (0, assert_1.fail)('Command should not be executable'); } // Undo command twice: throws an error success = false; try { command.undo(document); success = true; } catch (error) { // Okay } if (success) { (0, assert_1.fail)('Command should not be undoable'); } }); describe('MultiPatchCommand', () => { it('1 Sub command', async () => { const patch = [ { op: 'replace', path: '/id', value: 'updated-test-id', }, ]; const document = (0, cloneDeep_1.default)(testDocument); const expectedDocument = (0, cloneDeep_1.default)(testDocument); expectedDocument.id = 'updated-test-id'; const command = new patch_command_1.MultiPatchCommand('Test Multi Command', { modelId: 'test', patch, }); const getModel = (modelId) => modelId == 'test' ? document : undefined; // Execute await (0, chai_1.expect)(command.canExecute(getModel)).to.eventually.be.true; const executeResult = await command.execute(getModel); (0, chai_1.expect)(document).to.deep.equal(expectedDocument); (0, chai_1.expect)(executeResult).to.not.be.undefined; if (executeResult) { const modelToPatch = (0, core_1.groupByModelId)(executeResult); (0, chai_1.expect)(modelToPatch.size).to.equal(1); const resultPatch = modelToPatch.get('test'); (0, chai_1.expect)(resultPatch).to.not.be.undefined; if (resultPatch !== undefined) { const operations = resultPatch.filter((operation) => operation.op !== 'test'); // Should contain a single 'replace' operation (and maybe some optional 'test' operations) (0, chai_1.expect)(operations).to.be.an('array').of.length(1); (0, chai_1.expect)(operations[0].op).to.equal('replace'); } } // Undo await (0, chai_1.expect)(command.canUndo(getModel)).to.eventually.be.true; await command.undo(getModel); (0, chai_1.expect)(document).to.deep.equal(testDocument); // Redo await (0, chai_1.expect)(command.canRedo(getModel)).to.eventually.be.true; await command.redo(getModel); (0, chai_1.expect)(document).to.deep.equal(expectedDocument); }); it('2 Sub commands', async () => { const patch1 = [ { op: 'replace', path: '/id', value: 'updated-test-id', }, ]; const patch2 = [ { op: 'add', path: '/children/-', value: { type: 'child-node', value: 890, valid: false, }, }, ]; const document1 = (0, cloneDeep_1.default)(testDocument); const document2 = (0, cloneDeep_1.default)(secondTestDocument); const expectedDocument1 = (0, cloneDeep_1.default)(testDocument); expectedDocument1.id = 'updated-test-id'; const expectedDocument2 = (0, cloneDeep_1.default)(secondTestDocument); expectedDocument2.children.push({ type: 'child-node', value: 890, valid: false, }); const command = new patch_command_1.MultiPatchCommand('Test Multi Command', { modelId: 'document1', patch: patch1, }, { modelId: 'document2', patch: patch2, }); const getModel = (modelId) => { switch (modelId) { case 'document1': return document1; case 'document2': return document2; default: return undefined; } }; // Execute // PatchCommand is synchronous, so MultiPatchCommand is, too await (0, chai_1.expect)(command.canExecute(getModel)).to.eventually.be.true; const executeResult = await command.execute(getModel); (0, chai_1.expect)(document1).to.deep.equal(expectedDocument1); (0, chai_1.expect)(document2).to.deep.equal(expectedDocument2); (0, chai_1.expect)(executeResult).to.not.be.undefined; if (executeResult) { const modelToPatch = (0, core_1.groupByModelId)(executeResult); (0, chai_1.expect)(modelToPatch.size).to.equal(2); const resultPatch1 = modelToPatch.get('document1'); (0, chai_1.expect)(resultPatch1).to.not.be.undefined; if (resultPatch1 !== undefined) { const operations = resultPatch1.filter((operation) => operation.op !== 'test'); // Should contain a single 'replace' operation (and maybe some optional 'test' operations) (0, chai_1.expect)(operations).to.be.an('array').of.length(1); (0, chai_1.expect)(operations[0].op).to.equal('replace'); } const resultPatch2 = modelToPatch.get('document2'); (0, chai_1.expect)(resultPatch2).to.not.be.undefined; if (resultPatch2 !== undefined) { const operations = resultPatch2.filter((operation) => operation.op !== 'test'); // Should contain a single 'replace' operation (and maybe some optional 'test' operations) (0, chai_1.expect)(operations).to.be.an('array').of.length(1); (0, chai_1.expect)(operations[0].op).to.equal('add'); } } // Undo await (0, chai_1.expect)(command.canUndo(getModel)).to.eventually.be.true; await command.undo(getModel); (0, chai_1.expect)(document1).to.deep.equal(testDocument); (0, chai_1.expect)(document2).to.deep.equal(secondTestDocument); // Redo await (0, chai_1.expect)(command.canRedo(getModel)).to.eventually.be.true; await command.redo(getModel); (0, chai_1.expect)(document1).to.deep.equal(expectedDocument1); (0, chai_1.expect)(document2).to.deep.equal(expectedDocument2); }); it('2 Sub commands - 1 model', async () => { const patch1 = [ { op: 'replace', path: '/id', value: 'updated-test-id', }, ]; const patch2 = [ { op: 'add', path: '/children/-', value: { type: 'child-node', value: 90, valid: true, }, }, ]; const document = (0, cloneDeep_1.default)(testDocument); const expectedDocument = (0, cloneDeep_1.default)(testDocument); expectedDocument.id = 'updated-test-id'; expectedDocument.children.push({ type: 'child-node', value: 90, valid: true, }); const command = new patch_command_1.MultiPatchCommand('Test Multi Command', { modelId: 'document', patch: patch1, }, { modelId: 'document', patch: patch2, }); const getModel = (modelId) => modelId === 'document' ? document : undefined; // Execute await (0, chai_1.expect)(command.canExecute(getModel)).to.eventually.be.true; const executeResult = await command.execute(getModel); (0, chai_1.expect)(document).to.deep.equal(expectedDocument); (0, chai_1.expect)(executeResult).to.not.be.undefined; if (executeResult) { const modelToPatch = (0, core_1.groupByModelId)(executeResult); (0, chai_1.expect)(modelToPatch.size).to.equal(1); const resultPatch = modelToPatch.get('document'); (0, chai_1.expect)(resultPatch).to.not.be.undefined; if (resultPatch !== undefined) { const operations = resultPatch.filter((operation) => operation.op !== 'test'); // Should contain one 'replace' operation and one 'add' operation // (and maybe some optional 'test' operations) (0, chai_1.expect)(operations).to.be.an('array').of.length(2); (0, chai_1.expect)(operations[0].op).to.equal('replace'); (0, chai_1.expect)(operations[1].op).to.equal('add'); } } // Undo await (0, chai_1.expect)(command.canUndo(getModel)).to.eventually.be.true; await command.undo(getModel); (0, chai_1.expect)(document).to.deep.equal(testDocument); // Redo await (0, chai_1.expect)(command.canRedo(getModel)).to.eventually.be.true; await command.redo(getModel); (0, chai_1.expect)(document).to.deep.equal(expectedDocument); }); }); describe('createModelUpdaterCommand', () => { it('Simple execute/undo/redo', async () => { const updater = (workingCopy, modelId) => { (0, chai_1.expect)(modelId).to.be.equal('document'); workingCopy.id = 'updated-test-id'; }; const document = (0, cloneDeep_1.default)(testDocument); const expectedDocument = (0, cloneDeep_1.default)(testDocument); expectedDocument.id = 'updated-test-id'; const command = (0, patch_command_1.createModelUpdaterCommand)('Test Command', 'document', updater); // Execute await (0, chai_1.expect)(command.canExecute(document)).to.eventually.be.true; const executeResult = await command.execute(document); (0, chai_1.expect)(document).to.deep.equal(expectedDocument); (0, chai_1.expect)(executeResult).to.not.be.undefined; if (executeResult) { // Should contain a single 'replace' operation (and maybe some optional 'test' operations) const operations = executeResult.filter((operation) => operation.op !== 'test'); (0, chai_1.expect)(operations).to.be.an('array').of.length(1); (0, chai_1.expect)(operations[0].op).to.equal('replace'); } // Undo (0, chai_1.expect)(command.canUndo(document)).to.be.true; command.undo(document); (0, chai_1.expect)(document).to.deep.equal(testDocument); // Redo (0, chai_1.expect)(command.canRedo(document)).to.be.true; command.redo(document); (0, chai_1.expect)(document).to.deep.equal(expectedDocument); }); it('Execute/undo/redo when modifying attribute to undefined', async () => { const updater = (workingCopy, modelId) => { (0, chai_1.expect)(modelId).to.be.equal('document'); workingCopy.label = undefined; if (workingCopy.children[0]) { workingCopy.children[0].value = undefined; } }; const document = (0, cloneDeep_1.default)(testDocument); const expectedDocument = (() => { const expectedDocument = (0, cloneDeep_1.default)(testDocument); expectedDocument.label = undefined; if (expectedDocument.children[0]) { expectedDocument.children[0].value = undefined; } return JSON.parse(JSON.stringify(expectedDocument)); })(); const command = (0, patch_command_1.createModelUpdaterCommand)('Test Command setting undefined attributes', 'document', updater); // Execute await (0, chai_1.expect)(command.canExecute(document)).to.eventually.be.true; const executeResult = await command.execute(document); (0, chai_1.expect)(document).to.deep.equal(expectedDocument); (0, chai_1.expect)(executeResult).to.not.be.undefined; if (executeResult) { // Should contain two 'remove' operations (and maybe some optional 'test' operations) const operations = executeResult.filter((operation) => operation.op !== 'test'); (0, chai_1.expect)(operations).to.be.an('array').of.length(2); (0, chai_1.expect)(operations[0].op).to.equal('remove'); (0, chai_1.expect)(operations[1].op).to.equal('remove'); } // Undo (0, chai_1.expect)(command.canUndo(document)).to.be.true; command.undo(document); (0, chai_1.expect)(document).to.deep.equal(testDocument); // Redo (0, chai_1.expect)(command.canRedo(document)).to.be.true; command.redo(document); (0, chai_1.expect)(document).to.deep.equal(expectedDocument); }); it('Execute/undo/redo when adding undefined value in array', async () => { const updater = (workingCopy, modelId) => { (0, chai_1.expect)(modelId).to.be.equal('document'); workingCopy.children.push(undefined); }; const document = (0, cloneDeep_1.default)(testDocument); const expectedDocument = (() => { const expectedDocument = (0, cloneDeep_1.default)(testDocument); expectedDocument.children.push(undefined); return JSON.parse(JSON.stringify(expectedDocument)); })(); const command = (0, patch_command_1.createModelUpdaterCommand)('Test Command adding an undefined value in array', 'document', updater); // Execute await (0, chai_1.expect)(command.canExecute(document)).to.eventually.be.true; const executeResult = await command.execute(document); (0, chai_1.expect)(document).to.deep.equal(expectedDocument); (0, chai_1.expect)(executeResult).to.not.be.undefined; if (executeResult) { // Should contain one 'add' operations (and maybe some optional 'test' operations) const operations = executeResult.filter((operation) => operation.op !== 'test'); (0, chai_1.expect)(operations).to.be.an('array').of.length(1); (0, chai_1.expect)(operations[0].op).to.equal('add'); } // Undo (0, chai_1.expect)(command.canUndo(document)).to.be.true; command.undo(document); (0, chai_1.expect)(document).to.deep.equal(testDocument); // Redo (0, chai_1.expect)(command.canRedo(document)).to.be.true; command.redo(document); (0, chai_1.expect)(document).to.deep.equal(expectedDocument); }); it('Execute/undo/redo when modifying model to be recursive', async () => { const updater = (workingCopy, modelId) => { (0, chai_1.expect)(modelId).to.be.equal('document'); workingCopy.children.push(workingCopy); }; const document = (0, cloneDeep_1.default)(testDocument); const command = (0, patch_command_1.createModelUpdaterCommand)('Test Command adding an undefined value in array', 'document', updater); await (0, chai_1.expect)(command.canExecute(document)).to.eventually.be.true; try { await command.execute(document); } catch (e) { // success return; } (0, assert_1.fail)('execution should not be possible as the model is recursive'); }); it('Execute/undo/redo when modifying model to contain doubly linked references', async () => { const updater = (workingCopy, modelId) => { (0, chai_1.expect)(modelId).to.be.equal('document'); if (workingCopy.children[0] && workingCopy.children[1]) { workingCopy.children[0].link = workingCopy.children[1]; workingCopy.children[1].link = workingCopy.children[0]; } else { (0, assert_1.fail)('Unexpected test document'); } }; const document = (0, cloneDeep_1.default)(testDocument); const command = (0, patch_command_1.createModelUpdaterCommand)('Test Command adding an undefined value in array', 'document', updater); await (0, chai_1.expect)(command.canExecute(document)).to.eventually.be.true; try { await command.execute(document); } catch (e) { // success return; } (0, assert_1.fail)('execution should not be possible as the model is doubly linked'); }); it('Execute/undo/redo empty change', async () => { const updater = (workingCopy, modelId) => { (0, chai_1.expect)(modelId).to.be.equal('document'); (0, chai_1.expect)(workingCopy).to.be.equal(document); // Don't make any change. That's the point }; const document = (0, cloneDeep_1.default)(testDocument); const expectedDocument = (0, cloneDeep_1.default)(testDocument); const command = (0, patch_command_1.createModelUpdaterCommand)('Empty Command', 'document', updater); // Execute await (0, chai_1.expect)(command.canExecute(document)).to.eventually.be.true; const executeResult = await command.execute(document); (0, chai_1.expect)(document).to.deep.equal(expectedDocument); (0, chai_1.expect)(executeResult).to.not.be.undefined; if (executeResult) { // Should contain a single 'replace' operation (and maybe some optional 'test' operations) const operations = executeResult.filter((operation) => operation.op !== 'test'); (0, chai_1.expect)(operations).to.be.an('array').of.length(0); } // Undo (0, chai_1.expect)(command.canUndo(document)).to.be.true; command.undo(document); (0, chai_1.expect)(document).to.deep.equal(testDocument); // Redo (0, chai_1.expect)(command.canRedo(document)).to.be.true; command.redo(document); (0, chai_1.expect)(document).to.deep.equal(expectedDocument); }); describe('Options', () => { it('Non-executable', async () => { const updater = (model, modelId) => { (0, chai_1.expect)(model).to.be.equal(document); (0, chai_1.expect)(modelId).to.be.equal('document'); }; const canExecute = (model, modelId) => { (0, chai_1.expect)(model).to.be.equal(document); (0, chai_1.expect)(modelId).to.be.equal('document'); return Promise.resolve('test reason'); }; const document = (0, cloneDeep_1.default)(testDocument); const command = (0, patch_command_1.createModelUpdaterCommand)('Test Command', 'document', updater, { canExecute }); await (0, chai_1.expect)(command.canExecute(document)).to.eventually.be.false; await (0, chai_1.expect)(command.execute(document)).to.eventually.be.rejectedWith(/PatchCommand.*cannot be executed: test reason/); }); it('Strict undo predicate', async () => { const document = (0, cloneDeep_1.default)(testDocument); const updater = (workingCopy) => { workingCopy.id = 'updated-id'; }; const command = (0, patch_command_1.createModelUpdaterCommand)('Undo Preconditions', 'document', updater // Default mode is 'strict', so test that, too ); await command.execute(document); // Simulate an interfering change as from a trigger document.id = 'broken'; let thrown; try { await command.undo(document); } catch (error) { thrown = error; } (0, chai_1.expect)(thrown) .to.be.an('Error') .with.property('message') .contains('Test operation failed'); }); it('Lax undo predicate', async () => { const debug = sandbox.stub(console, 'debug'); const document = (0, cloneDeep_1.default)(testDocument); const updater = (workingCopy) => { workingCopy.id = 'updated-id'; }; const command = (0, patch_command_1.createModelUpdaterCommand)('Undo Preconditions', 'document', updater, { preconditionsMode: 'lax' }); await command.execute(document); // Simulate an interfering change as from a trigger document.id = 'broken'; await command.undo(document); (0, chai_1.expect)(debug).to.have.been.calledWithMatch('model-manager/patch-command', 'Inapplicable undo/redo patch.', Error); }); }); describe('With Result', () => { it('success', async () => { const updaterWithResult = (workingCopy, modelId) => { (0, chai_1.expect)(modelId).to.be.equal('document'); workingCopy.id = 'updated-test-id'; return { message: 'Hello, world.' }; }; const document = (0, cloneDeep_1.default)(testDocument); const expectedDocument = (0, cloneDeep_1.default)(testDocument); expectedDocument.id = 'updated-test-id'; const command = (0, patch_command_1.createModelUpdaterCommandWithResult)('Test Command', 'document', updaterWithResult); (0, chai_1.expect)(command.result).to.be.like({ status: 'pending' }); // Execute await (0, chai_1.expect)(command.canExecute(document)).to.eventually.be.true; await command.execute(document); (0, chai_1.expect)(command.result).to.exist; (0, chai_1.expect)(command.result).to.be.like({ status: 'ready', value: { message: 'Hello, world.' }, }); }); it('fails with Error', async () => { const updaterWithResult = () => { throw new Error('💣'); }; const document = (0, cloneDeep_1.default)(testDocument); const expectedDocument = (0, cloneDeep_1.default)(testDocument); expectedDocument.id = 'updated-test-id'; const command = (0, patch_command_1.createModelUpdaterCommandWithResult)('Test Command', 'document', updaterWithResult); let error; await (0, chai_1.expect)(command.canExecute(document)).to.eventually.be.true; try { await command.execute(document); } catch (e) { error = e; } (0, chai_1.expect)(command.result).to.exist; (0, chai_1.expect)(command.result).to.be.like({ status: 'failed', error, }); }); it('fails with string', async () => { const updaterWithResult = () => { throw '💣'; }; const document = (0, cloneDeep_1.default)(testDocument); const expectedDocument = (0, cloneDeep_1.default)(testDocument); expectedDocument.id = 'updated-test-id'; const command = (0, patch_command_1.createModelUpdaterCommandWithResult)('Test Command', 'document', updaterWithResult); let error; await (0, chai_1.expect)(command.canExecute(document)).to.eventually.be.true; try { await command.execute(document); } catch (e) { error = e; } (0, chai_1.expect)(command.result).to.exist; (0, chai_1.expect)(command.result).to.be.like({ status: 'failed', error, }); }); }); }); describe('createModelPatchCommand', () => { it('Simple execute/undo/redo', async () => { const patchFunction = (workingCopy, modelId) => { (0, chai_1.expect)(modelId).to.be.equal('document'); (0, chai_1.expect)(workingCopy).to.be.equal(document); return [ { op: 'replace', path: '/id', value: 'updated-test-id', }, ]; }; const document = (0, cloneDeep_1.default)(testDocument); const expectedDocument = (0, cloneDeep_1.default)(testDocument); expectedDocument.id = 'updated-test-id'; const command = (0, patch_command_1.createModelPatchCommand)('Test Command', 'document', patchFunction); // Execute await (0, chai_1.expect)(command.canExecute(document)).to.eventually.be.true; const executeResult = await command.execute(document); (0, chai_1.expect)(document).to.deep.equal(expectedDocument); (0, chai_1.expect)(executeResult).to.not.be.undefined; if (executeResult) { // Should contain a single 'replace' operation (and maybe some optional 'test' operations) const operations = executeResult.filter((operation) => operation.op !== 'test'); (0, chai_1.expect)(operations).to.be.an('array').of.length(1); (0, chai_1.expect)(operations[0].op).to.equal('replace'); } // Undo (0, chai_1.expect)(command.canUndo(document)).to.be.true; command.undo(document); (0, chai_1.expect)(document).to.deep.equal(testDocument); // Redo (0, chai_1.expect)(command.canRedo(document)).to.be.true; command.redo(document); (0, chai_1.expect)(document).to.deep.equal(expectedDocument); }); }); }); const testDocument = { id: 'test-id', label: 'Test Document', children: [ { type: 'child-node', value: 13, valid: true, }, { type: 'child-node', value: 175, valid: false, }, { type: 'child-node', value: 1, valid: true, }, ], }; const secondTestDocument = { id: 'second-test-id', label: 'Second Test Document', children: [ { type: 'child-node', value: 130, valid: true, }, { type: 'child-node', value: 775, valid: false, }, ], }; //# sourceMappingURL=patch-command.spec.js.map