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"