@eclipse-glsp/client
Version:
A sprotty-based client for GLSP
361 lines (353 loc) • 19.9 kB
text/typescript
/********************************************************************************
* Copyright (c) 2023-2024 EclipseSource and others.
*
* 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: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
import {
AnimationFrameSyncer,
CommandExecutionContext,
ConsoleLogger,
EdgeTypeHint,
GChildElement,
GModelFactory,
GModelRoot,
GNode,
SetTypeHintsAction,
ShapeTypeHint,
TYPES,
Writable,
bindOrRebind,
createFeatureSet,
editFeature,
isDeletable,
isMoveable
} from '@eclipse-glsp/sprotty';
import { expect } from 'chai';
import { Container } from 'inversify';
import * as sinon from 'sinon';
import { GLSPActionDispatcher } from '../../base/action-dispatcher';
import { FeedbackActionDispatcher } from '../../base/feedback/feedback-action-dispatcher';
import { FeedbackEmitter } from '../../base/feedback/feedback-emitter';
import { GEdge } from '../../model';
import { isResizable } from '../change-bounds/model';
import { isReconnectable } from '../reconnect/model';
import { Containable, isContainable, isReparentable } from './model';
import { ApplyTypeHintsAction, ApplyTypeHintsCommand, ITypeHintProvider, TypeHintProvider } from './type-hint-provider';
describe('TypeHintProvider', () => {
const container = new Container();
container.bind(GLSPActionDispatcher).toConstantValue(sinon.createStubInstance(GLSPActionDispatcher));
container.bind(TYPES.IActionDispatcher).toService(GLSPActionDispatcher);
const stub = sinon.createStubInstance(FeedbackActionDispatcher);
stub.createEmitter.returns(new FeedbackEmitter(stub));
container.bind(TYPES.IFeedbackActionDispatcher).toConstantValue(stub);
const typeHintProvider = container.resolve(TypeHintProvider);
describe('getShapeTypeHint', () => {
const nodeHint: ShapeTypeHint = {
deletable: true,
elementTypeId: 'node',
reparentable: false,
repositionable: true,
resizable: true
};
const taskHint: ShapeTypeHint = {
deletable: true,
elementTypeId: 'node:task',
reparentable: false,
repositionable: true,
resizable: true
};
it('should return `undefined` if no `SetTypeHintsAction` has been handled yet', () => {
expect(typeHintProvider.getShapeTypeHint('some')).to.be.undefined;
});
it('should return `undefined` if no hint is registered for the given type (exact type match)', () => {
typeHintProvider.handle(SetTypeHintsAction.create({ shapeHints: [nodeHint], edgeHints: [] }));
expect(typeHintProvider.getShapeTypeHint('port')).to.be.undefined;
});
it('should return the corresponding type hint for the given type (exact type match)', () => {
typeHintProvider.handle(SetTypeHintsAction.create({ shapeHints: [nodeHint, taskHint], edgeHints: [] }));
expect(typeHintProvider.getShapeTypeHint('node')).to.equal(nodeHint);
expect(typeHintProvider.getShapeTypeHint('node:task')).to.equal(taskHint);
});
it('should return the corresponding type hint for the given type (sub type match)', () => {
typeHintProvider.handle(SetTypeHintsAction.create({ shapeHints: [nodeHint, taskHint], edgeHints: [] }));
expect(typeHintProvider.getShapeTypeHint('node:task:manual')).to.equal(taskHint);
expect(typeHintProvider.getShapeTypeHint('node:task:manual:foo')).to.equal(taskHint);
expect(typeHintProvider.getShapeTypeHint('node:event')).to.equal(nodeHint);
expect(typeHintProvider.getShapeTypeHint('node:event:initial')).to.equal(nodeHint);
});
});
describe('getEdgeTypeHint', () => {
const edgeHint: EdgeTypeHint = {
deletable: true,
elementTypeId: 'edge',
repositionable: true,
routable: true,
dynamic: false
};
const fooEdgeHint: EdgeTypeHint = {
deletable: true,
elementTypeId: 'edge:foo',
repositionable: true,
routable: true,
dynamic: true
};
it('should return `undefined` if no `SetTypeHintsAction` has been handled yet', () => {
expect(typeHintProvider.getEdgeTypeHint('some')).to.be.undefined;
});
it('should return `undefined` if no hint is registered for the given type (exact type match)', () => {
typeHintProvider.handle(SetTypeHintsAction.create({ shapeHints: [], edgeHints: [edgeHint] }));
expect(typeHintProvider.getEdgeTypeHint('link')).to.be.undefined;
});
it('should return the corresponding type hint for the given type (exact type match)', () => {
typeHintProvider.handle(SetTypeHintsAction.create({ shapeHints: [], edgeHints: [edgeHint, fooEdgeHint] }));
expect(typeHintProvider.getEdgeTypeHint('edge')).to.equal(edgeHint);
expect(typeHintProvider.getEdgeTypeHint('edge:foo')).to.equal(fooEdgeHint);
});
it('should return the corresponding type hint for the given type (sub type match)', () => {
typeHintProvider.handle(SetTypeHintsAction.create({ shapeHints: [], edgeHints: [edgeHint, fooEdgeHint] }));
expect(typeHintProvider.getEdgeTypeHint('edge:foo:bar')).to.equal(fooEdgeHint);
expect(typeHintProvider.getEdgeTypeHint('edge:foo:bar:baz')).to.equal(fooEdgeHint);
expect(typeHintProvider.getEdgeTypeHint('edge:some')).to.equal(edgeHint);
expect(typeHintProvider.getEdgeTypeHint('edge:some:other')).to.equal(edgeHint);
});
});
});
describe('ApplyTypeHintCommand', () => {
function createCommandExecutionContext(child: GChildElement): CommandExecutionContext {
const root = new GModelRoot();
root.id = 'root';
root.type = 'root';
root.add(child);
return {
root,
modelFactory,
duration: 0,
modelChanged: undefined!,
logger: new ConsoleLogger(),
syncer: new AnimationFrameSyncer()
};
}
function createNode(type?: string): GNode {
const node = new GNode();
node.type = type ?? 'node';
node.id = 'node';
node.features = createFeatureSet(GNode.DEFAULT_FEATURES);
return node;
}
function createEdge(type?: string): GEdge {
const edge = new GEdge();
edge.type = type ?? 'edge';
edge.id = 'edge';
edge.features = createFeatureSet(GEdge.DEFAULT_FEATURES);
return edge;
}
const sandbox = sinon.createSandbox();
const container = new Container();
const modelFactory = sinon.createStubInstance(GModelFactory);
const typeHintProviderMock = sandbox.stub<ITypeHintProvider>({
getEdgeTypeHint: () => undefined,
getShapeTypeHint: () => undefined
});
container.bind(GLSPActionDispatcher).toConstantValue(sandbox.createStubInstance(GLSPActionDispatcher));
container.bind(TYPES.IActionDispatcher).toService(GLSPActionDispatcher);
container.bind(TYPES.IFeedbackActionDispatcher).toConstantValue(sandbox.createStubInstance(FeedbackActionDispatcher));
container.bind(TYPES.ITypeHintProvider).toConstantValue(typeHintProviderMock);
bindOrRebind(container, TYPES.Action).toConstantValue(ApplyTypeHintsAction.create());
const command = container.resolve(ApplyTypeHintsCommand);
beforeEach(() => {
sandbox.reset();
});
describe('test hints to model feature translation (after command execution)`', () => {
describe('ShapeTypeHint', () => {
const allEnabledHint: ShapeTypeHint = {
elementTypeId: 'node',
deletable: true,
reparentable: true,
repositionable: true,
resizable: true,
containableElementTypeIds: []
};
const allDisabledHint: ShapeTypeHint = {
elementTypeId: 'node',
deletable: false,
reparentable: false,
repositionable: false,
resizable: false,
containableElementTypeIds: []
};
it('should not modify feature set of model element with no applicable type hint', () => {
typeHintProviderMock.getShapeTypeHint.returns(undefined);
const result = command.execute(createCommandExecutionContext(createNode()));
const element = result.children[0];
expect(GNode.DEFAULT_FEATURES, 'Element should have default feature set').to.have.same.members([
...(element.features as Set<symbol>)
]);
});
it('should add all enabled (`true`) features, derived from the applied type hint, to the model', () => {
typeHintProviderMock.getShapeTypeHint.returns(allEnabledHint);
const result = command.execute(createCommandExecutionContext(createNode()));
const element = result.children[0];
expect(isDeletable(element), 'Element should have deletable feature').to.be.true;
expect(isReparentable(element), 'Element should have reparentable feature').to.be.true;
expect(isMoveable(element), 'Element should have moveable feature').to.be.true;
expect(isContainable(element), 'Element should have containable feature').to.be.true;
expect(isResizable(element), 'Element should have resizeable feature').to.be.true;
});
it('should remove all disabled (`false`) features, derived from the applied type hint, from the model', () => {
typeHintProviderMock.getShapeTypeHint.returns(allDisabledHint);
const result = command.execute(createCommandExecutionContext(createNode()));
const element = result.children[0];
expect(isDeletable(element), 'Element should not have deletable feature').to.be.false;
expect(isReparentable(element), 'Element should not have reparentable feature').to.be.false;
expect(isMoveable(element), 'Element should not have moveable feature').to.be.false;
expect(isResizable(element), 'Element should not have resizeable feature').to.be.false;
});
describe('`isConnectable` (after hint has been applied to element)', () => {
const shapeHint: Writable<ShapeTypeHint> = {
deletable: false,
elementTypeId: 'node',
reparentable: false,
repositionable: false,
resizable: false
};
const edgeHint: Writable<EdgeTypeHint> = {
deletable: false,
elementTypeId: 'edge',
repositionable: false,
routable: false
};
it('should return `true` if source/target elements are not defined in edge hint', () => {
typeHintProviderMock.getShapeTypeHint.returns(shapeHint);
typeHintProviderMock.getEdgeTypeHint.returns(edgeHint);
const result = command.execute(createCommandExecutionContext(createNode()));
const element = result.children[0] as GNode;
const edge = createEdge();
expect(element.canConnect(edge, 'source')).to.be.true;
expect(element.canConnect(edge, 'target')).to.be.true;
});
it('should return `false` if element type is not in source/target elements of edge hint', () => {
typeHintProviderMock.getShapeTypeHint.returns(shapeHint);
typeHintProviderMock.getEdgeTypeHint.returns(edgeHint);
edgeHint.sourceElementTypeIds = [];
edgeHint.targetElementTypeIds = [];
const result = command.execute(createCommandExecutionContext(createNode()));
const element = result.children[0] as GNode;
const edge = createEdge();
expect(element.canConnect(edge, 'source')).to.be.false;
expect(element.canConnect(edge, 'target')).to.be.false;
});
it('should return `true` if element type is in source/target elements of edge hint (exact type)', () => {
typeHintProviderMock.getShapeTypeHint.returns(shapeHint);
typeHintProviderMock.getEdgeTypeHint.returns(edgeHint);
edgeHint.sourceElementTypeIds = ['node'];
edgeHint.targetElementTypeIds = ['node'];
const result = command.execute(createCommandExecutionContext(createNode()));
const element = result.children[0] as GNode;
const edge = createEdge();
expect(element.canConnect(edge, 'source')).to.be.true;
expect(element.canConnect(edge, 'target')).to.be.true;
});
it('should return `true` if element super type is in source/target elements of edge hint (super type)', () => {
typeHintProviderMock.getShapeTypeHint.returns(shapeHint);
typeHintProviderMock.getEdgeTypeHint.returns(edgeHint);
edgeHint.sourceElementTypeIds = ['node'];
edgeHint.targetElementTypeIds = ['node'];
const result = command.execute(createCommandExecutionContext(createNode('node:task:automated')));
const element = result.children[0] as GNode;
const edge = createEdge();
expect(element.canConnect(edge, 'source')).to.be.true;
expect(element.canConnect(edge, 'target')).to.be.true;
});
it('should fallback to class-level `canConnect` implementation if no edge hint is applicable to routable', () => {
typeHintProviderMock.getEdgeTypeHint.returns(undefined);
typeHintProviderMock.getShapeTypeHint.returns(shapeHint);
const node = createNode();
const originalCanConnectSpy = sinon.spy(node, 'canConnect');
const result = command.execute(createCommandExecutionContext(node));
const element = result.children[0] as GNode;
const edge = createEdge();
expect(element.canConnect(edge, 'source')).to.be.true;
expect(element.canConnect(edge, 'target')).to.be.true;
expect(originalCanConnectSpy.called).to.be.true;
});
});
describe('`isContainable` (after hint has been applied to element)', () => {
const shapeHint: Writable<ShapeTypeHint> = {
deletable: false,
elementTypeId: 'node',
reparentable: false,
repositionable: false,
resizable: false
};
it('should return `false` if corresponding hint has no containable elements defined', () => {
typeHintProviderMock.getShapeTypeHint.returns(shapeHint);
const result = command.execute(createCommandExecutionContext(createNode('node')));
const element = result.children[0] as GNode & Containable;
expect(element.isContainableElement('other')).to.be.false;
});
it('should return `true` if corresponding hint has containable element with matching type', () => {
typeHintProviderMock.getShapeTypeHint.returns(shapeHint);
shapeHint.containableElementTypeIds = ['node'];
const result = command.execute(createCommandExecutionContext(createNode('node')));
const element = result.children[0] as GNode & Containable;
expect(element.isContainableElement('node')).to.be.true;
});
it('should return `true` if corresponding hint as has containable element with matching super type', () => {
typeHintProviderMock.getShapeTypeHint.returns(shapeHint);
shapeHint.containableElementTypeIds = ['node'];
const result = command.execute(createCommandExecutionContext(createNode('node')));
const element = result.children[0] as GNode & Containable;
expect(element.isContainableElement('node:task:automated')).to.be.true;
});
});
});
describe('EdgeTypeHint', () => {
const allEnabledHint: EdgeTypeHint = {
elementTypeId: 'edge',
deletable: true,
repositionable: true,
routable: true
};
const allDisabledHint: EdgeTypeHint = {
elementTypeId: 'edge',
deletable: false,
repositionable: false,
routable: false
};
it('should not modify feature set of model element with no applicable type hint', () => {
typeHintProviderMock.getEdgeTypeHint.returns(undefined);
const result = command.execute(createCommandExecutionContext(createEdge()));
const element = result.children[0];
expect(GEdge.DEFAULT_FEATURES, 'Element should have default feature set').to.have.same.members([
...(element.features as Set<symbol>)
]);
});
it('should add all enabled (`true`) features, derived from the applied type hint, to the model', () => {
typeHintProviderMock.getEdgeTypeHint.returns(allEnabledHint);
const result = command.execute(createCommandExecutionContext(createEdge()));
const element = result.children[0];
expect(isDeletable(element), 'Element should have deletable feature').to.be.true;
expect(element.hasFeature(editFeature), 'Element should have edit feature').to.be.true;
expect(isReconnectable(element), 'Element should have reconnectable feature').to.be.true;
});
it('should remove all disabled (`false`) features, derived from the applied type hint, from the model', () => {
typeHintProviderMock.getEdgeTypeHint.returns(allDisabledHint);
const result = command.execute(createCommandExecutionContext(createEdge()));
const element = result.children[0];
expect(isDeletable(element), 'Element should not have deletable feature').to.be.false;
expect(element.hasFeature(editFeature), 'Element should not have edit feature').to.be.false;
expect(isReconnectable(element), 'Element should not have reconnectable feature').to.be.false;
});
});
});
});