@eclipse-emfcloud/model-service
Version:
Model service framework
193 lines • 8.65 kB
JavaScript
;
// *****************************************************************************
// 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