UNPKG

@eclipse-emfcloud/model-service-theia

Version:
327 lines 15.6 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 model_manager_1 = require("@eclipse-emfcloud/model-manager"); const model_service_1 = require("@eclipse-emfcloud/model-service"); const model_validation_1 = require("@eclipse-emfcloud/model-validation"); const inversify_1 = require("@theia/core/shared/inversify"); const chai_1 = __importStar(require("chai")); const chai_like_1 = __importDefault(require("chai-like")); const sinon_1 = __importDefault(require("sinon")); const sinon_chai_1 = __importDefault(require("sinon-chai")); const common_1 = require("../../common"); const backend_module_1 = __importDefault(require("../backend-module")); const model_hub_provider_1 = require("../model-hub-provider"); const model_service_contribution_1 = require("../model-service-contribution"); const fake_json_rpc_1 = require("./fake-json-rpc"); chai_1.default.use(chai_like_1.default); chai_1.default.use(sinon_chai_1.default); const MODEL1_ID = 'test.model1'; function createTestContainer(...contributions) { const container = new inversify_1.Container(); container.load(backend_module_1.default); contributions.forEach((contrib) => container.bind(model_service_contribution_1.ModelServiceContribution).toConstantValue(contrib)); (0, fake_json_rpc_1.bindFakeRpcConnectionFactory)(container); return container; } describe('ModelHubServer', () => { const appContext = 'test-app'; let sandbox; let contrib1; let modelHubServer; let clientProxy; let modelHub; beforeEach(async () => { sandbox = sinon_1.default.createSandbox(); contrib1 = new Contribution1(); const container = createTestContainer(contrib1); const factory = container.get(fake_json_rpc_1.RpcConnectionFactory); clientProxy = { onModelChanged: sandbox.stub(), onModelDirtyState: sandbox.stub(), onModelValidated: sandbox.stub(), onModelLoaded: sandbox.stub(), onModelUnloaded: sandbox.stub(), onModelHubDisposed: sandbox.stub(), closeSubscription: sandbox.stub(), onModelHubCreated: sandbox.stub(), onModelHubDestroyed: sandbox.stub(), }; modelHubServer = await factory.getServer(common_1.ModelHubProtocolServicePath, clientProxy); // Some tests need to start interacting with the model service // right away, so initialize the context's provided hub modelHub = await container.get(model_hub_provider_1.ModelHubProvider)(appContext); }); afterEach(() => { sandbox.restore(); }); it('provides a model', async () => { const model = await modelHubServer.getModel(appContext, MODEL1_ID); (0, chai_1.expect)(model).to.be.like({ name: 'Model 1' }); }); it('validates a model', async () => { const model = await modelHubServer.getModel(appContext, MODEL1_ID); model.name = 'NoSpace'; let diagnostic; diagnostic = await modelHubServer.validateModels(appContext, MODEL1_ID); (0, chai_1.expect)(diagnostic).to.be.like({ severity: 'error', path: '/name' }); model.name = 'Has Space'; // Haven't revalidated, yet diagnostic = await modelHubServer.getValidationState(appContext, MODEL1_ID); (0, chai_1.expect)(diagnostic).to.be.like({ severity: 'error', path: '/name' }); diagnostic = await modelHubServer.validateModels(appContext, MODEL1_ID); (0, chai_1.expect)(diagnostic).to.be.eql((0, model_validation_1.ok)()); }); it('can undo and redo', async () => { const service = contrib1.getModelService(); (0, chai_1.expect)(service).to.exist; await service?.setName('New Name'); let model = await modelHubServer.getModel(appContext, MODEL1_ID); (0, chai_1.expect)(model).to.be.like({ name: 'New Name' }); const undone = await modelHubServer.undo(appContext, MODEL1_ID); (0, chai_1.expect)(undone).to.be.true; model = await modelHubServer.getModel(appContext, MODEL1_ID); (0, chai_1.expect)(model).to.be.like({ name: 'Model 1' }); const redone = await modelHubServer.redo(appContext, MODEL1_ID); (0, chai_1.expect)(redone).to.be.true; model = await modelHubServer.getModel(appContext, MODEL1_ID); (0, chai_1.expect)(model).to.be.like({ name: 'New Name' }); }); it('manages dirty state', async () => { let dirty = await modelHubServer.isDirty(appContext, MODEL1_ID); (0, chai_1.expect)(dirty).to.be.false; const service = contrib1.getModelService(); (0, chai_1.expect)(service).to.exist; await service?.setName('New Name'); dirty = await modelHubServer.isDirty(appContext, MODEL1_ID); (0, chai_1.expect)(dirty).to.be.true; await modelHubServer.undo(appContext, MODEL1_ID); dirty = await modelHubServer.isDirty(appContext, MODEL1_ID); (0, chai_1.expect)(dirty).to.be.false; await modelHubServer.redo(appContext, MODEL1_ID); dirty = await modelHubServer.isDirty(appContext, MODEL1_ID); (0, chai_1.expect)(dirty).to.be.true; const saved = await modelHubServer.save(appContext, MODEL1_ID); (0, chai_1.expect)(saved).to.be.true; dirty = await modelHubServer.isDirty(appContext, MODEL1_ID); (0, chai_1.expect)(dirty).to.be.false; await modelHubServer.undo(appContext, MODEL1_ID); dirty = await modelHubServer.isDirty(appContext, MODEL1_ID); (0, chai_1.expect)(dirty).to.be.true; const flushed = await modelHubServer.flush(appContext, MODEL1_ID); (0, chai_1.expect)(flushed).to.be.true; dirty = await modelHubServer.isDirty(appContext, MODEL1_ID); (0, chai_1.expect)(dirty).to.be.true; // Cannot redo after flush const redone = await modelHubServer.redo(appContext, MODEL1_ID); (0, chai_1.expect)(redone).to.be.false; }); describe('manages subscriptions', async () => { let token; beforeEach(async () => { token = await modelHubServer.subscribe(appContext, MODEL1_ID); const service = contrib1.getModelService(); (0, chai_1.expect)(service).to.exist; await service?.setName('NewName'); }); it('creates the subscription', () => { (0, chai_1.expect)(token).to.exist; (0, chai_1.expect)(token.id).to.be.greaterThan(0); (0, chai_1.expect)(token.modelIds).to.be.eql([MODEL1_ID]); }); it('notifies model changed', () => { sinon_1.default.assert.calledOnceWithMatch(clientProxy.onModelChanged, token.id, MODEL1_ID, [ { op: 'test', path: '/name', value: 'Model 1' }, { op: 'replace', path: '/name', value: 'NewName' }, ]); }); it('notifies model validation', async () => { await modelHubServer.validateModels(appContext, MODEL1_ID); // It was already called once with an OK result on creation of the model sinon_1.default.assert.calledWithMatch(clientProxy.onModelValidated, token.id, MODEL1_ID, sinon_1.default.match({ severity: 'error', path: '/name', })); }); it('notifies dirty state', async () => { sinon_1.default.assert.calledWithMatch(clientProxy.onModelDirtyState, token.id, MODEL1_ID, true); await modelHubServer.save(appContext, MODEL1_ID); sinon_1.default.assert.calledWithMatch(clientProxy.onModelDirtyState, token.id, MODEL1_ID, false); }); it('notifies model loaded', async () => { sinon_1.default.assert.calledWithMatch(clientProxy.onModelLoaded, token.id, MODEL1_ID); }); it('notifies model unloaded', async () => { const modelManager = modelHub.modelManager; modelManager.removeModel(MODEL1_ID); sinon_1.default.assert.calledWithMatch(clientProxy.onModelUnloaded, token.id, MODEL1_ID); }); it('notifies hub disposal', () => { modelHub.dispose(); sinon_1.default.assert.calledWithMatch(clientProxy.onModelHubDisposed, token.id); }); it('can be closed', async () => { await modelHubServer.closeSubscription(token); sinon_1.default.assert.calledWithExactly(clientProxy.closeSubscription, token.id); // Second attempt is harmless and without effect await modelHubServer.closeSubscription(token); (0, chai_1.expect)(clientProxy.closeSubscription.callCount).to.be.equal(1); }); }); describe('disposal scenarios', () => { // The main state to clean up is the subscriptions let token; beforeEach(async () => { token = await modelHubServer.subscribe(appContext, MODEL1_ID); }); it('dispose the server', () => { modelHubServer.dispose(); sinon_1.default.assert.calledWithExactly(clientProxy.closeSubscription, token.id); }); it('delete the client', async () => { modelHubServer.setClient(undefined); (0, chai_1.expect)(modelHubServer.getClient()).not.to.exist; sinon_1.default.assert.calledWithExactly(clientProxy.closeSubscription, token.id); // After this point, subscriptions don't do anything const newSub = await modelHubServer.subscribe(appContext, MODEL1_ID); const service = contrib1.getModelService(); (0, chai_1.expect)(service).to.exist; await service?.setName('New Name'); sinon_1.default.assert.neverCalledWithMatch(clientProxy.onModelChanged, newSub.id, sinon_1.default.match.any, sinon_1.default.match.any); }); it('setting the same client has no effect', () => { modelHubServer.setClient(clientProxy); sinon_1.default.assert.notCalled(clientProxy.closeSubscription); }); }); describe('model hub tracking', () => { it('notifies model hub creation', async () => { await modelHubServer.getModel(appContext, MODEL1_ID); (0, chai_1.expect)(clientProxy.onModelHubCreated).to.have.been.calledWith(appContext); }); it('notifies model hub creation', async () => { await modelHubServer.getModel(appContext, MODEL1_ID); modelHub.dispose(); (0, chai_1.expect)(clientProxy.onModelHubDestroyed).to.have.been.calledWith(appContext); }); }); }); class Contribution1 extends model_service_1.AbstractModelServiceContribution { constructor() { super(); this.modelService = { getModel: () => { let model = this.modelManager.getModel(MODEL1_ID); if (!model) { this.modelManager.setModel(MODEL1_ID, this.model); model = this.modelManager.getModel(MODEL1_ID); } if (!model) { throw new Error('No model to edit'); } return model; }, setName: async (newName) => { this.modelService.getModel(); // Ensure the model is loaded const command = createSetNameCommand(MODEL1_ID, newName); await this.commandStack.execute(command); }, }; this.model = { name: 'Model 1', }; this.initialize({ id: 'test.contrib1', persistenceContribution: { canHandle: (modelId) => Promise.resolve(modelId === MODEL1_ID), loadModel: (modelId) => modelId === MODEL1_ID ? Promise.resolve(this.model) : Promise.reject(new Error(`No such model: ${modelId}`)), saveModel: async (modelId, model) => { if (modelId !== MODEL1_ID) { throw new Error(`Unsupported model ${modelId}`); } const newImage = { ...model }; Array.from(Object.keys(this.model)).forEach((key) => delete this.model[key]); Object.assign(this.model, newImage); return true; }, }, validationContribution: { getValidators: () => [ { validate: async (_modelId, model) => { if (typeof model.name !== 'string') { return errorDiagnostic('No name.', '/name'); } if (!model.name.includes(' ')) { return errorDiagnostic('No space in the name.', '/name'); } return (0, model_validation_1.ok)(); }, }, ], }, }); } getModelService() { return this.modelService; } setModelManager(modelManager) { super.setModelManager(modelManager); this.commandStack = modelManager.getCommandStack(MODEL1_ID); } } function errorDiagnostic(message, path) { return { message, path, severity: 'error', source: 'test', }; } function createSetNameCommand(modelId, newName) { return (0, model_manager_1.createModelUpdaterCommand)('Set Name', modelId, (model) => { model.name = newName; }); } //# sourceMappingURL=model-hub-server.spec.js.map