UNPKG

@eclipse-emfcloud/model-service

Version:
193 lines 8.65 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 chai_1 = __importStar(require("chai")); const chai_like_1 = __importDefault(require("chai-like")); const chai_things_1 = __importDefault(require("chai-things")); const model_trigger_engine_1 = require("../model-trigger-engine"); const trigger_engine_1 = require("@eclipse-emfcloud/trigger-engine"); 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")); chai_1.default.use(chai_like_1.default); chai_1.default.use(chai_things_1.default); chai_1.default.use(sinon_chai_1.default); const originalModelImage = { name: 'The Model', tasks: [{ label: 'a', priority: 'medium' }], mediumPrioTasks: 1, }; const MODEL_ID = 'example://38BCC304-652F-4C27-A225-81BF6C8EDCA8/model'; /** A trigger that initializes the priority of a newly added task. */ const priorityOfNewTask = { getPatch: async (modelId, model, delta) => { const op = (0, trigger_engine_1.addOperations)(delta)[0]; if (modelId !== MODEL_ID || !op) { return undefined; } const match = /\/tasks\/(-|\d+)/.exec(op.path); if (!match) { return undefined; } const taskIndex = match[1] === '-' ? -1 : Number(match[1]); return setPriority(model, taskIndex, 'medium'); }, }; /** A trigger recounts the medium priority tasks when a task's priority is changed. */ const recountMediumPrioTasks = { getPatch: (modelId, model, delta) => { const op = (0, trigger_engine_1.addOrReplaceOperations)(delta)[0]; if (op && modelId == MODEL_ID && /\/tasks\/\d+\/priority/.test(op.path)) { return updateMediumPrioTasks(model); } return undefined; }, }; describe('ModelTriggerEngine', () => { let engine; let sandbox; beforeEach(() => { sandbox = sinon_1.default.createSandbox(); engine = new model_trigger_engine_1.ModelTriggerEngine(); }); afterEach(() => { sandbox.restore(); }); describe('applyModelTriggers', () => { it('no triggers present', async () => { const patch = await runTriggers(engine, addTask('b')); (0, chai_1.expect)(patch).not.to.exist; }); it('trigger present', async () => { engine.addModelTrigger(priorityOfNewTask); const patch = await runTriggers(engine, addTask('b')); (0, chai_1.expect)(patch).to.contain.something.like({ op: 'add', path: '/tasks/1/priority', value: 'medium', }); }); describe('cascading triggers present', () => { const permutations = [ [priorityOfNewTask, recountMediumPrioTasks], [recountMediumPrioTasks, priorityOfNewTask], ]; permutations.forEach((permutation, index) => { it(`permutation ${index}`, async () => { permutation.forEach((trigger) => engine.addModelTrigger(trigger)); const patch = await runTriggers(engine, addTask('b')); (0, chai_1.expect)(patch).to.contain.something.like({ op: 'replace', path: '/mediumPrioTasks', value: 2, }); // Of course, it also still sets the priority of the new task, also (0, chai_1.expect)(patch).to.contain.something.like({ op: 'add', path: '/tasks/1/priority', value: 'medium', }); }); }); it('correct interim model states provided', async () => { const spyOnPriorityOfNewTask = sandbox.spy(priorityOfNewTask, 'getPatch'); engine.addModelTrigger(priorityOfNewTask); const initialModelState = (0, lodash_1.cloneDeep)(originalModelImage); const secondModelState = (0, lodash_1.cloneDeep)(initialModelState); secondModelState.tasks.push({ label: 'b', }); const finalModelState = (0, lodash_1.cloneDeep)(secondModelState); finalModelState.tasks[1].priority = 'medium'; await runTriggers(engine, addTask('b')); (0, chai_1.expect)(spyOnPriorityOfNewTask).to.have.been.calledTwice; (0, chai_1.expect)(spyOnPriorityOfNewTask).to.have.been.calledWithMatch(MODEL_ID, sinon_1.default.match.any, sinon_1.default.match.any, initialModelState); (0, chai_1.expect)(spyOnPriorityOfNewTask).to.have.been.calledWithMatch(MODEL_ID, finalModelState, sinon_1.default.match.any, secondModelState); }); }); describe('edge cases', () => { it('apply empty patch', async () => { engine.addModelTrigger(priorityOfNewTask); const patch = await runTriggers(engine, []); (0, chai_1.expect)(patch).not.to.exist; }); }); }); }); async function runTriggers(engine, patch) { return engine.applyModelTriggers(MODEL_ID, ...modifyModel(patch)); } function modifyModel(patch) { const previousModel = (0, lodash_1.cloneDeep)(originalModelImage); // This creates a copy, not modifying the `previousDocument` const model = (0, fast_json_patch_1.applyPatch)(previousModel, patch, false, false).newDocument; // Calculate a diff to ensure that we don't have '/-' segments for array appends const delta = (0, fast_json_patch_1.compare)(previousModel, model, true); return [model, delta, previousModel]; } function addTask(label) { return [{ op: 'add', path: '/tasks/-', value: { label } }]; } function setPriority(model, taskOffset, priority) { const taskIndex = taskOffset >= 0 ? taskOffset : model.tasks.length + taskOffset; const task = model.tasks[taskIndex]; if (task.priority !== priority) { return [ { op: task.priority ? 'replace' : 'add', path: `/tasks/${taskIndex}/priority`, value: priority, }, ]; } return undefined; } function updateMediumPrioTasks(model) { const oldValue = model.mediumPrioTasks; const newValue = model.tasks.reduce((count, task) => (task.priority !== 'medium' ? count : count + 1), 0); if (newValue != oldValue) { return [{ op: 'replace', path: '/mediumPrioTasks', value: newValue }]; } return undefined; } //# sourceMappingURL=model-trigger-engine.spec.js.map