diff --git a/.eslintrc.js b/.eslintrc.js
index 01da5d8..1a74b5f 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -25,7 +25,7 @@ module.exports = {
"The sprotty-protocol default exports are customized and reexported by GLSP. Please use '@eclipse-glsp/client' instead"
}
],
- patterns: ['../../../../*', '**/../index']
+ patterns: ['**/../index']
}
]
}
diff --git a/examples/workflow-test/README.md b/examples/workflow-test/README.md
index 1d6d18d..f859ea6 100644
--- a/examples/workflow-test/README.md
+++ b/examples/workflow-test/README.md
@@ -21,16 +21,16 @@ This package contains code examples that demonstrate how to test diagram editors
| Element Hover | ✓ | ✓ | ✓ |
| Element Validation | ✓ | ✓ | ✓ |
| Element Navigation | ✓ | ✓ | x |
-| Element Type Hints | - | - | - |
-| Element Creation and Deletion | - | - | - |
-| Node Change Bounds
- Move
- Resize |
-
- |
-
- |
-
- |
+| Element Type Hints | ✓ | ✓ | ✓ |
+| Element Creation and Deletion | ✓ | ✓ | ✓ |
+| Node Change Bounds
- Move
- Resize |
✓
✓ |
✓
✓ |
✓
✓ |
| Node Change Container | - | - | - |
-| Edge Reconnect | - | - | - |
-| Edge Routing Points | - | - | - |
+| Edge Reconnect | ✓ | ✓ | ✓ |
+| Edge Routing Points | ✓ | ✓ | ✓ |
| Ghost Elements | - | - | - |
-| Element Text Editing | - | - | - |
+| Element Text Editing | ✓ | ✓ | ✓ |
| Clipboard (Cut, Copy, Paste) | - | - | - |
-| Undo / Redo | - | - | - |
+| Undo / Redo | ✓ | ✓ | x |
| Contexts
- Context Menu
- Command Palette
- Tool Palette |
-
- |
-
-
- |
-
-
- |
| Accessibility Features (experimental)
- Search
- Move
- Zoom
- Resize |
-
-
-
- | | |
| Helper Lines (experimental) | - | - | - |
diff --git a/examples/workflow-test/package.json b/examples/workflow-test/package.json
index 2cecab6..e95e837 100644
--- a/examples/workflow-test/package.json
+++ b/examples/workflow-test/package.json
@@ -25,6 +25,8 @@
"url": "https://me.big.tuwien.ac.at/"
}
],
+ "main": "lib/index",
+ "types": "lib/index",
"files": [
"src",
"tests"
@@ -43,8 +45,11 @@
"test:vscode-setup": "yarn test --project=vscode-setup",
"watch": "tsc -w"
},
+ "dependencies": {
+ "@eclipse-glsp/client": "next"
+ },
"devDependencies": {
- "@eclipse-glsp-examples/workflow-server-bundled": "next",
+ "@eclipse-glsp-examples/workflow-server-bundled": "2.3.0-next",
"@eclipse-glsp/glsp-playwright": "2.3.0-next",
"@playwright/test": "^1.37.1",
"@theia/playwright": "~1.49.1",
diff --git a/examples/workflow-test/playwright.config.ts b/examples/workflow-test/playwright.config.ts
index f3c2de2..0a8e2c2 100644
--- a/examples/workflow-test/playwright.config.ts
+++ b/examples/workflow-test/playwright.config.ts
@@ -34,7 +34,7 @@ const config: PlaywrightTestConfig = {
},
fullyParallel: true,
forbidOnly: !!process.env.CI,
- retries: process.env.CI ? 2 : 0,
+ retries: process.env.CI ? 2 : 1,
reporter: process.env.CI ? [['html', { open: 'never' }], ['@estruyf/github-actions-reporter']] : [['html', { open: 'never' }]],
use: {
actionTimeout: 0,
diff --git a/examples/workflow-test/src/graph/elements/activity-node-decision.po.ts b/examples/workflow-test/src/graph/elements/activity-node-decision.po.ts
new file mode 100644
index 0000000..513cbba
--- /dev/null
+++ b/examples/workflow-test/src/graph/elements/activity-node-decision.po.ts
@@ -0,0 +1,24 @@
+/********************************************************************************
+ * Copyright (c) 2023 Business Informatics Group (TU Wien) 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 { NodeMetadata, PNode } from '@eclipse-glsp/glsp-playwright';
+import { Mix, useClickableFlow } from '@eclipse-glsp/glsp-playwright/src/extension';
+
+export const ActivityNodeDecisionMixin = Mix(PNode).flow(useClickableFlow).build();
+
+@NodeMetadata({
+ type: 'activityNode:decision'
+})
+export class ActivityNodeDecision extends ActivityNodeDecisionMixin {}
diff --git a/examples/workflow-test/src/graph/elements/activity-node-fork.po.ts b/examples/workflow-test/src/graph/elements/activity-node-fork.po.ts
index 9826d3b..c10c122 100644
--- a/examples/workflow-test/src/graph/elements/activity-node-fork.po.ts
+++ b/examples/workflow-test/src/graph/elements/activity-node-fork.po.ts
@@ -14,8 +14,11 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
import { NodeMetadata, PNode } from '@eclipse-glsp/glsp-playwright';
+import { Mix, useClickableFlow } from '@eclipse-glsp/glsp-playwright/src/extension';
+
+export const ActivityNodeForkMixin = Mix(PNode).flow(useClickableFlow).build();
@NodeMetadata({
type: 'activityNode:fork'
})
-export class ActivityNodeFork extends PNode {}
+export class ActivityNodeFork extends ActivityNodeForkMixin {}
diff --git a/examples/workflow-test/src/graph/elements/category.po.ts b/examples/workflow-test/src/graph/elements/category.po.ts
new file mode 100644
index 0000000..1a842b4
--- /dev/null
+++ b/examples/workflow-test/src/graph/elements/category.po.ts
@@ -0,0 +1,58 @@
+/********************************************************************************
+ * Copyright (c) 2023 Business Informatics Group (TU Wien) 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 {
+ ChildrenAccessor,
+ Mix,
+ NodeMetadata,
+ PLabelledElement,
+ PNode,
+ SVGMetadataUtils,
+ useClickableFlow,
+ useCommandPaletteCapability,
+ useDeletableFlow,
+ useDraggableFlow,
+ useRenameableFlow,
+ useResizeHandleCapability,
+ useSelectableFlow
+} from '@eclipse-glsp/glsp-playwright/';
+import { LabelHeading } from './label-heading.po';
+
+export const CategoryMixin = Mix(PNode)
+ .flow(useClickableFlow)
+ .flow(useDeletableFlow)
+ .flow(useDraggableFlow)
+ .flow(useRenameableFlow)
+ .flow(useSelectableFlow)
+ .capability(useResizeHandleCapability)
+ .capability(useCommandPaletteCapability)
+ .build();
+
+@NodeMetadata({
+ type: 'category'
+})
+export class Category extends CategoryMixin implements PLabelledElement {
+ override readonly children = new CategoryChildren(this);
+
+ get label(): Promise {
+ return this.children.label().then(label => label.textContent());
+ }
+}
+
+export class CategoryChildren extends ChildrenAccessor {
+ async label(): Promise {
+ return this.ofType(LabelHeading, { selector: SVGMetadataUtils.typeAttrOf(LabelHeading) });
+ }
+}
diff --git a/examples/workflow-test/src/graph/elements/edge.po.ts b/examples/workflow-test/src/graph/elements/edge.po.ts
index 86b87ae..989725c 100644
--- a/examples/workflow-test/src/graph/elements/edge.po.ts
+++ b/examples/workflow-test/src/graph/elements/edge.po.ts
@@ -14,8 +14,9 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
import { EdgeMetadata, Mix, PEdge, useClickableFlow, useRoutingPointCapability } from '@eclipse-glsp/glsp-playwright/';
+import { useReconnectCapability } from '@eclipse-glsp/glsp-playwright/src/glsp/features/reconnect';
-const EdgeMixin = Mix(PEdge).flow(useClickableFlow).capability(useRoutingPointCapability).build();
+const EdgeMixin = Mix(PEdge).flow(useClickableFlow).capability(useRoutingPointCapability).capability(useReconnectCapability).build();
@EdgeMetadata({
type: 'edge'
diff --git a/examples/workflow-test/src/graph/elements/weighted-edge.po.ts b/examples/workflow-test/src/graph/elements/weighted-edge.po.ts
new file mode 100644
index 0000000..94fa041
--- /dev/null
+++ b/examples/workflow-test/src/graph/elements/weighted-edge.po.ts
@@ -0,0 +1,23 @@
+/********************************************************************************
+ * Copyright (c) 2023 Business Informatics Group (TU Wien) 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 { EdgeMetadata, Mix, PEdge, useClickableFlow, useRoutingPointCapability } from '@eclipse-glsp/glsp-playwright/';
+
+const EdgeMixin = Mix(PEdge).flow(useClickableFlow).capability(useRoutingPointCapability).build();
+
+@EdgeMetadata({
+ type: 'edge:weighted'
+})
+export class WeightedEdge extends EdgeMixin {}
diff --git a/examples/workflow-test/tests/core/connectable-element.spec.ts b/examples/workflow-test/tests/core/connectable-element.spec.ts
index 0752670..572f7c3 100644
--- a/examples/workflow-test/tests/core/connectable-element.spec.ts
+++ b/examples/workflow-test/tests/core/connectable-element.spec.ts
@@ -19,8 +19,7 @@ import { ActivityNodeFork } from '../../src/graph/elements/activity-node-fork.po
import { Edge } from '../../src/graph/elements/edge.po';
import { TaskManual } from '../../src/graph/elements/task-manual.po';
import { WorkflowGraph } from '../../src/graph/workflow.graph';
-
-const elementLabel = 'Push';
+import { TaskManualNodes } from '../nodes';
test.describe('The edge accessor of a connectable element', () => {
let app: WorkflowApp;
@@ -35,7 +34,7 @@ test.describe('The edge accessor of a connectable element', () => {
});
test('should allow accessing all edges of a type', async () => {
- const task = await graph.getNodeByLabel(elementLabel, TaskManual);
+ const task = await graph.getNodeByLabel(TaskManualNodes.pushLabel, TaskManual);
const edges = await task.edges().outgoingEdgesOfType(Edge);
const ids = await Promise.all(edges.map(async e => e.idAttr()));
@@ -50,7 +49,7 @@ test.describe('The edge accessor of a connectable element', () => {
});
test('should return typed sources on access', async () => {
- const task = await graph.getNodeByLabel(elementLabel, TaskManual);
+ const task = await graph.getNodeByLabel(TaskManualNodes.pushLabel, TaskManual);
const edges = await task.edges().outgoingEdgesOfType(Edge);
expect(edges.length).toBe(1);
@@ -60,7 +59,7 @@ test.describe('The edge accessor of a connectable element', () => {
});
test('should allow accessing all edges of a type against a target type', async () => {
- const task = await graph.getNodeByLabel(elementLabel, TaskManual);
+ const task = await graph.getNodeByLabel(TaskManualNodes.pushLabel, TaskManual);
const edges = await task.edges().outgoingEdgesOfType(Edge, { targetConstructor: ActivityNodeFork });
expect(edges.length).toBe(1);
diff --git a/examples/workflow-test/tests/core/edge.spec.ts b/examples/workflow-test/tests/core/edge.spec.ts
index 9c2246a..d4297ef 100644
--- a/examples/workflow-test/tests/core/edge.spec.ts
+++ b/examples/workflow-test/tests/core/edge.spec.ts
@@ -19,8 +19,7 @@ import { ActivityNodeFork } from '../../src/graph/elements/activity-node-fork.po
import { Edge } from '../../src/graph/elements/edge.po';
import { TaskManual } from '../../src/graph/elements/task-manual.po';
import { WorkflowGraph } from '../../src/graph/workflow.graph';
-
-const elementLabel = 'Push';
+import { TaskManualNodes } from '../nodes';
test.describe('Edges', () => {
let app: WorkflowApp;
@@ -35,7 +34,7 @@ test.describe('Edges', () => {
});
test('should have source and target nodes', async () => {
- const source = await graph.getNodeByLabel(elementLabel, TaskManual);
+ const source = await graph.getNodeByLabel(TaskManualNodes.pushLabel, TaskManual);
const target = await graph.getNode('[id$="fork_1"]', ActivityNodeFork);
const edge = await graph.getEdgeBetween(Edge, { sourceNode: source, targetNode: target });
diff --git a/examples/workflow-test/tests/core/parent.spec.ts b/examples/workflow-test/tests/core/parent.spec.ts
index 29fe7a5..dfa012e 100644
--- a/examples/workflow-test/tests/core/parent.spec.ts
+++ b/examples/workflow-test/tests/core/parent.spec.ts
@@ -18,8 +18,7 @@ import { WorkflowApp } from '../../src/app/workflow-app';
import { LabelHeading } from '../../src/graph/elements/label-heading.po';
import { TaskManual } from '../../src/graph/elements/task-manual.po';
import { WorkflowGraph } from '../../src/graph/workflow.graph';
-
-const elementLabel = 'Push';
+import { TaskManualNodes } from '../nodes';
test.describe('The children accessor of a parent element', () => {
let app: WorkflowApp;
@@ -34,7 +33,7 @@ test.describe('The children accessor of a parent element', () => {
});
test('should allow to access all elements by using a type', async () => {
- const task = await graph.getNodeByLabel(elementLabel, TaskManual);
+ const task = await graph.getNodeByLabel(TaskManualNodes.pushLabel, TaskManual);
const children = task.children;
const labels = await children.allOfType(LabelHeading);
@@ -45,7 +44,7 @@ test.describe('The children accessor of a parent element', () => {
});
test('should allow to access the element by using a type', async () => {
- const task = await graph.getNodeByLabel(elementLabel, TaskManual);
+ const task = await graph.getNodeByLabel(TaskManualNodes.pushLabel, TaskManual);
const children = task.children;
const label = await children.ofType(LabelHeading);
@@ -53,7 +52,7 @@ test.describe('The children accessor of a parent element', () => {
});
test('should allow to access the element by using a type and a selector', async () => {
- const task = await graph.getNodeByLabel(elementLabel, TaskManual);
+ const task = await graph.getNodeByLabel(TaskManualNodes.pushLabel, TaskManual);
const children = task.children;
const label = await children.ofType(LabelHeading, { selector: '[id$="task_Push_label"]' });
@@ -61,7 +60,7 @@ test.describe('The children accessor of a parent element', () => {
});
test('should allow to use typed elements', async () => {
- const task = await graph.getNodeByLabel(elementLabel, TaskManual);
+ const task = await graph.getNodeByLabel(TaskManualNodes.pushLabel, TaskManual);
const children = task.children;
const label = await children.label();
diff --git a/examples/workflow-test/tests/core/shortcuts.spec.ts b/examples/workflow-test/tests/core/shortcuts.spec.ts
index cfcb269..43cbbc6 100644
--- a/examples/workflow-test/tests/core/shortcuts.spec.ts
+++ b/examples/workflow-test/tests/core/shortcuts.spec.ts
@@ -17,8 +17,7 @@ import { expect, test } from '@eclipse-glsp/glsp-playwright';
import { WorkflowApp } from '../../src/app/workflow-app';
import { TaskManual } from '../../src/graph/elements/task-manual.po';
import { WorkflowGraph } from '../../src/graph/workflow.graph';
-
-const elementLabel = 'Push';
+import { TaskManualNodes } from '../nodes';
test.describe('Shortcuts', () => {
let app: WorkflowApp;
@@ -33,7 +32,7 @@ test.describe('Shortcuts', () => {
});
test('should allow deleting the element in the graph', async ({ integration }) => {
- const task = await graph.getNodeByLabel(elementLabel, TaskManual);
+ const task = await graph.getNodeByLabel(TaskManualNodes.pushLabel, TaskManual);
expect(await task.isVisible()).toBeTruthy();
await task.click();
diff --git a/examples/workflow-test/tests/features/change-bounds/resize-handle.spec.ts b/examples/workflow-test/tests/features/change-bounds/resize-handle.spec.ts
index 72424f1..5185425 100644
--- a/examples/workflow-test/tests/features/change-bounds/resize-handle.spec.ts
+++ b/examples/workflow-test/tests/features/change-bounds/resize-handle.spec.ts
@@ -17,8 +17,7 @@ import { PMetadata, ResizeHandle, expect, test } from '@eclipse-glsp/glsp-playwr
import { WorkflowApp } from '../../../src/app/workflow-app';
import { TaskManual } from '../../../src/graph/elements/task-manual.po';
import { WorkflowGraph } from '../../../src/graph/workflow.graph';
-
-const elementLabel = 'Push';
+import { TaskManualNodes } from '../../nodes';
test.describe('The resizing handle', () => {
let app: WorkflowApp;
@@ -33,7 +32,7 @@ test.describe('The resizing handle', () => {
});
test('should allow resizing', async () => {
- const task = await graph.getNodeByLabel(elementLabel, TaskManual);
+ const task = await graph.getNodeByLabel(TaskManualNodes.pushLabel, TaskManual);
const oldBounds = await task.bounds();
const oldTopLeft = oldBounds.position('top_left');
@@ -52,7 +51,7 @@ test.describe('The resizing handle', () => {
});
test('should show 4 handles', async () => {
- const task = await graph.getNodeByLabel(elementLabel, TaskManual);
+ const task = await graph.getNodeByLabel(TaskManualNodes.pushLabel, TaskManual);
await graph.waitForCreation(PMetadata.getType(ResizeHandle), async () => {
await task.click();
diff --git a/examples/workflow-test/tests/features/command-palette/command-palette.spec.ts b/examples/workflow-test/tests/features/command-palette/command-palette.spec.ts
index 581f7c5..e3a0433 100644
--- a/examples/workflow-test/tests/features/command-palette/command-palette.spec.ts
+++ b/examples/workflow-test/tests/features/command-palette/command-palette.spec.ts
@@ -13,18 +13,15 @@
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
+import { GLSPServer, ServerVariable } from '@eclipse-glsp/glsp-playwright';
import { GLSPGlobalCommandPalette, expect, test } from '@eclipse-glsp/glsp-playwright/';
-import { GLSPServer } from '@eclipse-glsp/glsp-playwright/src/glsp-server';
-import { ServerVariable } from '@eclipse-glsp/glsp-playwright/src/test/dynamic-variable';
import { WorkflowApp } from '../../../src/app/workflow-app';
import { Edge } from '../../../src/graph/elements/edge.po';
import { TaskAutomated } from '../../../src/graph/elements/task-automated.po';
import { TaskManual } from '../../../src/graph/elements/task-manual.po';
import { WorkflowGraph } from '../../../src/graph/workflow.graph';
import { GLSP_SERVER_TYPE_JAVA, GLSP_SERVER_TYPE_NODE } from '../../../src/server';
-
-const element1Label = 'Push';
-const element2Label = 'ChkWt';
+import { TaskAutomatedNodes, TaskManualNodes } from '../../nodes';
test.describe('The command palette', () => {
let app: WorkflowApp;
@@ -127,7 +124,7 @@ test.describe('The command palette', () => {
await command.open();
await command.search('Create Manual Task', { confirm: true });
});
- expect(nodes.length).toBe(1);
+ expect(nodes).toHaveLength(1);
await graph.focus();
const newTask = nodes[0];
@@ -139,7 +136,7 @@ test.describe('The command palette', () => {
test.describe('in the element context', () => {
test('should allow to search suggestions', async () => {
- const task = await graph.getNodeByLabel(element1Label, TaskManual);
+ const task = await graph.getNodeByLabel(TaskManualNodes.pushLabel, TaskManual);
const elementCommandPalette = await task.commandPalette();
await elementCommandPalette.open();
@@ -207,14 +204,14 @@ test.describe('The command palette', () => {
});
test('should allow creating new elements in the diagram', async () => {
- const task = await graph.getNodeByLabel(element1Label, TaskManual);
+ const task = await graph.getNodeByLabel(TaskManualNodes.pushLabel, TaskManual);
const nodes = await graph.waitForCreationOfType(TaskManual, async () => {
const command = task.commandPalette();
await command.open();
await command.search('Create Manual Task', { confirm: true });
});
- expect(nodes.length).toBe(1);
+ expect(nodes).toHaveLength(1);
await graph.focus();
const newTask = nodes[0];
@@ -224,8 +221,8 @@ test.describe('The command palette', () => {
});
test('should allow creating edges in the graph', async () => {
- const source = await graph.getNodeByLabel(element1Label, TaskManual);
- const target = await graph.getNodeByLabel(element2Label, TaskAutomated);
+ const source = await graph.getNodeByLabel(TaskManualNodes.pushLabel, TaskManual);
+ const target = await graph.getNodeByLabel(TaskAutomatedNodes.chkwtLabel, TaskAutomated);
const edges = await graph.waitForCreationOfType(Edge, async () => {
const command = source.commandPalette();
diff --git a/examples/workflow-test/tests/features/hover/popup.spec.ts b/examples/workflow-test/tests/features/hover/popup.spec.ts
index bfe3394..3643662 100644
--- a/examples/workflow-test/tests/features/hover/popup.spec.ts
+++ b/examples/workflow-test/tests/features/hover/popup.spec.ts
@@ -14,16 +14,16 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
import {
+ PLabelledElement,
PNode,
PNodeConstructor,
PopupCapability,
+ ServerVariable,
expect,
runInIntegration,
skipNonIntegration,
test
-} from '@eclipse-glsp/glsp-playwright/';
-import { PLabelledElement } from '@eclipse-glsp/glsp-playwright/src/extension';
-import { ServerVariable } from '@eclipse-glsp/glsp-playwright/src/test/dynamic-variable';
+} from '@eclipse-glsp/glsp-playwright';
import { dedent } from 'ts-dedent';
import { WorkflowApp } from '../../../src/app/workflow-app';
import { TaskAutomated } from '../../../src/graph/elements/task-automated.po';
diff --git a/examples/workflow-test/tests/core/flows/deletable.spec.ts b/examples/workflow-test/tests/features/label-edit/label-edit-tool.spec.ts
similarity index 52%
rename from examples/workflow-test/tests/core/flows/deletable.spec.ts
rename to examples/workflow-test/tests/features/label-edit/label-edit-tool.spec.ts
index af2e0ee..7d3ccb2 100644
--- a/examples/workflow-test/tests/core/flows/deletable.spec.ts
+++ b/examples/workflow-test/tests/features/label-edit/label-edit-tool.spec.ts
@@ -13,14 +13,13 @@
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
-import { expect, test } from '@eclipse-glsp/glsp-playwright/';
+import { expect, test } from '@eclipse-glsp/glsp-playwright';
import { WorkflowApp } from '../../../src/app/workflow-app';
import { TaskManual } from '../../../src/graph/elements/task-manual.po';
import { WorkflowGraph } from '../../../src/graph/workflow.graph';
+import { TaskManualNodes } from '../../nodes';
-const elementLabel = 'Push';
-
-test.describe('Deletable flow', () => {
+test.describe('The label edit tool', () => {
let app: WorkflowApp;
let graph: WorkflowGraph;
@@ -32,12 +31,34 @@ test.describe('Deletable flow', () => {
graph = app.graph;
});
- test('should delete element', async () => {
- const task = await graph.getNodeByLabel(elementLabel, TaskManual);
+ test('should allow nodes to be renamed', async () => {
+ const node = await graph.getNodeByLabel(TaskManualNodes.pushLabel, TaskManual);
+
+ await node.rename('New Label');
+ expect(await node.label).toBe('New Label');
+ });
+
+ test('should allow nodes to be renamed by using the keyboard', async () => {
+ const node = await graph.getNodeByLabel(TaskManualNodes.pushLabel, TaskManual);
+
+ await node.click();
+ await node.page.keyboard.press('F2');
+ await node.page.keyboard.type('New Label');
+ await node.page.keyboard.press('Enter');
+
+ expect(await node.label).toBe('New Label');
+ });
+
+ test('should not allow empty text', async () => {
+ const node = await graph.getNodeByLabel(TaskManualNodes.pushLabel, TaskManual);
+
+ await node.click();
+ await node.page.keyboard.press('F2');
+ await node.page.keyboard.type(' ');
+ await node.page.keyboard.press('Backspace');
+ await node.page.keyboard.press('Enter');
- expect(await task.locate().count()).toBe(1);
- await task.delete();
- expect(await task.locate().count()).toBe(0);
+ await expect(await app.labelEditor.getWarning()).toBe('Name must not be empty');
});
test.afterEach(async ({ integration }) => {
diff --git a/examples/workflow-test/tests/features/tool-palette/tool-palette.spec.ts b/examples/workflow-test/tests/features/tool-palette/tool-palette.spec.ts
index 97c2625..dedb1b9 100644
--- a/examples/workflow-test/tests/features/tool-palette/tool-palette.spec.ts
+++ b/examples/workflow-test/tests/features/tool-palette/tool-palette.spec.ts
@@ -16,14 +16,8 @@
import { Marker, expect, test } from '@eclipse-glsp/glsp-playwright';
import { WorkflowApp } from '../../../src/app/workflow-app';
import { WorkflowToolPalette } from '../../../src/features/tool-palette/workflow-tool-palette';
-import { Edge } from '../../../src/graph/elements/edge.po';
-import { TaskAutomated } from '../../../src/graph/elements/task-automated.po';
-import { TaskManual } from '../../../src/graph/elements/task-manual.po';
import { WorkflowGraph } from '../../../src/graph/workflow.graph';
-const element1Label = 'Push';
-const element2Label = 'ChkWt';
-
test.describe('The tool palette', () => {
let app: WorkflowApp;
let graph: WorkflowGraph;
@@ -70,25 +64,25 @@ test.describe('The tool palette', () => {
await toolPalette.waitForVisible();
const deleteTool = await toolPalette.toolbar.deletionTool();
- expect(await deleteTool.classAttr()).not.toContain('clicked');
+ await expect(deleteTool).not.toContainClass('clicked');
await deleteTool.click();
- expect(await deleteTool.classAttr()).toContain('clicked');
+ await expect(deleteTool).toContainClass('clicked');
const selectionTool = await toolPalette.toolbar.selectionTool();
- expect(await selectionTool.classAttr()).not.toContain('clicked');
+ await expect(selectionTool).not.toContainClass('clicked');
await selectionTool.click();
- expect(await selectionTool.classAttr()).toContain('clicked');
+ await expect(selectionTool).toContainClass('clicked');
const marqueeTool = await toolPalette.toolbar.marqueeTool();
- expect(await marqueeTool.classAttr()).not.toContain('clicked');
+ await expect(marqueeTool).not.toContainClass('clicked');
await marqueeTool.click();
- expect(await marqueeTool.classAttr()).toContain('clicked');
+ await expect(marqueeTool).toContainClass('clicked');
const searchTool = await toolPalette.toolbar.searchTool();
- expect(await searchTool.input.isHidden()).toBeTruthy();
+ expect(searchTool.input.isHidden()).toBeTruthy();
await searchTool.click();
- expect(await searchTool.input.isVisible()).toBeTruthy();
+ expect(searchTool.input.isVisible()).toBeTruthy();
await searchTool.search('Auto');
const groups = await toolPalette.content.toolGroups();
@@ -100,60 +94,6 @@ test.describe('The tool palette', () => {
expect(await elements0[0].text()).toBe('Automated Task');
});
- test('should allow creating edges in the graph', async () => {
- const source = await graph.getNodeByLabel(element1Label, TaskManual);
- const target = await graph.getNodeByLabel(element2Label, TaskAutomated);
-
- const edges = await graph.waitForCreationOfType(Edge, async () => {
- await toolPalette.waitForVisible();
- const paletteItem = await toolPalette.content.toolElement('Edges', 'Edge');
- await paletteItem.click();
-
- await source.click();
- await target.click();
- });
- expect(edges.length).toBe(1);
- await graph.focus();
-
- const newEdge = edges[0];
-
- const sourceId = await newEdge.sourceId();
- expect(sourceId).toBe(await source.idAttr());
-
- const targetId = await newEdge.targetId();
- expect(targetId).toBe(await target.idAttr());
- });
-
- test('should allow creating new nodes in the graph', async () => {
- const task = await graph.getNodeByLabel(element1Label, TaskManual);
- const nodes = await graph.waitForCreationOfType(TaskManual, async () => {
- const paletteItem = await toolPalette.content.toolElement('Nodes', 'Manual Task');
- await paletteItem.click();
-
- const taskBounds = await task.bounds();
- await taskBounds.position('bottom_left').moveRelative(-50, 0).click();
- });
- expect(nodes.length).toBe(1);
- await graph.focus();
-
- const newTask = nodes[0];
-
- const label = await newTask.children.label();
- expect(await label.textContent()).toBe('ManualTask8');
- });
-
- test('should allow deleting elements in the graph', async () => {
- await toolPalette.toolbar.deletionTool().click();
-
- const task = await graph.getNodeByLabel(element1Label, TaskManual);
- expect(await task.isVisible()).toBeTruthy();
-
- await task.click();
- await task.waitFor({ state: 'detached' });
-
- expect(await task.locate().count()).toBe(0);
- });
-
test('should allow to validate', async () => {
const markers = await graph.waitForCreationOfType(Marker, async () => {
await toolPalette.toolbar.validateTool().click();
diff --git a/examples/workflow-test/tests/features/tools/deletion/deletion-tool.spec.ts b/examples/workflow-test/tests/features/tools/deletion/deletion-tool.spec.ts
new file mode 100644
index 0000000..b706fd8
--- /dev/null
+++ b/examples/workflow-test/tests/features/tools/deletion/deletion-tool.spec.ts
@@ -0,0 +1,68 @@
+/********************************************************************************
+ * Copyright (c) 2023 Business Informatics Group (TU Wien) 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 { expect, test } from '@eclipse-glsp/glsp-playwright';
+import { WorkflowApp } from '../../../../src/app/workflow-app';
+import { WorkflowToolPalette } from '../../../../src/features/tool-palette/workflow-tool-palette';
+import { TaskManual } from '../../../../src/graph/elements/task-manual.po';
+import { WorkflowGraph } from '../../../../src/graph/workflow.graph';
+import { TaskManualNodes } from '../../../nodes';
+
+test.describe('The deletion tool', () => {
+ let app: WorkflowApp;
+ let graph: WorkflowGraph;
+ let toolPalette: WorkflowToolPalette;
+
+ test.beforeEach(async ({ integration }) => {
+ app = new WorkflowApp({
+ type: 'integration',
+ integration
+ });
+ graph = app.graph;
+ toolPalette = app.toolPalette;
+ });
+
+ test('should allow deleting elements in the graph by mouse', async () => {
+ await toolPalette.toolbar.deletionTool().click();
+
+ const task = await graph.getNodeByLabel(TaskManualNodes.pushLabel, TaskManual);
+ expect(await task.isVisible()).toBeTruthy();
+
+ await task.click();
+ await task.waitFor({ state: 'detached' });
+
+ expect(await task.locate().count()).toBe(0);
+ });
+
+ test('should allow deleting elements in the graph by keyboard', async () => {
+ const task = await graph.getNodeByLabel(TaskManualNodes.pushLabel, TaskManual);
+
+ expect(await task.locate().count()).toBe(1);
+ await task.delete();
+ expect(await task.locate().count()).toBe(0);
+ });
+
+ test('should allow deleting elements in the graph', async () => {
+ const task = await graph.getNodeByLabel(TaskManualNodes.pushLabel, TaskManual);
+
+ expect(await task.locate().count()).toBe(1);
+ await task.delete();
+ expect(await task.locate().count()).toBe(0);
+ });
+
+ test.afterEach(async ({ integration }) => {
+ await integration?.close();
+ });
+});
diff --git a/examples/workflow-test/tests/features/tools/edge-creation/edge-creation-tool.spec.ts b/examples/workflow-test/tests/features/tools/edge-creation/edge-creation-tool.spec.ts
new file mode 100644
index 0000000..f930d1c
--- /dev/null
+++ b/examples/workflow-test/tests/features/tools/edge-creation/edge-creation-tool.spec.ts
@@ -0,0 +1,131 @@
+/********************************************************************************
+ * Copyright (c) 2023 Business Informatics Group (TU Wien) 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 { CursorCSS } from '@eclipse-glsp/client/lib/base/feedback/css-feedback';
+import { expect, test } from '@eclipse-glsp/glsp-playwright';
+import { WorkflowApp } from '../../../../src/app/workflow-app';
+import { WorkflowToolPalette } from '../../../../src/features/tool-palette/workflow-tool-palette';
+import { ActivityNodeDecision } from '../../../../src/graph/elements/activity-node-decision.po';
+import { Edge } from '../../../../src/graph/elements/edge.po';
+import { TaskAutomated } from '../../../../src/graph/elements/task-automated.po';
+import { TaskManual } from '../../../../src/graph/elements/task-manual.po';
+import { WeightedEdge } from '../../../../src/graph/elements/weighted-edge.po';
+import { WorkflowGraph } from '../../../../src/graph/workflow.graph';
+import { TaskAutomatedNodes, TaskManualNodes } from '../../../nodes';
+
+test.describe('The edge creation tool', () => {
+ let app: WorkflowApp;
+ let graph: WorkflowGraph;
+ let toolPalette: WorkflowToolPalette;
+
+ test.beforeEach(async ({ integration }) => {
+ app = new WorkflowApp({
+ type: 'integration',
+ integration
+ });
+ graph = app.graph;
+ toolPalette = app.toolPalette;
+ });
+
+ test('should allow creating edges in the graph', async () => {
+ const source = await graph.getNodeByLabel(TaskManualNodes.pushLabel, TaskManual);
+ const target = await graph.getNodeByLabel(TaskAutomatedNodes.chkwtLabel, TaskAutomated);
+
+ const edges = await graph.waitForCreationOfType(Edge, async () => {
+ await toolPalette.waitForVisible();
+ const paletteItem = await toolPalette.content.toolElement('Edges', 'Edge');
+ await paletteItem.click();
+
+ await source.bounds().then(bounds => bounds.position('middle_center').move());
+ await expect(graph).toContainClass(CursorCSS.EDGE_CREATION_SOURCE);
+ await source.click();
+
+ await target.bounds().then(bounds => bounds.position('middle_center').move());
+ await expect(graph).toContainClass(CursorCSS.EDGE_CREATION_TARGET);
+ await target.click();
+ });
+ expect(edges.length).toBe(1);
+ await graph.focus();
+
+ const newEdge = edges[0];
+
+ const sourceId = await newEdge.sourceId();
+ expect(sourceId).toBe(await source.idAttr());
+
+ const targetId = await newEdge.targetId();
+ expect(targetId).toBe(await target.idAttr());
+ });
+
+ // TODO: Test fails because of missing implementation
+ test('should allow creating weighted edges in the graph', async () => {
+ const chwkt = await graph.getNodeByLabel(TaskAutomatedNodes.chkwtLabel, TaskAutomated);
+ const wtok = await graph.getNodeByLabel(TaskAutomatedNodes.wtokLabel, TaskAutomated);
+
+ const outgoingEdge = await chwkt.edges().outgoingEdgeOfType(Edge);
+ const decisionNode = await outgoingEdge.targetOfType(ActivityNodeDecision);
+
+ await decisionNode.bounds().then(bounds => bounds.position('middle_center').move());
+
+ const edges = await graph.waitForCreationOfType(WeightedEdge, async () => {
+ await toolPalette.waitForVisible();
+ const paletteItem = await toolPalette.content.toolElement('Edges', 'Weighted edge');
+ await paletteItem.click();
+
+ await decisionNode.bounds().then(bounds => bounds.position('middle_center').move());
+ await expect(graph).toContainClass(CursorCSS.EDGE_CREATION_SOURCE);
+ await decisionNode.click();
+
+ await wtok.bounds().then(bounds => bounds.position('middle_center').move());
+ await expect(graph).toContainClass(CursorCSS.EDGE_CREATION_TARGET);
+ await wtok.click();
+ });
+ expect(edges.length).toBe(1);
+ await graph.focus();
+
+ const newEdge = edges[0];
+
+ const sourceId = await newEdge.sourceId();
+ expect(sourceId).toBe(await decisionNode.idAttr());
+
+ const targetId = await newEdge.targetId();
+ expect(targetId).toBe(await wtok.idAttr());
+ });
+
+ test('should prevent invalid combinations', async () => {
+ await toolPalette.waitForVisible();
+ const paletteItem = await toolPalette.content.toolElement('Edges', 'Weighted edge');
+ await paletteItem.click();
+
+ const source = await graph.getNodeByLabel(TaskManualNodes.pushLabel, TaskManual);
+ await source.bounds().then(bounds => bounds.position('middle_center').move());
+ await expect(graph).toContainClass(CursorCSS.OPERATION_NOT_ALLOWED);
+ });
+
+ test('should allow to cancel the operation', async () => {
+ const paletteItem = await toolPalette.content.toolElement('Nodes', 'Manual Task');
+ await paletteItem.click();
+
+ await expect(graph).toContainClass(CursorCSS.NODE_CREATION);
+
+ await graph.focus();
+ await graph.page.keyboard.press('Escape');
+
+ await expect(graph).not.toContainClass(CursorCSS.NODE_CREATION);
+ });
+
+ test.afterEach(async ({ integration }) => {
+ await integration?.close();
+ });
+});
diff --git a/examples/workflow-test/tests/features/tools/edge-edit/edge-edit-tool.spec.ts b/examples/workflow-test/tests/features/tools/edge-edit/edge-edit-tool.spec.ts
new file mode 100644
index 0000000..1ef0555
--- /dev/null
+++ b/examples/workflow-test/tests/features/tools/edge-edit/edge-edit-tool.spec.ts
@@ -0,0 +1,107 @@
+/********************************************************************************
+ * Copyright (c) 2023 Business Informatics Group (TU Wien) 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 { expect, test } from '@eclipse-glsp/glsp-playwright';
+import { WorkflowApp } from '../../../../src/app/workflow-app';
+import { Edge } from '../../../../src/graph/elements/edge.po';
+import { TaskAutomated } from '../../../../src/graph/elements/task-automated.po';
+import { TaskManual } from '../../../../src/graph/elements/task-manual.po';
+import { WorkflowGraph } from '../../../../src/graph/workflow.graph';
+import { TaskAutomatedNodes, TaskManualNodes } from '../../../nodes';
+
+test.describe('The edge edit tool', () => {
+ let app: WorkflowApp;
+ let graph: WorkflowGraph;
+
+ test.beforeEach(async ({ integration }) => {
+ app = new WorkflowApp({
+ type: 'integration',
+ integration
+ });
+ graph = app.graph;
+ });
+
+ test('should allow reconnecting edges in the graph', async () => {
+ const source = await graph.getNodeByLabel(TaskManualNodes.pushLabel, TaskManual);
+ const newSource = await graph.getNodeByLabel(TaskAutomatedNodes.chktpLabel, TaskAutomated);
+ const newTarget = await graph.getNodeByLabel(TaskAutomatedNodes.chkwtLabel, TaskAutomated);
+
+ const edge = await source.edges().outgoingEdgeOfType(Edge);
+ await edge.reconnectTarget(newTarget);
+ expect(await edge.targetId()).toBe(await newTarget.idAttr());
+
+ await edge.reconnectSource(newSource);
+ expect(await edge.sourceId()).toBe(await newSource.idAttr());
+ });
+
+ test('should allow moving the routing points in the graph', async () => {
+ const source = await graph.getNodeByLabel(TaskManualNodes.pushLabel, TaskManual);
+
+ const edge = await source.edges().outgoingEdgeOfType(Edge);
+ const routingPoints = edge.routingPoints();
+ await routingPoints.enable();
+ let points = await routingPoints.points();
+ const currentPointsLength = points.length;
+
+ let volatilePoints = await routingPoints.volatilePoints();
+ expect(volatilePoints).toHaveLength(1);
+
+ const volatilePoint = volatilePoints[0];
+ await volatilePoint.dragToRelativePosition({ x: 50, y: 50 });
+
+ points = await routingPoints.points();
+ expect(points).toHaveLength(currentPointsLength + 1);
+
+ volatilePoints = await routingPoints.volatilePoints();
+ expect(volatilePoints).toHaveLength(2);
+ });
+
+ test('should allow removing the routing points in the graph by realigning', async () => {
+ const source = await graph.getNodeByLabel(TaskManualNodes.pushLabel, TaskManual);
+
+ const edge = await source.edges().outgoingEdgeOfType(Edge);
+ const routingPoints = edge.routingPoints();
+ await routingPoints.enable();
+ let points = await routingPoints.points();
+ const currentPointsLength = points.length;
+
+ let volatilePoints = await routingPoints.volatilePoints();
+ expect(volatilePoints).toHaveLength(1);
+
+ // Middle one
+ await volatilePoints[0].dragToRelativePosition({ x: 0, y: 50 });
+
+ points = await routingPoints.points();
+ expect(points).toHaveLength(currentPointsLength + 1);
+
+ volatilePoints = await routingPoints.volatilePoints();
+ expect(volatilePoints).toHaveLength(2);
+
+ // Junction
+ const junction = points.find(p => p.lastSnapshot?.kind === 'junction')!;
+ await junction.dragToRelativePosition({ x: 20, y: -40 });
+ await junction.waitForHidden();
+
+ points = await routingPoints.points();
+ expect(points).toHaveLength(currentPointsLength);
+
+ volatilePoints = await routingPoints.volatilePoints();
+ expect(volatilePoints).toHaveLength(1);
+ });
+
+ test.afterEach(async ({ integration }) => {
+ await integration?.close();
+ });
+});
diff --git a/examples/workflow-test/tests/features/tools/node-creation/node-creation-tool.spec.ts b/examples/workflow-test/tests/features/tools/node-creation/node-creation-tool.spec.ts
new file mode 100644
index 0000000..e68d745
--- /dev/null
+++ b/examples/workflow-test/tests/features/tools/node-creation/node-creation-tool.spec.ts
@@ -0,0 +1,136 @@
+/********************************************************************************
+ * Copyright (c) 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 { CursorCSS } from '@eclipse-glsp/client/lib/base/feedback/css-feedback';
+import { expect, test } from '@eclipse-glsp/glsp-playwright';
+import { ServerVariable } from '@eclipse-glsp/glsp-playwright/src/test';
+import { WorkflowApp } from '../../../../src/app/workflow-app';
+import { WorkflowToolPalette } from '../../../../src/features/tool-palette/workflow-tool-palette';
+import { Category } from '../../../../src/graph/elements/category.po';
+import { TaskManual } from '../../../../src/graph/elements/task-manual.po';
+import { WorkflowGraph } from '../../../../src/graph/workflow.graph';
+import { CategoryNodes, TaskManualNodes } from '../../../nodes';
+
+test.describe('The node creation tool', () => {
+ let app: WorkflowApp;
+ let graph: WorkflowGraph;
+ let toolPalette: WorkflowToolPalette;
+
+ let taskManualCreatedLabel: ServerVariable;
+ let categoryCreatedLabel: ServerVariable;
+
+ test.beforeEach(async ({ integration, glspServer }) => {
+ app = new WorkflowApp({
+ type: 'integration',
+ integration
+ });
+ graph = app.graph;
+ toolPalette = app.toolPalette;
+ taskManualCreatedLabel = TaskManualNodes.createdLabel(glspServer);
+ categoryCreatedLabel = CategoryNodes.createdLabel(glspServer);
+ });
+
+ test('should allow creating new nodes in the graph', async () => {
+ const task = await graph.getNodeByLabel(TaskManualNodes.pushLabel, TaskManual);
+ const nodes = await graph.waitForCreationOfType(TaskManual, async () => {
+ const paletteItem = await toolPalette.content.toolElement('Nodes', 'Manual Task');
+ await paletteItem.click();
+
+ await expect(graph).toContainClass(CursorCSS.NODE_CREATION);
+
+ const taskBounds = await task.bounds();
+ await taskBounds.position('bottom_left').moveRelative(-50, 0).click();
+ });
+ expect(nodes).toHaveLength(1);
+ await graph.focus();
+
+ const newTask = nodes[0];
+
+ const label = await newTask.children.label();
+ expect(await label.textContent()).toBe(taskManualCreatedLabel.get());
+ });
+
+ test('should allow creating new child nodes', async () => {
+ // Create a new category
+ let nodes = await graph.waitForCreationOfType(Category, async () => {
+ const paletteItem = await toolPalette.content.toolElement('Nodes', 'Category');
+ await paletteItem.click();
+
+ await expect(graph).toContainClass(CursorCSS.NODE_CREATION);
+ const node = await graph.getNodeByLabel(TaskManualNodes.pushLabel, TaskManual);
+ const bounds = await node.bounds();
+ await bounds.position('bottom_left').moveRelative(0, 60).click();
+ });
+ expect(nodes).toHaveLength(1);
+ await graph.focus();
+
+ const newCategory = nodes[0];
+
+ let label = await newCategory.children.label();
+ expect(await label.textContent()).toBe(categoryCreatedLabel.get());
+
+ // Create a new task inside the category
+ nodes = await graph.waitForCreationOfType(TaskManual, async () => {
+ const paletteItem = await toolPalette.content.toolElement('Nodes', 'Manual Task');
+ await paletteItem.click();
+
+ await expect(graph).toContainClass(CursorCSS.NODE_CREATION);
+
+ const bounds = await newCategory.bounds();
+ await bounds.position('middle_right').click();
+ });
+ expect(nodes).toHaveLength(1);
+ await graph.focus();
+
+ const newTask = nodes[0];
+
+ label = await newTask.children.label();
+ expect(await label.textContent()).toBe(taskManualCreatedLabel.get());
+ // Check if it is inside the category
+ const children = await newCategory.children.allOfType(TaskManual);
+ expect(children).toHaveLength(1);
+ expect(await children[0].idAttr()).toBe(await newTask.idAttr());
+ });
+
+ test('should prevent invalid combinations', async () => {
+ const paletteItem = await toolPalette.content.toolElement('Nodes', 'Manual Task');
+ await paletteItem.click();
+
+ await expect(graph).toContainClass(CursorCSS.NODE_CREATION);
+
+ const task = await graph.getNodeByLabel(TaskManualNodes.pushLabel, TaskManual);
+ const bounds = await task.bounds();
+ await bounds.position('middle_right').move();
+
+ await expect(graph).not.toContainClass(CursorCSS.NODE_CREATION);
+ await expect(graph).toContainClass(CursorCSS.OPERATION_NOT_ALLOWED);
+ });
+
+ test('should allow to cancel the operation', async () => {
+ const paletteItem = await toolPalette.content.toolElement('Nodes', 'Manual Task');
+ await paletteItem.click();
+
+ await expect(graph).toContainClass(CursorCSS.NODE_CREATION);
+
+ await graph.focus();
+ await graph.page.keyboard.press('Escape');
+
+ await expect(graph).not.toContainClass(CursorCSS.NODE_CREATION);
+ });
+
+ test.afterEach(async ({ integration }) => {
+ await integration?.close();
+ });
+});
diff --git a/examples/workflow-test/tests/features/undo-redo/undo-redo.spec.ts b/examples/workflow-test/tests/features/undo-redo/undo-redo.spec.ts
new file mode 100644
index 0000000..c8a7be4
--- /dev/null
+++ b/examples/workflow-test/tests/features/undo-redo/undo-redo.spec.ts
@@ -0,0 +1,60 @@
+/********************************************************************************
+ * Copyright (c) 2023 Business Informatics Group (TU Wien) 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 { expect, test } from '@eclipse-glsp/glsp-playwright';
+import { provideUndoRedoTriggerVariable, UndoRedoTrigger } from '@eclipse-glsp/glsp-playwright/src/glsp';
+import { skipIntegration } from '@eclipse-glsp/glsp-playwright/src/test';
+import { WorkflowApp } from '../../../src/app/workflow-app';
+import { TaskManual } from '../../../src/graph/elements/task-manual.po';
+import { WorkflowGraph } from '../../../src/graph/workflow.graph';
+import { TaskManualNodes } from '../../nodes';
+
+test.describe('The undo redo trigger', () => {
+ let app: WorkflowApp;
+ let graph: WorkflowGraph;
+ let trigger: UndoRedoTrigger;
+
+ test.beforeEach(async ({ integration }) => {
+ app = new WorkflowApp({
+ type: 'integration',
+ integration
+ });
+ graph = app.graph;
+ trigger = provideUndoRedoTriggerVariable(integration, app).get();
+ });
+
+ test.skip(({ integrationOptions }) => skipIntegration(integrationOptions, 'VSCode'), 'TODO: Keyboard event not handled in VSCode');
+
+ test('should allow undo and redo', async () => {
+ await expect(graph).toContainElement({ type: TaskManual, query: { label: TaskManualNodes.pushLabel } });
+
+ const node = await graph.getNodeByLabel(TaskManualNodes.pushLabel, TaskManual);
+ await node.delete();
+
+ await expect(graph).not.toContainElement({ type: TaskManual, query: { label: TaskManualNodes.pushLabel } });
+
+ await trigger.undo();
+
+ await expect(graph).toContainElement({ type: TaskManual, query: { label: TaskManualNodes.pushLabel } });
+
+ await trigger.redo();
+
+ await expect(graph).not.toContainElement({ type: TaskManual, query: { label: TaskManualNodes.pushLabel } });
+ });
+
+ test.afterEach(async ({ integration }) => {
+ await integration?.close();
+ });
+});
diff --git a/examples/workflow-test/tests/features/validation/marker-navigator.spec.ts b/examples/workflow-test/tests/features/validation/marker-navigator.spec.ts
index c27ffee..e644c80 100644
--- a/examples/workflow-test/tests/features/validation/marker-navigator.spec.ts
+++ b/examples/workflow-test/tests/features/validation/marker-navigator.spec.ts
@@ -13,19 +13,19 @@
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
-import { MarkerNavigator, StandaloneMarkerNavigator, TheiaMarkerNavigator, expect, test } from '@eclipse-glsp/glsp-playwright/';
-import { skipIntegration } from '@eclipse-glsp/glsp-playwright/src/test';
-import { IntegrationVariable } from '@eclipse-glsp/glsp-playwright/src/test/dynamic-variable';
+import { MarkerNavigator, expect, skipIntegration, test } from '@eclipse-glsp/glsp-playwright';
+import { provideMarkerNavigatorVariable } from '@eclipse-glsp/glsp-playwright/src/glsp';
import { WorkflowApp } from '../../../src/app/workflow-app';
import { TaskAutomated } from '../../../src/graph/elements/task-automated.po';
import { WorkflowGraph } from '../../../src/graph/workflow.graph';
+import { TaskAutomatedNodes } from '../../nodes';
-const element1 = 'ChkWt';
-const element2 = 'WtOK';
-const element3 = 'Brew';
-const element4 = 'KeepTp';
-const element5 = 'ChkTp';
-const element6 = 'PreHeat';
+const element1 = TaskAutomatedNodes.chkwtLabel;
+const element2 = TaskAutomatedNodes.wtokLabel;
+const element3 = TaskAutomatedNodes.brewLabel;
+const element4 = TaskAutomatedNodes.keepTpLabel;
+const element5 = TaskAutomatedNodes.chktpLabel;
+const element6 = TaskAutomatedNodes.preheatLabel;
const forwardOrder = [element1, element2, element3, element4, element5, element6, element1];
const backwardOrder = [element1, element6, element5, element4, element3, element2, element1, element6];
@@ -41,16 +41,7 @@ test.describe('The marker navigator', () => {
integration
});
graph = app.graph;
- const navigatorProvider = new IntegrationVariable({
- value: {
- Standalone: new StandaloneMarkerNavigator(app),
- Theia: new TheiaMarkerNavigator(app)
- },
- integration
- });
-
- navigator = navigatorProvider.get();
-
+ navigator = provideMarkerNavigatorVariable(integration, app).get();
await navigator.trigger();
});
@@ -61,7 +52,6 @@ test.describe('The marker navigator', () => {
test.skip(skipIntegration(integrationOptions, 'VSCode'), 'Not supported');
await navigator.navigateForward();
- await app.page.pause();
await expect(graph).toHaveSelected({
type: TaskAutomated,
elements: [await graph.getNodeByLabel(element1, TaskAutomated)]
diff --git a/examples/workflow-test/tests/nodes.ts b/examples/workflow-test/tests/nodes.ts
new file mode 100644
index 0000000..0db169a
--- /dev/null
+++ b/examples/workflow-test/tests/nodes.ts
@@ -0,0 +1,52 @@
+/********************************************************************************
+ * Copyright (c) 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 { GLSPServer } from '@eclipse-glsp/glsp-playwright/src/glsp-server';
+import { ServerVariable } from '@eclipse-glsp/glsp-playwright/src/test';
+import { GLSP_SERVER_TYPE_JAVA, GLSP_SERVER_TYPE_NODE } from '../src/server';
+
+export namespace TaskAutomatedNodes {
+ export const wtokLabel = 'WtOK';
+ export const chkwtLabel = 'ChkWt';
+ export const chktpLabel = 'ChkTp';
+ export const brewLabel = 'Brew';
+ export const keepTpLabel = 'KeepTp';
+ export const preheatLabel = 'PreHeat';
+}
+
+export namespace TaskManualNodes {
+ export const pushLabel = 'Push';
+ export const rflwtLabel = 'RflWt';
+
+ export function createdLabel(server: GLSPServer): ServerVariable {
+ return new ServerVariable({
+ server,
+ defaultValue: 'ManualTask8'
+ });
+ }
+}
+
+export namespace CategoryNodes {
+ export function createdLabel(server: GLSPServer): ServerVariable {
+ return new ServerVariable({
+ server,
+ value: {
+ [GLSP_SERVER_TYPE_NODE]: 'Category0',
+ [GLSP_SERVER_TYPE_JAVA]: 'Category 0'
+ }
+ });
+ }
+}
diff --git a/packages/glsp-playwright/src/extension/flows/click.flow.ts b/packages/glsp-playwright/src/extension/flows/click.flow.ts
index e6cd2f6..4a49209 100644
--- a/packages/glsp-playwright/src/extension/flows/click.flow.ts
+++ b/packages/glsp-playwright/src/extension/flows/click.flow.ts
@@ -29,7 +29,7 @@ export interface Clickable {
*
* @see {@link Locator.click}
*/
- click(options?: Parameters[0]): Promise;
+ click(options?: Parameters[0] & { dispatch?: boolean }): Promise;
/**
* Double click the element.
@@ -69,7 +69,15 @@ export function useClickableFlow>(Base: T
*
* @see {@link Locator.click}
*/
- click(options?: Parameters[0]): Promise {
+ async click(options?: Parameters[0] & { dispatch?: boolean }): Promise {
+ this.locate().dispatchEvent('mouseover', { bubbles: true });
+
+ if (options?.dispatch) {
+ this.locate().dispatchEvent('mousedown', { bubbles: true });
+ this.locate().dispatchEvent('mouseup', { bubbles: true });
+ return;
+ }
+
return this.locate().click(options);
}
diff --git a/packages/glsp-playwright/src/glsp/app/app.po.ts b/packages/glsp-playwright/src/glsp/app/app.po.ts
index 123e029..c13f0ae 100644
--- a/packages/glsp-playwright/src/glsp/app/app.po.ts
+++ b/packages/glsp-playwright/src/glsp/app/app.po.ts
@@ -21,6 +21,7 @@ import { GLSPGlobalCommandPalette } from '../features/command-palette';
import { GLSPLabelEditor } from '../features/label-editor/label-editor.po';
import { GLSPPopup } from '../features/popup/popup.po';
import { GLSPToolPalette } from '../features/tool-palette/tool-palette.po';
+import { GLSPSemanticGraph } from '../graph';
import { GLSPGraph } from '../graph/graph.po';
/**
@@ -127,7 +128,7 @@ export class GLSPApp {
}
protected createGraph(_options: GLSPAppOptions): GLSPGraph {
- return new GLSPGraph({ locator: GLSPGraph.locate(this) });
+ return new GLSPSemanticGraph({ locator: GLSPGraph.locate(this) });
}
protected createLabelEditor(_options: GLSPAppOptions): GLSPLabelEditor {
diff --git a/packages/glsp-playwright/src/glsp/features/index.ts b/packages/glsp-playwright/src/glsp/features/index.ts
index 0a1682a..f247c4b 100644
--- a/packages/glsp-playwright/src/glsp/features/index.ts
+++ b/packages/glsp-playwright/src/glsp/features/index.ts
@@ -20,4 +20,5 @@ export * from './label-editor';
export * from './popup';
export * from './routing';
export * from './tool-palette';
+export * from './undo-redo';
export * from './validation';
diff --git a/packages/glsp-playwright/src/glsp/features/label-editor/label-editor.po.ts b/packages/glsp-playwright/src/glsp/features/label-editor/label-editor.po.ts
index b0bf803..ebadbc8 100644
--- a/packages/glsp-playwright/src/glsp/features/label-editor/label-editor.po.ts
+++ b/packages/glsp-playwright/src/glsp/features/label-editor/label-editor.po.ts
@@ -16,6 +16,7 @@
import type { GLSPApp } from '~/glsp';
import type { GLSPLocator } from '~/remote';
import { Locateable } from '~/remote';
+import { expect } from '../../../test';
export interface GLSPLabelEditorOptions {
locator: GLSPLocator;
@@ -29,4 +30,9 @@ export class GLSPLabelEditor extends Locateable {
constructor(protected readonly options: GLSPLabelEditorOptions) {
super(options.locator);
}
+
+ async getWarning(): Promise {
+ await expect(this.locate()).toHaveAttribute('data-balloon');
+ return (await this.locate().getAttribute('data-balloon')) ?? undefined;
+ }
}
diff --git a/packages/glsp-playwright/src/glsp/features/reconnect/index.ts b/packages/glsp-playwright/src/glsp/features/reconnect/index.ts
new file mode 100644
index 0000000..e8f7704
--- /dev/null
+++ b/packages/glsp-playwright/src/glsp/features/reconnect/index.ts
@@ -0,0 +1,17 @@
+/********************************************************************************
+ * Copyright (c) 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
+ ********************************************************************************/
+
+export * from './reconnect.capability';
diff --git a/packages/glsp-playwright/src/glsp/features/reconnect/reconnect.capability.ts b/packages/glsp-playwright/src/glsp/features/reconnect/reconnect.capability.ts
new file mode 100644
index 0000000..bb7c64b
--- /dev/null
+++ b/packages/glsp-playwright/src/glsp/features/reconnect/reconnect.capability.ts
@@ -0,0 +1,62 @@
+/********************************************************************************
+ * Copyright (c) 2023 Business Informatics Group (TU Wien) 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 type { Capability, Clickable } from '~/extension';
+import { SVGMetadata, type PEdge, type PNode } from '~/glsp/graph';
+import type { ConstructorA } from '~/types';
+import { expect } from '../../../test';
+import { RoutingPointCapability } from '../routing';
+
+/**
+ * Elements can be resized by using the resize handles.
+ */
+export interface ReconnectCapability {
+ reconnectSource(node: PNode & Clickable): Promise;
+ reconnectTarget(node: PNode & Clickable): Promise;
+}
+
+/**
+ * The default [Extension-Provider](https://github.com/eclipse-glsp/glsp-playwright/docs/concepts/extension.md)
+ * for the {@link ReconnectCapability}.
+ *
+ * @param Base Base class that should be extended
+ * @returns Extended base class with the {@link ReconnectCapability}
+ */
+export function useReconnectCapability>(
+ Base: TBase
+): Capability {
+ abstract class Mixin extends Base implements ReconnectCapability {
+ async reconnectSource(node: PNode & Clickable): Promise {
+ await this.reconnect(node, 0);
+ await expect(this.locate()).toHaveAttribute(SVGMetadata.Edge.sourceId, await node.idAttr());
+ }
+
+ async reconnectTarget(node: PNode & Clickable): Promise {
+ await this.reconnect(node, -1);
+ await expect(this.locate()).toHaveAttribute(SVGMetadata.Edge.targetId, await node.idAttr());
+ }
+
+ private async reconnect(node: PNode & Clickable, index: number): Promise {
+ const routingPoints = this.routingPoints();
+ await routingPoints.enable();
+ const points = await routingPoints.points();
+ await points.at(index)!.dragTo(node.locate(), { force: true });
+ await node.click();
+ await expect(node).toBeSelected();
+ }
+ }
+
+ return Mixin;
+}
diff --git a/packages/glsp-playwright/src/glsp/features/routing/routing-point.po.ts b/packages/glsp-playwright/src/glsp/features/routing/routing-point.po.ts
index eeeff24..5ae0f89 100644
--- a/packages/glsp-playwright/src/glsp/features/routing/routing-point.po.ts
+++ b/packages/glsp-playwright/src/glsp/features/routing/routing-point.po.ts
@@ -16,7 +16,7 @@
import type { Locator } from '@playwright/test';
import type { AutoPrepareOptions, AutoWaitOptions } from '~/extension';
import { Clickable, Mix, useDraggableFlow } from '~/extension';
-import { ModelElementMetadata, PEdge, PMetadata, PModelElement, PModelElementData, SVGMetadata } from '~/glsp/graph';
+import { ModelElementMetadata, PEdge, PMetadata, PModelElement, PModelElementData, PModelElementSnapshot, SVGMetadata } from '~/glsp/graph';
import type { GLSPLocator } from '~/remote';
import type { Position } from '~/types';
import { definedAttr, definedGLSPAttr } from '~/utils/ts.utils';
@@ -41,6 +41,12 @@ export class RoutingPoints {
await this.volatilePointsLocator.nth(0).waitFor({ state: 'visible', timeout: options?.timeout });
}
+ async enable(): Promise {
+ await this.element.graph.waitForCreation(PMetadata.getType(RoutingPoint), async () => {
+ await this.element.click({ dispatch: true });
+ });
+ }
+
async points(options?: AutoWaitOptions): Promise {
await this.autoWait(options);
@@ -49,7 +55,9 @@ export class RoutingPoints {
for await (const childLocator of await this.pointsLocator.all()) {
const id = await definedAttr(childLocator, 'id');
- elements.push(new RoutingPoint(this.element.locator.child(`#${id}`), this));
+ const routingPoint = new RoutingPoint(this.element.locator.child(`#${id}`), this);
+ await routingPoint.snapshot();
+ elements.push(routingPoint);
}
return elements;
@@ -63,16 +71,23 @@ export class RoutingPoints {
for await (const childLocator of await this.volatilePointsLocator.all()) {
const id = await definedAttr(childLocator, 'id');
- elements.push(new RoutingPoint(this.element.locator.child(`#${id}`), this));
+ const routingPoint = new RoutingPoint(this.element.locator.child(`#${id}`), this);
+ await routingPoint.snapshot();
+ elements.push(routingPoint);
}
return elements;
}
}
+export interface RoutingPointSnapshot extends PModelElementSnapshot {
+ kind: RoutingPointKind;
+}
+
const BaseRoutingPointMixin = Mix(PModelElement).flow(useDraggableFlow).build();
export abstract class BaseRoutingPoint extends BaseRoutingPointMixin {
readonly routingPoints;
+ override lastSnapshot?: RoutingPointSnapshot | undefined;
constructor(data: PModelElementData & { routingPoints: RoutingPoints }) {
super(data);
@@ -80,6 +95,18 @@ export abstract class BaseRoutingPoint extends BaseRoutingPointMixin {
this.routingPoints = data.routingPoints;
}
+ override async snapshot(): Promise {
+ this.lastSnapshot = {
+ snapshotTime: new Date().getTime(),
+ class: await this.classAttr(),
+ id: await this.idAttr(),
+ type: await this.typeAttr(),
+ kind: await this.dataKindAttr()
+ } as RoutingPointSnapshot;
+
+ return this.lastSnapshot;
+ }
+
protected async autoPrepare(options?: AutoPrepareOptions): Promise {
if (options?.prepare === false) {
return;
@@ -99,14 +126,12 @@ export abstract class BaseRoutingPoint extends BaseRoutingPointMixin {
await this.autoPrepare(options);
await super.dragToAbsolutePosition(position);
- await this.routingPoints.element.graph.focus();
}
override async dragToRelativePosition(position: Position, options?: AutoPrepareOptions): Promise {
await this.autoPrepare(options);
await super.dragToRelativePosition(position);
- await this.routingPoints.element.graph.focus();
}
override async dragTo(
@@ -122,8 +147,7 @@ export abstract class BaseRoutingPoint extends BaseRoutingPointMixin {
): Promise {
await this.autoPrepare();
- await this.dragTo(target, options);
- await this.routingPoints.element.graph.focus();
+ await super.dragTo(target, options);
}
}
diff --git a/packages/glsp-playwright/src/glsp/features/undo-redo/index.ts b/packages/glsp-playwright/src/glsp/features/undo-redo/index.ts
new file mode 100644
index 0000000..b913ed9
--- /dev/null
+++ b/packages/glsp-playwright/src/glsp/features/undo-redo/index.ts
@@ -0,0 +1,18 @@
+/********************************************************************************
+ * Copyright (c) 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
+ ********************************************************************************/
+
+export * from './undo-redo';
+export * from './undo-redo.integration';
diff --git a/packages/glsp-playwright/src/glsp/features/undo-redo/undo-redo.integration.ts b/packages/glsp-playwright/src/glsp/features/undo-redo/undo-redo.integration.ts
new file mode 100644
index 0000000..a450108
--- /dev/null
+++ b/packages/glsp-playwright/src/glsp/features/undo-redo/undo-redo.integration.ts
@@ -0,0 +1,45 @@
+/********************************************************************************
+ * Copyright (c) 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 type { Integration } from '~/integration';
+import { IntegrationVariable } from '~/test';
+import type { GLSPApp } from '../../app';
+import { UndoRedoTrigger } from './undo-redo';
+
+export class StandaloneUndoRedoTrigger extends UndoRedoTrigger {
+ protected readonly undoKey = 'ControlOrMeta+z';
+ protected readonly redoKey = 'ControlOrMeta+Shift+z';
+}
+
+export class TheiaUndoRedoTrigger extends UndoRedoTrigger {
+ protected readonly undoKey = 'ControlOrMeta+z';
+ protected readonly redoKey = 'ControlOrMeta+Shift+z';
+}
+
+export class VscodeUndoRedoTrigger extends UndoRedoTrigger {
+ protected readonly undoKey = 'ControlOrMeta+z';
+ protected readonly redoKey = 'ControlOrMeta+Shift+z';
+}
+
+export const provideUndoRedoTriggerVariable = (integration: Integration, app: GLSPApp): IntegrationVariable =>
+ new IntegrationVariable({
+ value: {
+ Standalone: new StandaloneUndoRedoTrigger(app),
+ Theia: new TheiaUndoRedoTrigger(app),
+ VSCode: new VscodeUndoRedoTrigger(app)
+ },
+ integration
+ });
diff --git a/packages/glsp-playwright/src/glsp/features/undo-redo/undo-redo.ts b/packages/glsp-playwright/src/glsp/features/undo-redo/undo-redo.ts
new file mode 100644
index 0000000..c213fe9
--- /dev/null
+++ b/packages/glsp-playwright/src/glsp/features/undo-redo/undo-redo.ts
@@ -0,0 +1,41 @@
+/********************************************************************************
+ * Copyright (c) 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 { GLSPApp } from '../../app';
+import { GLSPGraph } from '../../graph';
+
+export abstract class UndoRedoTrigger {
+ protected abstract undoKey: string;
+ protected abstract redoKey: string;
+
+ protected get graph(): GLSPGraph {
+ return this.app.graph;
+ }
+
+ constructor(protected readonly app: GLSPApp) {}
+
+ async undo(): Promise {
+ await this.do(this.undoKey);
+ }
+
+ async redo(): Promise {
+ await this.do(this.redoKey);
+ }
+
+ protected async do(key: string): Promise {
+ await this.app.graph.locate().press(key);
+ }
+}
diff --git a/packages/glsp-playwright/src/glsp/features/validation/marker-navigator.integration.ts b/packages/glsp-playwright/src/glsp/features/validation/marker-navigator.integration.ts
index 72e8342..c603abf 100644
--- a/packages/glsp-playwright/src/glsp/features/validation/marker-navigator.integration.ts
+++ b/packages/glsp-playwright/src/glsp/features/validation/marker-navigator.integration.ts
@@ -14,6 +14,9 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
+import type { Integration } from '~/integration';
+import { IntegrationVariable } from '~/test';
+import type { GLSPSemanticApp } from '../../app';
import { MarkerNavigator } from './marker-navigator';
export class StandaloneMarkerNavigator extends MarkerNavigator {
@@ -25,3 +28,12 @@ export class TheiaMarkerNavigator extends MarkerNavigator {
protected readonly forwardKey = 'F8';
protected readonly backwardKey = 'Shift+F8';
}
+
+export const provideMarkerNavigatorVariable = (integration: Integration, app: GLSPSemanticApp): IntegrationVariable =>
+ new IntegrationVariable({
+ value: {
+ Standalone: new StandaloneMarkerNavigator(app),
+ Theia: new TheiaMarkerNavigator(app)
+ },
+ integration
+ });
diff --git a/packages/glsp-playwright/src/glsp/graph/decorators/decorator.ts b/packages/glsp-playwright/src/glsp/graph/decorators/decorator.ts
index ff82b0c..31d5e51 100644
--- a/packages/glsp-playwright/src/glsp/graph/decorators/decorator.ts
+++ b/packages/glsp-playwright/src/glsp/graph/decorators/decorator.ts
@@ -37,33 +37,18 @@ export namespace PMetadata {
export function assert(target: PEdgeConstructor): EdgeMetadata;
export function assert(target: PModelElementConstructor): ModelElementMetadata;
export function assert(target: ConstructorT): ModelElementMetadata;
- export function assert(target: ConstructorT): ModelElementMetadata {
+ export function assert(target: PNode): NodeMetadata;
+ export function assert(target: PEdge): EdgeMetadata;
+ export function assert(target: PModelElement): ModelElementMetadata;
+ export function assert(target: object): ModelElementMetadata;
+ export function assert(target: object): ModelElementMetadata {
if (!Reflect.hasMetadata(metadataKey, target)) {
- throw Error(`Provided target "${target.name ?? target}" has no metadata. Did you use the class decorator?`);
+ throw Error(`Provided target "${target}" has no metadata. Did you use the class decorator?`);
}
return Reflect.getMetadata(metadataKey, target);
}
- /**
- * Returns the `metadata` of a target.
- * It throws an error if the metadata does not exist.
- *
- * @param target Object with the `metadata`
- * @returns Metadata of the page object
- */
- export function assertOwn(target: PNode): NodeMetadata;
- export function assertOwn(target: PEdge): EdgeMetadata;
- export function assertOwn(target: PModelElement): ModelElementMetadata;
- export function assertOwn(target: object): ModelElementMetadata;
- export function assertOwn(target: object): ModelElementMetadata {
- if (!Reflect.hasOwnMetadata(metadataKey, target)) {
- throw Error(`Provided target "${target.constructor.name ?? target}" has no own metadata. Did you use the class decorator?`);
- }
-
- return Reflect.getOwnMetadata(metadataKey, target);
- }
-
/**
* Defines the provided `metadata` on the target.
*
diff --git a/packages/glsp-playwright/src/glsp/graph/elements/element.ts b/packages/glsp-playwright/src/glsp/graph/elements/element.ts
index da1ee44..d43a103 100644
--- a/packages/glsp-playwright/src/glsp/graph/elements/element.ts
+++ b/packages/glsp-playwright/src/glsp/graph/elements/element.ts
@@ -100,7 +100,7 @@ export class PModelElement extends Locateable {
constructor(data: PModelElementData) {
super(data.locator);
this.graph = this.app.graph;
- this._metadata = PMetadata.assertOwn(this.constructor);
+ this._metadata = PMetadata.assert(this.constructor);
}
async snapshot(): Promise {
diff --git a/packages/glsp-playwright/src/glsp/graph/elements/node.ts b/packages/glsp-playwright/src/glsp/graph/elements/node.ts
index c24b8e9..1f19461 100644
--- a/packages/glsp-playwright/src/glsp/graph/elements/node.ts
+++ b/packages/glsp-playwright/src/glsp/graph/elements/node.ts
@@ -63,9 +63,9 @@ export class ChildrenAccessor {
async allOfType(
constructor: PModelElementConstructor,
- options?: { deep?: boolean }
+ options: { deep?: boolean } = { deep: true }
): Promise {
- const childrenLocator = options?.deep
+ const childrenLocator = options.deep
? this.parent.locate().locator(SVGMetadataUtils.typeAttrOf(constructor))
: this.parent.locate().locator(`> ${SVGMetadataUtils.typeAttrOf(constructor)}`);
@@ -125,7 +125,7 @@ export class EdgesAccessor {
return graph.getEdgesOfType(constructor, {
...options,
- sourceSelectorOrLocator: `#${sourceId}`,
+ sourceId,
sourceConstructor: this.sourceConstructor
}) as any;
}
@@ -150,7 +150,7 @@ export class EdgesAccessor {
return graph.getEdgesOfType(constructor, {
...options,
- targetSelectorOrLocator: `#${sourceId}`,
+ targetId: sourceId,
targetConstructor: this.sourceConstructor
}) as any;
}
diff --git a/packages/glsp-playwright/src/glsp/graph/graph-semantic.po.ts b/packages/glsp-playwright/src/glsp/graph/graph-semantic.po.ts
index fc58c33..9746a44 100644
--- a/packages/glsp-playwright/src/glsp/graph/graph-semantic.po.ts
+++ b/packages/glsp-playwright/src/glsp/graph/graph-semantic.po.ts
@@ -14,6 +14,7 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
import { expect } from '@playwright/test';
+import { GraphConstructorOptions, SVGMetadataUtils } from '.';
import { Selectable } from '../../extension';
import { PLabelledElement } from '../../extension/model/labelled/labelled-element.model';
import { PEdge, PEdgeConstructor, PModelElement, PModelElementConstructor, PNode, PNodeConstructor } from './elements';
@@ -26,9 +27,10 @@ import { GLSPGraph } from './graph.po';
export class GLSPSemanticGraph extends GLSPGraph {
async getNodeByLabel(
label: string,
- constructor: PNodeConstructor
+ constructor: PNodeConstructor,
+ options?: GraphConstructorOptions
): Promise {
- const nodes = await this.getNodesByLabel(label, constructor);
+ const nodes = await this.getNodesByLabel(label, constructor, options);
expect(nodes).toHaveLength(1);
@@ -37,9 +39,10 @@ export class GLSPSemanticGraph extends GLSPGraph {
async getNodesByLabel(
label: string,
- constructor: PNodeConstructor
+ constructor: PNodeConstructor,
+ options?: GraphConstructorOptions
): Promise {
- const nodes = await this.getNodesOfType(constructor);
+ const nodes = await this.getNodes(`${SVGMetadataUtils.typeAttrOf(constructor)}:has-text("${label}")`, constructor, options);
const elements: TElement[] = [];
for (const node of nodes) {
diff --git a/packages/glsp-playwright/src/glsp/graph/graph.po.ts b/packages/glsp-playwright/src/glsp/graph/graph.po.ts
index d3fa421..3868303 100644
--- a/packages/glsp-playwright/src/glsp/graph/graph.po.ts
+++ b/packages/glsp-playwright/src/glsp/graph/graph.po.ts
@@ -16,14 +16,13 @@
import type { Locator } from '@playwright/test';
import type { GLSPApp } from '~/glsp';
import { asLocator, type GLSPLocator } from '~/remote';
-import { Locateable } from '~/remote/locateable';
-import { definedAttr } from '~/utils/ts.utils';
-import { PMetadata } from './decorators';
+import { definedAttr, isUndefinedOrValue } from '~/utils/ts.utils';
+import { ModelElementMetadata, PMetadata } from './decorators';
import { assertEqualType, createTypedEdgeProxy, getPModelElementConstructorOfType } from './elements';
import { isPEdgeConstructor, PEdge, PEdgeConstructor } from './elements/edge';
import { isEqualLocatorType, PModelElement, PModelElementConstructor } from './elements/element';
import { isPNodeConstructor, PNode, PNodeConstructor } from './elements/node';
-import type { EdgeConstructorOptions, EdgeSearchOptions, ElementQuery, TypedEdge } from './graph.type';
+import type { EdgeConstructorOptions, EdgeSearchOptions, ElementQuery, GraphConstructorOptions, TypedEdge } from './graph.type';
import { waitForElementChanges, waitForElementIncrease } from './graph.wait';
import { SVGMetadata, SVGMetadataUtils } from './svg-metadata-api';
@@ -41,33 +40,45 @@ export interface GLSPGraphOptions {
* It works directy with the underlying DOM by using the selector, {@link Locator},
* or constructor (e.g., {@link PModelElementConstructor}) of an element.
*/
-export class GLSPGraph extends Locateable {
+@ModelElementMetadata({
+ type: 'graph'
+})
+export class GLSPGraph extends PModelElement {
static locate(app: GLSPApp): GLSPLocator {
return app.locator.child(SVGMetadataUtils.typeAttrOf('graph'));
}
constructor(protected readonly options: GLSPGraphOptions) {
- super(options.locator);
+ super({
+ locator: options.locator
+ });
}
async getModelElement(
selectorOrLocator: string | Locator,
- constructor: PModelElementConstructor
+ constructor: PModelElementConstructor,
+ options?: GraphConstructorOptions
): Promise {
const locator = asLocator(selectorOrLocator, selector => this.locator.child(selector).locate());
const element = new constructor({ locator: this.locator.override(locator) });
- await assertEqualType(element);
- await element.snapshot();
+ if (options === undefined || isUndefinedOrValue(options.assert, true)) {
+ await assertEqualType(element);
+ await element.snapshot();
+ }
return element;
}
- async getModelElementsOfType(constructor: PModelElementConstructor): Promise {
- return this.getModelElements(this.locate().locator(SVGMetadataUtils.typeAttrOf(constructor)), constructor);
+ async getModelElementsOfType(
+ constructor: PModelElementConstructor,
+ options?: GraphConstructorOptions
+ ): Promise {
+ return this.getModelElements(this.locate().locator(SVGMetadataUtils.typeAttrOf(constructor)), constructor, options);
}
async getModelElements(
selectorOrLocator: string | Locator,
- constructor: PModelElementConstructor
+ constructor: PModelElementConstructor,
+ options?: GraphConstructorOptions
): Promise {
const locator = asLocator(selectorOrLocator, selector => this.locator.child(selector).locate());
const elements: TElement[] = [];
@@ -76,7 +87,7 @@ export class GLSPGraph extends Locateable {
if ((await childLocator.count()) > 0) {
const id = await childLocator.getAttribute('id');
if (id !== null && (await isEqualLocatorType(childLocator, constructor))) {
- elements.push(await this.getModelElement(`#${id}`, constructor));
+ elements.push(await this.getModelElement(`#${id}`, constructor, options));
}
}
}
@@ -88,22 +99,32 @@ export class GLSPGraph extends Locateable {
return this.getModelElements(`[${SVGMetadata.type}]`, PModelElement);
}
- async getNode(selectorOrLocator: string | Locator, constructor: PNodeConstructor): Promise {
+ async getNode(
+ selectorOrLocator: string | Locator,
+ constructor: PNodeConstructor,
+ options?: GraphConstructorOptions
+ ): Promise {
const locator = asLocator(selectorOrLocator, selector => this.locator.child(selector).locate());
const element = new constructor({
locator: this.locator.override(locator.and(this.locate().locator(SVGMetadataUtils.typeAttrOf(constructor))))
});
- await assertEqualType(element);
+ if (options === undefined || isUndefinedOrValue(options.assert, true)) {
+ await assertEqualType(element);
+ }
return element;
}
- async getNodesOfType(constructor: PNodeConstructor): Promise {
- return this.getNodes(this.locate().locator(SVGMetadataUtils.typeAttrOf(constructor)), constructor);
+ async getNodesOfType(
+ constructor: PNodeConstructor,
+ options?: GraphConstructorOptions
+ ): Promise {
+ return this.getNodes(this.locate().locator(SVGMetadataUtils.typeAttrOf(constructor)), constructor, options);
}
async getNodes(
selectorOrLocator: string | Locator,
- constructor: PNodeConstructor
+ constructor: PNodeConstructor,
+ options?: GraphConstructorOptions
): Promise {
const locator = asLocator(selectorOrLocator, selector => this.locator.child(selector).locate());
const elements: TElement[] = [];
@@ -112,7 +133,7 @@ export class GLSPGraph extends Locateable {
if ((await childLocator.count()) > 0) {
const id = await childLocator.getAttribute('id');
if (id !== null && (await isEqualLocatorType(childLocator, constructor))) {
- elements.push(await this.getNode(`#${id}`, constructor));
+ elements.push(await this.getNode(`#${id}`, constructor, options));
}
}
}
@@ -139,7 +160,14 @@ export class GLSPGraph extends Locateable {
): Promise[]> {
const elements: TypedEdge[] = [];
- for await (const locator of await this.locate().locator(SVGMetadataUtils.typeAttrOf(constructor)).all()) {
+ let query = SVGMetadataUtils.typeAttrOf(constructor);
+ if (options?.sourceId) {
+ query += `[${SVGMetadata.Edge.sourceId}="${options.sourceId}"]`;
+ } else if (options?.targetId) {
+ query += `[${SVGMetadata.Edge.targetId}="${options.targetId}"]`;
+ }
+
+ for await (const locator of await this.locate().locator(query).all()) {
const id = await locator.getAttribute('id');
if (id !== null && (await isEqualLocatorType(locator, constructor))) {
const element = await this.getEdge(`#${id}`, constructor, options);
@@ -192,7 +220,6 @@ export class GLSPGraph extends Locateable {
*/
async focus(): Promise {
await this.locate().click();
- await this.locate().focus();
}
async waitForCreationOfType(
diff --git a/packages/glsp-playwright/src/glsp/graph/graph.type.ts b/packages/glsp-playwright/src/glsp/graph/graph.type.ts
index f7b66c4..d943079 100644
--- a/packages/glsp-playwright/src/glsp/graph/graph.type.ts
+++ b/packages/glsp-playwright/src/glsp/graph/graph.type.ts
@@ -19,12 +19,21 @@ import type { PModelElement } from './elements/element';
import type { PNodeConstructor } from './elements/node';
import type { BothTypedEdge, SourceTypedEdge, TargetTypedEdge } from './elements/typed-edge/typed-edge.type';
-export interface EdgeConstructorOptions {
+export interface GraphConstructorOptions {
+ /**
+ * If any assertions should be triggered
+ */
+ assert?: boolean;
+}
+
+export interface EdgeConstructorOptions extends GraphConstructorOptions {
sourceConstructor?: PNodeConstructor;
targetConstructor?: PNodeConstructor;
}
export interface EdgeSearchOptions extends EdgeConstructorOptions {
+ sourceId?: string;
+ targetId?: string;
sourceSelectorOrLocator?: string | Locator;
targetSelectorOrLocator?: string | Locator;
}
diff --git a/packages/glsp-playwright/src/test/assertions.ts b/packages/glsp-playwright/src/test/assertions.ts
index aa28c0d..90d6143 100644
--- a/packages/glsp-playwright/src/test/assertions.ts
+++ b/packages/glsp-playwright/src/test/assertions.ts
@@ -13,12 +13,12 @@
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
-import { expect as baseExpect, ExpectMatcherState } from '@playwright/test';
+import { expect as baseExpect, ExpectMatcherState, Locator } from '@playwright/test';
import { Selectable } from '../extension';
-import { GLSPSemanticGraph, PModelElement } from '../glsp';
+import { GLSPSemanticGraph, PModelElement, SVGMetadataUtils } from '../glsp';
+import { GLSPLocator, Locateable } from '../remote';
import { ConstructorT } from '../types';
-
-export { test } from '@playwright/test';
+import { unwrapLocator } from '../utils';
interface MatcherReturnType {
message: () => string;
@@ -30,26 +30,97 @@ interface MatcherReturnType {
}
/* eslint-disable no-invalid-this */
+async function toBeSelected(this: ExpectMatcherState, element: PModelElement): Promise {
+ const assertionName = 'toBeSelected';
+ let pass: boolean;
+ let matcherResult: any;
+
+ try {
+ await toContainClass.call(this, element, Selectable.CSS);
+ pass = true;
+ } catch (e: any) {
+ matcherResult = e.matcherResult;
+ pass = false;
+ }
+
+ return {
+ message: () =>
+ this.utils.matcherHint(assertionName, undefined, undefined, { isNot: this.isNot }) +
+ '\n\n' +
+ assertionName +
+ ' - Assertions\n' +
+ matcherResult.message,
+ pass,
+ name: assertionName,
+ expected: Selectable.CSS,
+ actual: matcherResult?.actual
+ };
+}
+
async function toHaveSelected(
this: ExpectMatcherState,
graph: GLSPSemanticGraph,
- expected: { type: ConstructorT; elements: PModelElement[] }
+ expected: { type: ConstructorT; elements: PModelElement[] | (() => PModelElement[]) }
): Promise {
const assertionName = 'toHaveSelected';
let pass: boolean;
let matcherResult: any;
try {
+ await baseExpect(graph.locate().locator(`.${Selectable.CSS}`).first()).toBeAttached();
const nodes = await graph.getSelectedElements(expected.type);
- baseExpect(nodes).toHaveLength(expected.elements.length);
+ const elements = typeof expected.elements === 'function' ? expected.elements() : expected.elements;
+ baseExpect(nodes).toHaveLength(elements.length);
const nodeIds = await Promise.all(nodes.map(n => n.idAttr()));
- const expectedIds = await Promise.all(expected.elements.map(n => n.idAttr()));
+ const expectedIds = await Promise.all(elements.map(n => n.idAttr()));
baseExpect(nodeIds.sort()).toEqual(expectedIds.sort());
pass = true;
} catch (e: any) {
- matcherResult = e.matcherResult;
+ matcherResult = e.matcherResult ?? e.error.message;
+ pass = false;
+ }
+
+ return {
+ message: () =>
+ this.utils.matcherHint(assertionName, undefined, undefined, { isNot: this.isNot }) +
+ '\n\n' +
+ assertionName +
+ ' - Assertions\n' +
+ matcherResult.message,
+ pass,
+ name: assertionName,
+ expected,
+ actual: matcherResult?.actual
+ };
+}
+
+async function toContainElement(
+ this: ExpectMatcherState,
+ graph: GLSPSemanticGraph,
+ expected: { type: ConstructorT; query: { label: string } }
+): Promise {
+ const assertionName = 'toHaveSelected';
+ let pass: boolean;
+ let matcherResult: any;
+
+ try {
+ const element = await graph.getModelElement(
+ `${SVGMetadataUtils.typeAttrOf(expected.type)}:has-text("${expected.query.label}")`,
+ expected.type,
+ { assert: false }
+ );
+
+ if (this.isNot) {
+ await baseExpect(element.locate()).toBeAttached({ attached: false });
+ pass = false;
+ } else {
+ await baseExpect(element.locate()).toBeAttached();
+ pass = true;
+ }
+ } catch (e: any) {
+ matcherResult = e.matcherResult ?? e.error.message;
pass = false;
}
@@ -57,7 +128,8 @@ async function toHaveSelected(
message: () =>
this.utils.matcherHint(assertionName, undefined, undefined, { isNot: this.isNot }) +
'\n\n' +
- 'SemanticGraph Selected Elements - Assertions\n' +
+ assertionName +
+ ' - Assertions\n' +
matcherResult.message,
pass,
name: assertionName,
@@ -76,7 +148,7 @@ async function toBeUnselected(this: ExpectMatcherState, graph: GLSPSemanticGraph
await toHaveSelected.call(this, graph, { type: PModelElement, elements: [] });
pass = true;
} catch (e: any) {
- matcherResult = e.matcherResult;
+ matcherResult = e.matcherResult ?? e.error.message;
pass = false;
}
@@ -84,15 +156,53 @@ async function toBeUnselected(this: ExpectMatcherState, graph: GLSPSemanticGraph
message: () =>
this.utils.matcherHint(assertionName, undefined, undefined, { isNot: this.isNot }) +
'\n\n' +
- 'SemanticGraph toBeUnselected - Assertions\n' +
+ assertionName +
+ ' - Assertions\n' +
matcherResult.message,
pass,
name: assertionName
};
}
+
+async function toContainClass(
+ this: ExpectMatcherState,
+ target: Locator | GLSPLocator | Locateable,
+ expected: string
+): Promise {
+ const assertionName = 'toContainClass';
+ let pass: boolean;
+ let matcherResult: any;
+
+ try {
+ const locator = unwrapLocator(target);
+
+ await baseExpect(locator).toHaveClass(new RegExp(expected));
+ pass = true;
+ } catch (e: any) {
+ matcherResult = e.matcherResult ?? e.error.message;
+ pass = false;
+ }
+
+ return {
+ message: () =>
+ this.utils.matcherHint(assertionName, undefined, undefined, { isNot: this.isNot }) +
+ '\n\n' +
+ assertionName +
+ ' - Assertions\n' +
+ matcherResult.message,
+ pass,
+ name: assertionName,
+ expected,
+ actual: matcherResult?.actual
+ };
+}
+
/* eslint-enable no-invalid-this */
export const expect = baseExpect.extend({
toHaveSelected,
- toBeUnselected
+ toBeUnselected,
+ toContainClass,
+ toBeSelected,
+ toContainElement
});
diff --git a/packages/glsp-playwright/src/test/index.ts b/packages/glsp-playwright/src/test/index.ts
new file mode 100644
index 0000000..ac244d9
--- /dev/null
+++ b/packages/glsp-playwright/src/test/index.ts
@@ -0,0 +1,19 @@
+/********************************************************************************
+ * Copyright (c) 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
+ ********************************************************************************/
+
+export * from './assertions';
+export * from './dynamic-variable';
+export * from './test';
diff --git a/packages/glsp-playwright/src/test.ts b/packages/glsp-playwright/src/test/test.ts
similarity index 97%
rename from packages/glsp-playwright/src/test.ts
rename to packages/glsp-playwright/src/test/test.ts
index a4aab90..c9f57f2 100644
--- a/packages/glsp-playwright/src/test.ts
+++ b/packages/glsp-playwright/src/test/test.ts
@@ -25,7 +25,7 @@ import {
VSCodeIntegration,
VSCodeSetup
} from '~/integration';
-import { GLSP_SERVER_TYPE_UNKNWON, GLSPServer } from './glsp-server';
+import { GLSP_SERVER_TYPE_UNKNWON, GLSPServer } from '../glsp-server';
/**
* GLSP-Playwright specific options
@@ -167,6 +167,4 @@ export function skipIntegration(integrationOptions?: IntegrationOptions, ...inte
return integrationOptions === undefined || integration.includes(integrationOptions.type);
}
-export { expect } from './test/assertions';
-export { DynamicVariable } from './test/dynamic-variable';
export { test as setup };
diff --git a/packages/glsp-playwright/src/utils/index.ts b/packages/glsp-playwright/src/utils/index.ts
index 5bd00b5..e931246 100644
--- a/packages/glsp-playwright/src/utils/index.ts
+++ b/packages/glsp-playwright/src/utils/index.ts
@@ -13,5 +13,6 @@
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
+export * from './locator.utils';
export * from './position.utils';
export * from './ts.utils';
diff --git a/packages/glsp-playwright/src/utils/locator.utils.ts b/packages/glsp-playwright/src/utils/locator.utils.ts
new file mode 100644
index 0000000..dfd3f08
--- /dev/null
+++ b/packages/glsp-playwright/src/utils/locator.utils.ts
@@ -0,0 +1,26 @@
+/********************************************************************************
+ * Copyright (c) 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 { Locator } from '@playwright/test';
+import { GLSPLocator, Locateable } from '../remote';
+
+export function unwrapLocator(target: Locator | GLSPLocator | Locateable): Locator {
+ if ('locate' in target) {
+ return target.locate();
+ }
+
+ return target;
+}
diff --git a/packages/glsp-playwright/src/utils/test.utils.ts b/packages/glsp-playwright/src/utils/test.utils.ts
index 99253de..3fee058 100644
--- a/packages/glsp-playwright/src/utils/test.utils.ts
+++ b/packages/glsp-playwright/src/utils/test.utils.ts
@@ -16,8 +16,23 @@
import { Locator } from '@playwright/test';
import { waitForFunction } from '~/integration/wait.fixes';
+import { GLSPLocator, Locateable } from '../remote';
+
+export async function waitForClassRemoval(
+ target: Locator | Locateable | GLSPLocator,
+ className: string,
+ timeout: number = 30000
+): Promise {
+ let locator: Locator;
+
+ if (target instanceof GLSPLocator) {
+ locator = target.locate();
+ } else if (target instanceof Locateable) {
+ locator = target.locate();
+ } else {
+ locator = target;
+ }
-export async function waitForClassRemoval(locator: Locator, className: string, timeout: number = 30000): Promise {
await locator.waitFor({
timeout,
state: 'attached'
diff --git a/packages/glsp-playwright/src/utils/ts.utils.ts b/packages/glsp-playwright/src/utils/ts.utils.ts
index 3a81eb7..c6e53ad 100644
--- a/packages/glsp-playwright/src/utils/ts.utils.ts
+++ b/packages/glsp-playwright/src/utils/ts.utils.ts
@@ -72,3 +72,7 @@ export async function definedAttr(locator: Locator, attr: string): Promise(obj: object, prop: K): obj is Record {
return prop in obj;
}
+
+export function isUndefinedOrValue(element: T | undefined, value: T): boolean {
+ return element === undefined || element === value;
+}
diff --git a/yarn.lock b/yarn.lock
index d4a4143..65b65d7 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -223,10 +223,10 @@
dependencies:
"@jridgewell/trace-mapping" "0.3.9"
-"@eclipse-glsp-examples/workflow-server-bundled@next":
- version "2.2.0-next.104"
- resolved "https://registry.yarnpkg.com/@eclipse-glsp-examples/workflow-server-bundled/-/workflow-server-bundled-2.2.0-next.104.tgz#1bea5c293a779e2f41bf1682f59554bc0e7c5b50"
- integrity sha512-JKHluG5qTPXuU2HGb9Yx5T6Dw5CGFn8ea6NN6bewDup8+JYlRkmegqw5cmV8dfTptdmErLeHLfi4kL9I/vfxKg==
+"@eclipse-glsp-examples/workflow-server-bundled@2.3.0-next":
+ version "2.3.0-next.112"
+ resolved "https://registry.yarnpkg.com/@eclipse-glsp-examples/workflow-server-bundled/-/workflow-server-bundled-2.3.0-next.112.tgz#e72e9dcd3952ef9f3378862de65cddc9bab48d4c"
+ integrity sha512-ogXYvHEBKYRJDo4WxDPjHKVlUMCoWef1gpM7OOuLei/JcOj/dKBS/yTc7YtScQWUR9dHmE/uVwUxOz6Bl5AuZg==
"@eclipse-glsp/cli@2.2.0-next.166+85305d0":
version "2.2.0-next.166"
@@ -242,6 +242,18 @@
semver "^7.5.1"
shelljs "^0.8.5"
+"@eclipse-glsp/client@next":
+ version "2.3.0-next.378"
+ resolved "https://registry.yarnpkg.com/@eclipse-glsp/client/-/client-2.3.0-next.378.tgz#cab445fe862eb0f050f6fc5f98b97399d8cf189f"
+ integrity sha512-Sa8EQ27X+yyqaD/eEvCKFkJnyS7a33qe8aE81qbs5jZfxYH88hsV030Y5+l6pPWkiuSqRZBX+Qnis43Tl+03gQ==
+ dependencies:
+ "@eclipse-glsp/sprotty" "2.3.0-next.378+96fdfd3"
+ autocompleter "^9.1.2"
+ file-saver "^2.0.5"
+ lodash "4.17.21"
+ snabbdom "~3.5.1"
+ vscode-jsonrpc "8.2.0"
+
"@eclipse-glsp/config-test@2.2.0-next.166+85305d0":
version "2.2.0-next.166"
resolved "https://registry.yarnpkg.com/@eclipse-glsp/config-test/-/config-test-2.2.0-next.166.tgz#3665a56c0fb81431b074a876aea2e7f13476b873"
@@ -314,6 +326,27 @@
dependencies:
prettier-plugin-packagejson "~2.4.6"
+"@eclipse-glsp/protocol@2.3.0-next.378+96fdfd3":
+ version "2.3.0-next.378"
+ resolved "https://registry.yarnpkg.com/@eclipse-glsp/protocol/-/protocol-2.3.0-next.378.tgz#9d7c1962f44c65ee226df06cd3ed28c915fa22da"
+ integrity sha512-v8o8+zjeDrL7FqlR8vb7GFiLuXjZByHjZLuCFrZ6HIvk+Jg1CWYxvITSmUNc91XJPTl7yoSNWBLOGm6PO3u+yg==
+ dependencies:
+ sprotty-protocol "1.2.0"
+ uuid "~10.0.0"
+ vscode-jsonrpc "8.2.0"
+
+"@eclipse-glsp/sprotty@2.3.0-next.378+96fdfd3":
+ version "2.3.0-next.378"
+ resolved "https://registry.yarnpkg.com/@eclipse-glsp/sprotty/-/sprotty-2.3.0-next.378.tgz#4e50f69be3acbe2487d22ec3182c660173b24413"
+ integrity sha512-lxPT/KFl8lM/u13D4nfOsoBVh3fG3DJUg9tyAf71XpoEoSzxvNPLRaNI/hBh7cQ7oAXy3J7W3l5RJ7eDD+t8ng==
+ dependencies:
+ "@eclipse-glsp/protocol" "2.3.0-next.378+96fdfd3"
+ autocompleter "^9.1.0"
+ snabbdom "~3.5.1"
+ sprotty "1.2.0"
+ sprotty-protocol "1.2.0"
+ vscode-jsonrpc "8.2.0"
+
"@eclipse-glsp/ts-config@2.2.0-next.166+85305d0":
version "2.2.0-next.166"
resolved "https://registry.yarnpkg.com/@eclipse-glsp/ts-config/-/ts-config-2.2.0-next.166.tgz#93f280b3d74b5c54620b639478a75ce8a8dcee3b"
@@ -1089,18 +1122,6 @@
dependencies:
"@types/yargs-parser" "*"
-"@types/yargs-parser@*":
- version "21.0.3"
- resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz#815e30b786d2e8f0dcd85fd5bcf5e1a04d008f15"
- integrity sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==
-
-"@types/yargs@^17.0.32":
- version "17.0.32"
- resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.32.tgz#030774723a2f7faafebf645f4e5a48371dca6229"
- integrity sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==
- dependencies:
- "@types/yargs-parser" "*"
-
"@typescript-eslint/eslint-plugin@^6.7.5":
version "6.21.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz#30830c1ca81fd5f3c2714e524c4303e0194f9cd3"
@@ -1511,6 +1532,11 @@ at-least-node@^1.0.0:
resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2"
integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==
+autocompleter@^9.1.0, autocompleter@^9.1.2:
+ version "9.3.2"
+ resolved "https://registry.yarnpkg.com/autocompleter/-/autocompleter-9.3.2.tgz#b66d4a2d19a6b6c378c6a897bf3c000eccef5fb9"
+ integrity sha512-rLbf2TLGOD7y+gOS36ksrZdIsvoHa2KXc2A7503w+NBRPrcF73zzFeYBxEcV/iMPjaBH3jFhNIYObZ7zt1fkCQ==
+
available-typed-arrays@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846"
@@ -3008,6 +3034,11 @@ file-entry-cache@^6.0.1:
dependencies:
flat-cache "^3.0.4"
+file-saver@^2.0.5:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-2.0.5.tgz#d61cfe2ce059f414d899e9dd6d4107ee25670c38"
+ integrity sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==
+
file-type@5.2.0, file-type@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/file-type/-/file-type-5.2.0.tgz#2ddbea7c73ffe36368dfae49dc338c058c2b8ad6"
@@ -3850,6 +3881,11 @@ interpret@^1.0.0:
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e"
integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==
+inversify@~6.0.2:
+ version "6.0.2"
+ resolved "https://registry.yarnpkg.com/inversify/-/inversify-6.0.2.tgz#dc7fa0348213d789d35ffb719dea9685570989c7"
+ integrity sha512-i9m8j/7YIv4mDuYXUAcrpKPSaju/CIly9AHK5jvCBeoiM/2KEsuCQTTP+rzSWWpLYWRukdXFSl6ZTk2/uumbiA==
+
ip-address@^9.0.5:
version "9.0.5"
resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-9.0.5.tgz#117a960819b08780c3bd1f14ef3c1cc1d3f3ea5a"
@@ -4603,7 +4639,7 @@ lodash.merge@^4.6.2:
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
-lodash@^4.17.21:
+lodash@4.17.21, lodash@^4.17.21:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@@ -6470,6 +6506,11 @@ smart-buffer@^4.2.0:
resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae"
integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==
+snabbdom@~3.5.1:
+ version "3.5.1"
+ resolved "https://registry.yarnpkg.com/snabbdom/-/snabbdom-3.5.1.tgz#25f80ef15b194baea703d9d5441892e369de18e1"
+ integrity sha512-wHMNIOjkm/YNE5EM3RCbr/+DVgPg6AqQAX1eOxO46zYNvCXjKP5Y865tqQj3EXnaMBjkxmQA5jFuDpDK/dbfiA==
+
socks-proxy-agent@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz#dc069ecf34436621acb41e3efa66ca1b5fed15b6"
@@ -6599,6 +6640,28 @@ sprintf-js@~1.0.2:
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==
+sprotty-protocol@1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/sprotty-protocol/-/sprotty-protocol-1.2.0.tgz#cfd6d637f2670a3d641997bb5add27cb1bddb57a"
+ integrity sha512-SHu61Qiw7bAD2nyRqdOASSihVNbeEuKI7cQx+o9EeyLpbmXKX6NTcGSVpxmWztHUIP0I6gZhKnkhF/BWo46mUQ==
+
+sprotty-protocol@^1.2.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/sprotty-protocol/-/sprotty-protocol-1.3.0.tgz#78a6a69cc5eb8b94b352882a83d0f339fd828a0d"
+ integrity sha512-cQgKHgzVRJXbosvMsEZK4YiMSPOFhL1yYa3oLBzp9lgDL7ltYbdGCQcFqoVD6h56ObuVWyjNZu1R8YSryEkZrg==
+
+sprotty@1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/sprotty/-/sprotty-1.2.0.tgz#6c377b36fc6d410bcb65aff0d3893076f84bc8d8"
+ integrity sha512-/YL1+S+pLhV+hF0Z9C4vQGuaVv9NVsDgEqRnF+vevvdbeio1w8lfGxOMKjzY7DHcVDBQoKe0kbKJXvMr3f/RsA==
+ dependencies:
+ autocompleter "^9.1.2"
+ file-saver "^2.0.5"
+ inversify "~6.0.2"
+ snabbdom "~3.5.1"
+ sprotty-protocol "^1.2.0"
+ tinyqueue "^2.0.3"
+
ssri@^10.0.0, ssri@^10.0.1:
version "10.0.6"
resolved "https://registry.yarnpkg.com/ssri/-/ssri-10.0.6.tgz#a8aade2de60ba2bce8688e3fa349bad05c7dc1e5"
@@ -6924,6 +6987,11 @@ timed-out@^4.0.0:
resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f"
integrity sha512-G7r3AhovYtr5YKOWQkta8RKAPb+J9IsO4uVmzjl8AZwfhs8UcUwTiD6gcJYSgOtzyjvQKrKYn41syHbUWMkafA==
+tinyqueue@^2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/tinyqueue/-/tinyqueue-2.0.3.tgz#64d8492ebf39e7801d7bd34062e29b45b2035f08"
+ integrity sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==
+
tmp@^0.0.33:
version "0.0.33"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
@@ -7296,6 +7364,11 @@ uuid@^9.0.0:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30"
integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==
+uuid@~10.0.0:
+ version "10.0.0"
+ resolved "https://registry.yarnpkg.com/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294"
+ integrity sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==
+
v8-compile-cache-lib@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf"
@@ -7333,6 +7406,11 @@ validate-npm-package-name@^5.0.0:
resolved "https://registry.yarnpkg.com/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz#a316573e9b49f3ccd90dbb6eb52b3f06c6d604e8"
integrity sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==
+vscode-jsonrpc@8.2.0:
+ version "8.2.0"
+ resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz#f43dfa35fb51e763d17cd94dcca0c9458f35abf9"
+ integrity sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==
+
wcwidth@^1.0.0, wcwidth@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8"