diff --git a/.eslintrc.js b/.eslintrc.js index 33e3b4a..23f3a72 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -7,14 +7,6 @@ module.exports = { tsconfigRootDir: __dirname, project: 'tsconfig.eslint.json' }, - settings: { - // Necessary for aliasing paths: https://www.typescriptlang.org/tsconfig#paths - 'import/resolver': { - typescript: { - project: ['packages/glsp-playwright/tsconfig.json', 'tsconfig.json'] - } - } - }, rules: { 'no-null/no-null': 'off', // Accessing the browser DOM returns "null" instead of "undefined" 'no-restricted-imports': [ diff --git a/.vscode/settings.json b/.vscode/settings.json index 1e5b626..212682c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,8 +6,8 @@ { "editor.formatOnSave": true, "editor.codeActionsOnSave": { - "source.fixAll.eslint": true, - "source.organizeImports": true + "source.fixAll.eslint": "explicit", + "source.organizeImports": "explicit" }, "eslint.validate": ["javascript", "typescript"], "search.exclude": { diff --git a/examples/workflow-test/.env.example b/examples/workflow-test/.env.example index dca988a..c7e7ff5 100644 --- a/examples/workflow-test/.env.example +++ b/examples/workflow-test/.env.example @@ -1,6 +1,6 @@ -# For Theia and VSCode instances -GLSP_SERVER_DEBUG="true" -GLSP_SERVER_PORT="8081" +GLSP_SERVER_DEBUG="true" # For Theia and VSCode instances to connect to an external server +GLSP_SERVER_PORT="8081" +GLSP_SERVER_PLAYWRIGHT_MANAGED="true" # To allow Playwright to manage/start the server GLSP_WEBSOCKET_PATH="workflow" # Configurations diff --git a/examples/workflow-test/README.md b/examples/workflow-test/README.md index 17ea18b..8d47b8c 100644 --- a/examples/workflow-test/README.md +++ b/examples/workflow-test/README.md @@ -2,6 +2,40 @@ This package contains code examples that demonstrate how to test diagram editors using the [Graphical Language Server Platform (GLSP)](https://github.com/eclipse-glsp/glsp). +
+ Expand test list + +| Feature | Standalone | Theia Integration | Eclipse Integration | VS Code Integration | +| ------------------------------------------------------------------------------------ | :------------------: | :---------------: | :-----------------: | :-----------------: | +| Model Saving | - | - | - | - | +| Model Dirty State | | - | - | - | +| Model SVG Export | - | - | - | - | +| Model Layout | - | - | - | - | +| Restoring viewport on re-open | | - | | | +| Model Edit Modes
- Edit
- Read-only |
-
-  |
-
- |
-
  |
-
-  | +| Client View Port
- Center
- Fit to Screen |
-
- |
-
- |
-
- |
-
- | +| Client Status Notification | - | - | - | - | +| Client Message Notification | - | - | | - | +| Client Progress Reporting | | - | | - | +| Element Selection | ✓ | ✓ | - | ✓ | +| Element Hover | - | - | - | - | +| Element Validation | - | - | - | - | +| Element Navigation | | - | - | - | +| Element Type Hints | - | - | - | - | +| Element Creation and Deletion | - | - | - | - | +| Node Change Bounds
- Move
- Resize |
-
- |
-
- |
-
- |
-
- | +| Node Change Container | - | - | - | - | +| Edge Reconnect | - | - | - | - | +| Edge Routing Points | - | - | - | - | +| Ghost Elements | - | - | - | - | +| Element Text Editing | - | - | - | - | +| Clipboard (Cut, Copy, Paste) | - | - | - | - | +| Undo / Redo | - | - | - | - | +| Contexts
- Context Menu
- Command Palette
- Tool Palette |

-
- |
-
-
- |

-
- |
-
-
- | +| Accessibility Features (experimental)
- Search
- Move
- Zoom
- Resize |
-
-
-
- | | | | +| Helper Lines (experimental) | - | - | - | - | +
+ ## Prerequisites The following libraries/frameworks need to be installed on your system: diff --git a/examples/workflow-test/configs/project.config.ts b/examples/workflow-test/configs/project.config.ts new file mode 100644 index 0000000..87760ad --- /dev/null +++ b/examples/workflow-test/configs/project.config.ts @@ -0,0 +1,118 @@ +/******************************************************************************** + * 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 { + GLSPPlaywrightOptions, + StandaloneIntegrationOptions, + TheiaIntegrationOptions, + VSCodeIntegrationOptions +} from '@eclipse-glsp/glsp-playwright'; +import { PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, Project, devices } from '@playwright/test'; +import * as path from 'path'; +import { getEnv } from './utils'; + +const projectDevices = devices['Desktop Chrome']; + +export function createStandaloneProject(): Project[] { + const url = getEnv('STANDALONE_URL'); + + if (url === undefined) { + console.error(`[Worker: ${process.env.TEST_PARALLEL_INDEX}] Standalone project can not be created.\n`); + return []; + } + + const standaloneIntegrationOptions: StandaloneIntegrationOptions = { + type: 'Standalone', + url + }; + + return [ + { + name: 'standalone', + testMatch: ['**/*.spec.js'], + use: { + ...projectDevices, + integrationOptions: standaloneIntegrationOptions + } + } + ]; +} + +export function createTheiaProject(): Project[] { + const url = getEnv('THEIA_URL'); + + if (url === undefined) { + console.error(`[Worker: ${process.env.TEST_PARALLEL_INDEX}] Theia project can not be created.\n`); + return []; + } + + const theiaIntegrationOptions: TheiaIntegrationOptions = { + type: 'Theia', + url, + widgetId: 'workflow-diagram', + workspace: '../workspace', + file: 'example1.wf' + }; + + return [ + { + name: 'theia', + testMatch: ['**/*.spec.js'], + use: { + ...projectDevices, + baseURL: theiaIntegrationOptions.url, + integrationOptions: theiaIntegrationOptions + } + } + ]; +} + +export function createVSCodeProject(): Project[] { + const vsixId = getEnv('VSCODE_VSIX_ID'); + const vsixPath = getEnv('VSCODE_VSIX_PATH'); + + if (vsixId === undefined || vsixPath === undefined) { + console.error(`[Worker: ${process.env.TEST_PARALLEL_INDEX}] VSCode project can not be created.\n`); + return []; + } + + const vscodeIntegrationOptions: VSCodeIntegrationOptions = { + type: 'VSCode', + workspace: '../workspace', + file: 'example1.wf', + vsixId, + vsixPath, + storagePath: path.join(__dirname, '../playwright/.storage/vscode.setup.json') + }; + + return [ + { + name: 'vscode-setup', + testMatch: ['setup/vscode.setup.js'], + use: { + integrationOptions: vscodeIntegrationOptions + } + }, + { + name: 'vscode', + testMatch: ['**/*.spec.js'], + dependencies: ['vscode-setup'], + use: { + integrationOptions: vscodeIntegrationOptions + } + } + ]; +} diff --git a/examples/workflow-test/src/utils.ts b/examples/workflow-test/configs/utils.ts similarity index 69% rename from examples/workflow-test/src/utils.ts rename to examples/workflow-test/configs/utils.ts index 06e9808..3ec8e21 100644 --- a/examples/workflow-test/src/utils.ts +++ b/examples/workflow-test/configs/utils.ts @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (c) 2023 EclipseSource and others. + * 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 @@ -14,9 +14,11 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -export function getDefined(val?: string): string { - if (val === undefined || val === null) { - throw new Error('Given value is not defined'); +export function getEnv(parameter: string, log: boolean = true): string | undefined { + const val = process.env[parameter]; + + if (log && (val === undefined || val === null)) { + console.error(`[Worker: ${process.env.TEST_PARALLEL_INDEX}] Parameter "${parameter}" not found in process.env`); } return val; } diff --git a/examples/workflow-test/configs/webserver.config.ts b/examples/workflow-test/configs/webserver.config.ts new file mode 100644 index 0000000..6075bec --- /dev/null +++ b/examples/workflow-test/configs/webserver.config.ts @@ -0,0 +1,53 @@ +/******************************************************************************** + * 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 { GLSPPlaywrightOptions } from '@eclipse-glsp/glsp-playwright'; +import { PlaywrightTestConfig } from '@playwright/test'; +import { getEnv } from './utils'; + +export function isManagedServer(): boolean { + const env = getEnv('GLSP_SERVER_PLAYWRIGHT_MANAGED', false); + return env === undefined || env === 'true'; +} + +export function hasRunningServer(config: PlaywrightTestConfig): boolean { + const webserver = config.webServer; + + const isArray = Array.isArray(webserver); + return !isManagedServer() || (isArray && webserver.length > 0) || (!isArray && webserver !== undefined); +} + +export function createWebserver(): PlaywrightTestConfig['webServer'] { + if (!isManagedServer()) { + return []; + } + + const port = getEnv('GLSP_SERVER_PORT'); + + if (port === undefined) { + console.error('Webserver will be not created.\n'); + return []; + } + + return [ + { + command: `yarn start:server -w -p ${+port}`, + port: +port, + reuseExistingServer: !process.env.CI, + stdout: 'ignore' + } + ]; +} diff --git a/examples/workflow-test/package.json b/examples/workflow-test/package.json index 2332ca1..3cea89a 100644 --- a/examples/workflow-test/package.json +++ b/examples/workflow-test/package.json @@ -47,11 +47,8 @@ "devDependencies": { "@eclipse-glsp-examples/workflow-server-bundled": "~2.0.1", "@eclipse-glsp/glsp-playwright": "~2.0.0", - "@playwright/test": "^1.34.3", - "@theia/playwright": "1.39.0", - "commander": "^10.0.1", + "@playwright/test": "^1.40.1", "dotenv": "^16.0.3", - "mvn-artifact-download": "5.1.0", "ts-dedent": "^2.2.0", "tsx": "^3.12.4" }, diff --git a/examples/workflow-test/playwright.config.ts b/examples/workflow-test/playwright.config.ts index 2894858..2791d40 100644 --- a/examples/workflow-test/playwright.config.ts +++ b/examples/workflow-test/playwright.config.ts @@ -15,42 +15,14 @@ ********************************************************************************/ import 'reflect-metadata'; -import type { - GLSPPlaywrightOptions, - StandaloneIntegrationOptions, - TheiaIntegrationOptions, - VSCodeIntegrationOptions -} from '@eclipse-glsp/glsp-playwright'; +import type { GLSPPlaywrightOptions } from '@eclipse-glsp/glsp-playwright'; import type { PlaywrightTestConfig } from '@playwright/test'; -import { devices } from '@playwright/test'; import * as dotenv from 'dotenv'; -import * as path from 'path'; -import { getDefined } from './src/utils'; +import { createStandaloneProject, createTheiaProject, createVSCodeProject } from './configs/project.config'; +import { createWebserver, hasRunningServer } from './configs/webserver.config'; dotenv.config(); -const standaloneIntegrationOptions: StandaloneIntegrationOptions = { - type: 'Standalone', - url: getDefined(process.env.STANDALONE_URL) -}; - -const theiaIntegrationOptions: TheiaIntegrationOptions = { - type: 'Theia', - url: getDefined(process.env.THEIA_URL), - widgetId: 'workflow-diagram', - workspace: '../workspace', - file: 'example1.wf' -}; - -const vscodeIntegrationOptions: VSCodeIntegrationOptions = { - type: 'VSCode', - workspace: '../workspace', - file: 'example1.wf', - vsixId: getDefined(process.env.VSCODE_VSIX_ID), - vsixPath: getDefined(process.env.VSCODE_VSIX_PATH), - storagePath: path.join(__dirname, 'playwright/.storage/vscode.setup.json') -}; - /** * See https://playwright.dev/docs/test-configuration. */ @@ -69,48 +41,12 @@ const config: PlaywrightTestConfig = { actionTimeout: 0, trace: 'on-first-retry' }, - webServer: [ - { - command: `yarn start:server -w -p ${+getDefined(process.env.GLSP_SERVER_PORT)}`, - port: +getDefined(process.env.GLSP_SERVER_PORT), - reuseExistingServer: !process.env.CI, - stdout: 'ignore' - } - ], - projects: [ - { - name: 'standalone', - testMatch: ['**/*.spec.js'], - use: { - ...devices['Desktop Chrome'], - integrationOptions: standaloneIntegrationOptions - } - }, - { - name: 'theia', - testMatch: ['**/*.spec.js'], - use: { - ...devices['Desktop Chrome'], - baseURL: theiaIntegrationOptions.url, - integrationOptions: theiaIntegrationOptions - } - }, - { - name: 'vscode-setup', - testMatch: ['setup/vscode.setup.js'], - use: { - integrationOptions: vscodeIntegrationOptions - } - }, - { - name: 'vscode', - testMatch: ['**/*.spec.js'], - dependencies: ['vscode-setup'], - use: { - integrationOptions: vscodeIntegrationOptions - } - } - ] + webServer: createWebserver(), + projects: [] }; +if (hasRunningServer(config)) { + config.projects = [...createStandaloneProject(), ...createTheiaProject(), ...createVSCodeProject()]; +} + export default config; diff --git a/examples/workflow-test/src/graph/elements/task-automated.po.ts b/examples/workflow-test/src/graph/elements/task-automated.po.ts index 40b1181..da372b2 100644 --- a/examples/workflow-test/src/graph/elements/task-automated.po.ts +++ b/examples/workflow-test/src/graph/elements/task-automated.po.ts @@ -16,7 +16,9 @@ import { ChildrenAccessor, Mix, - NodeMetadata, PNode, SVGMetadataUtils, + NodeMetadata, + PNode, + SVGMetadataUtils, useClickableFlow, useCommandPaletteCapability, useDraggableFlow, diff --git a/examples/workflow-test/src/graph/elements/task-manual.po.ts b/examples/workflow-test/src/graph/elements/task-manual.po.ts index 08263c0..d4ac1b1 100644 --- a/examples/workflow-test/src/graph/elements/task-manual.po.ts +++ b/examples/workflow-test/src/graph/elements/task-manual.po.ts @@ -18,7 +18,8 @@ import { Mix, NodeMetadata, PLabelledElement, - PNode, SVGMetadataUtils, + PNode, + SVGMetadataUtils, useClickableFlow, useCommandPaletteCapability, useDeletableFlow, @@ -26,7 +27,8 @@ import { useHoverableFlow, usePopupCapability, useRenameableFlow, - useResizeHandleCapability + useResizeHandleCapability, + useSelectableFlow } from '@eclipse-glsp/glsp-playwright/'; import { LabelHeading } from './label-heading.po'; @@ -36,6 +38,7 @@ export const TaskManualMixin = Mix(PNode) .flow(useDeletableFlow) .flow(useDraggableFlow) .flow(useRenameableFlow) + .flow(useSelectableFlow) .capability(useResizeHandleCapability) .capability(usePopupCapability) .capability(useCommandPaletteCapability) diff --git a/examples/workflow-test/tests/features/connectable-element.spec.ts b/examples/workflow-test/tests/core/connectable-element.spec.ts similarity index 96% rename from examples/workflow-test/tests/features/connectable-element.spec.ts rename to examples/workflow-test/tests/core/connectable-element.spec.ts index f2ac644..76e3149 100644 --- a/examples/workflow-test/tests/features/connectable-element.spec.ts +++ b/examples/workflow-test/tests/core/connectable-element.spec.ts @@ -13,7 +13,7 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { GLSPApp, expect, test } from '@eclipse-glsp/glsp-playwright/'; +import { expect, test } from '@eclipse-glsp/glsp-playwright/'; import { WorkflowApp } from '../../src/app/workflow-app'; import { ActivityNodeFork } from '../../src/graph/elements/activity-node-fork.po'; import { Edge } from '../../src/graph/elements/edge.po'; @@ -25,7 +25,7 @@ test.describe('The edge accessor of a connectable element', () => { let graph: WorkflowGraph; test.beforeEach(async ({ integration }) => { - app = await GLSPApp.loadApp(WorkflowApp, { + app = new WorkflowApp({ type: 'integration', integration }); diff --git a/examples/workflow-test/tests/features/edge.spec.ts b/examples/workflow-test/tests/core/edge.spec.ts similarity index 94% rename from examples/workflow-test/tests/features/edge.spec.ts rename to examples/workflow-test/tests/core/edge.spec.ts index cd19dd7..2563f36 100644 --- a/examples/workflow-test/tests/features/edge.spec.ts +++ b/examples/workflow-test/tests/core/edge.spec.ts @@ -13,7 +13,7 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { GLSPApp, expect, test } from '@eclipse-glsp/glsp-playwright/'; +import { expect, test } from '@eclipse-glsp/glsp-playwright/'; import { WorkflowApp } from '../../src/app/workflow-app'; import { ActivityNodeFork } from '../../src/graph/elements/activity-node-fork.po'; import { Edge } from '../../src/graph/elements/edge.po'; @@ -25,7 +25,7 @@ test.describe('Edges', () => { let graph: WorkflowGraph; test.beforeEach(async ({ integration }) => { - app = await GLSPApp.loadApp(WorkflowApp, { + app = new WorkflowApp({ type: 'integration', integration }); diff --git a/examples/workflow-test/tests/features/flows/deletable.spec.ts b/examples/workflow-test/tests/core/flows/deletable.spec.ts similarity index 93% rename from examples/workflow-test/tests/features/flows/deletable.spec.ts rename to examples/workflow-test/tests/core/flows/deletable.spec.ts index a6e2eb7..133f946 100644 --- a/examples/workflow-test/tests/features/flows/deletable.spec.ts +++ b/examples/workflow-test/tests/core/flows/deletable.spec.ts @@ -13,7 +13,7 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { GLSPApp, 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'; @@ -23,7 +23,7 @@ test.describe('Deletable flow', () => { let graph: WorkflowGraph; test.beforeEach(async ({ integration }) => { - app = await GLSPApp.loadApp(WorkflowApp, { + app = new WorkflowApp({ type: 'integration', integration }); diff --git a/examples/workflow-test/tests/features/graph.spec.ts b/examples/workflow-test/tests/core/graph.spec.ts similarity index 97% rename from examples/workflow-test/tests/features/graph.spec.ts rename to examples/workflow-test/tests/core/graph.spec.ts index 63d3e99..40bc9bf 100644 --- a/examples/workflow-test/tests/features/graph.spec.ts +++ b/examples/workflow-test/tests/core/graph.spec.ts @@ -13,7 +13,7 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { GLSPApp, expect, test } from '@eclipse-glsp/glsp-playwright/'; +import { expect, test } from '@eclipse-glsp/glsp-playwright/'; import { WorkflowApp } from '../../src/app/workflow-app'; import { ActivityNodeFork } from '../../src/graph/elements/activity-node-fork.po'; import { Edge } from '../../src/graph/elements/edge.po'; @@ -25,7 +25,7 @@ test.describe('The graph', () => { let graph: WorkflowGraph; test.beforeEach(async ({ integration }) => { - app = await GLSPApp.loadApp(WorkflowApp, { + app = new WorkflowApp({ type: 'integration', integration }); diff --git a/examples/workflow-test/tests/features/parent.spec.ts b/examples/workflow-test/tests/core/parent.spec.ts similarity index 95% rename from examples/workflow-test/tests/features/parent.spec.ts rename to examples/workflow-test/tests/core/parent.spec.ts index e34f526..ee7a0e1 100644 --- a/examples/workflow-test/tests/features/parent.spec.ts +++ b/examples/workflow-test/tests/core/parent.spec.ts @@ -13,7 +13,7 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { GLSPApp, expect, test } from '@eclipse-glsp/glsp-playwright/'; +import { expect, test } from '@eclipse-glsp/glsp-playwright/'; 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'; @@ -24,7 +24,7 @@ test.describe('The children accessor of a parent element', () => { let graph: WorkflowGraph; test.beforeEach(async ({ integration }) => { - app = await GLSPApp.loadApp(WorkflowApp, { + app = new WorkflowApp({ type: 'integration', integration }); diff --git a/examples/workflow-test/tests/features/shortcuts.spec.ts b/examples/workflow-test/tests/core/shortcuts.spec.ts similarity index 93% rename from examples/workflow-test/tests/features/shortcuts.spec.ts rename to examples/workflow-test/tests/core/shortcuts.spec.ts index 6647694..03edefc 100644 --- a/examples/workflow-test/tests/features/shortcuts.spec.ts +++ b/examples/workflow-test/tests/core/shortcuts.spec.ts @@ -13,7 +13,7 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { GLSPApp, 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'; @@ -23,7 +23,7 @@ test.describe('Shortcuts', () => { let graph: WorkflowGraph; test.beforeEach(async ({ integration }) => { - app = await GLSPApp.loadApp(WorkflowApp, { + app = new WorkflowApp({ type: 'integration', integration }); diff --git a/examples/workflow-test/tests/features/resize-handle.spec.ts b/examples/workflow-test/tests/features/change-bounds/resize-handle.spec.ts similarity index 89% rename from examples/workflow-test/tests/features/resize-handle.spec.ts rename to examples/workflow-test/tests/features/change-bounds/resize-handle.spec.ts index 4155b0e..6ed887b 100644 --- a/examples/workflow-test/tests/features/resize-handle.spec.ts +++ b/examples/workflow-test/tests/features/change-bounds/resize-handle.spec.ts @@ -13,17 +13,17 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { GLSPApp, PMetadata, ResizeHandle, 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 { PMetadata, ResizeHandle, 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'; test.describe('The resizing handle', () => { let app: WorkflowApp; let graph: WorkflowGraph; test.beforeEach(async ({ integration }) => { - app = await GLSPApp.loadApp(WorkflowApp, { + app = new WorkflowApp({ type: 'integration', integration }); diff --git a/examples/workflow-test/tests/features/command-palette.spec.ts b/examples/workflow-test/tests/features/command-palette/command-palette.spec.ts similarity index 94% rename from examples/workflow-test/tests/features/command-palette.spec.ts rename to examples/workflow-test/tests/features/command-palette/command-palette.spec.ts index 42a1ac1..43a7430 100644 --- a/examples/workflow-test/tests/features/command-palette.spec.ts +++ b/examples/workflow-test/tests/features/command-palette/command-palette.spec.ts @@ -13,12 +13,12 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { GLSPApp, GLSPGlobalCommandPalette, 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 { GLSPGlobalCommandPalette, 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'; test.describe('The command palette', () => { let app: WorkflowApp; @@ -26,7 +26,7 @@ test.describe('The command palette', () => { let globalCommandPalette: GLSPGlobalCommandPalette; test.beforeEach(async ({ integration }) => { - app = await GLSPApp.loadApp(WorkflowApp, { + app = new WorkflowApp({ type: 'integration', integration }); diff --git a/examples/workflow-test/tests/features/popup.spec.ts b/examples/workflow-test/tests/features/hover/popup.spec.ts similarity index 87% rename from examples/workflow-test/tests/features/popup.spec.ts rename to examples/workflow-test/tests/features/hover/popup.spec.ts index 2cf1235..737d329 100644 --- a/examples/workflow-test/tests/features/popup.spec.ts +++ b/examples/workflow-test/tests/features/hover/popup.spec.ts @@ -13,18 +13,18 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { GLSPApp, expect, test } from '@eclipse-glsp/glsp-playwright/'; +import { expect, test } from '@eclipse-glsp/glsp-playwright/'; import { dedent } from 'ts-dedent'; -import { WorkflowApp } from '../../src/app/workflow-app'; -import { TaskManual } from '../../src/graph/elements/task-manual.po'; -import { WorkflowGraph } from '../../src/graph/workflow.graph'; +import { WorkflowApp } from '../../../src/app/workflow-app'; +import { TaskManual } from '../../../src/graph/elements/task-manual.po'; +import { WorkflowGraph } from '../../../src/graph/workflow.graph'; test.describe('The popup', () => { let app: WorkflowApp; let graph: WorkflowGraph; test.beforeEach(async ({ integration }) => { - app = await GLSPApp.loadApp(WorkflowApp, { + app = new WorkflowApp({ type: 'integration', integration }); diff --git a/examples/workflow-test/tests/features/routing-point.spec.ts b/examples/workflow-test/tests/features/routing/routing-point.spec.ts similarity index 88% rename from examples/workflow-test/tests/features/routing-point.spec.ts rename to examples/workflow-test/tests/features/routing/routing-point.spec.ts index 55c124c..388bfb5 100644 --- a/examples/workflow-test/tests/features/routing-point.spec.ts +++ b/examples/workflow-test/tests/features/routing/routing-point.spec.ts @@ -13,17 +13,17 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { GLSPApp, expect, test } from '@eclipse-glsp/glsp-playwright/'; -import { WorkflowApp } from '../../src/app/workflow-app'; -import { Edge } from '../../src/graph/elements/edge.po'; -import { WorkflowGraph } from '../../src/graph/workflow.graph'; +import { expect, test } from '@eclipse-glsp/glsp-playwright/'; +import { WorkflowApp } from '../../../src/app/workflow-app'; +import { Edge } from '../../../src/graph/elements/edge.po'; +import { WorkflowGraph } from '../../../src/graph/workflow.graph'; test.describe('The routing points of an edge', () => { let app: WorkflowApp; let graph: WorkflowGraph; test.beforeEach(async ({ integration }) => { - app = await GLSPApp.loadApp(WorkflowApp, { + app = new WorkflowApp({ type: 'integration', integration }); diff --git a/examples/workflow-test/tests/features/select/select.spec.ts b/examples/workflow-test/tests/features/select/select.spec.ts new file mode 100644 index 0000000..74b0c86 --- /dev/null +++ b/examples/workflow-test/tests/features/select/select.spec.ts @@ -0,0 +1,118 @@ +/******************************************************************************** + * 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 { PModelElement, Selectable, expect, skipIntegration, 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'; + +test.describe('The select feature', () => { + let app: WorkflowApp; + let graph: WorkflowGraph; + + test.beforeEach(async ({ integration }) => { + app = new WorkflowApp({ + type: 'integration', + integration + }); + graph = app.graph; + }); + + test('should allow to select a single element', async () => { + const element = await graph.getNodeByLabel('Push', TaskManual); + await element.select(); + + const selectedElement = await graph.getNodeBySelector(`.${Selectable.CSS}`, TaskManual); + + expect(await selectedElement.idAttr()).toBe(await element.idAttr()); + }); + + test('should deselect after a new selection', async () => { + const element1 = await graph.getNodeByLabel('Push', TaskManual); + await element1.select(); + + const selectedElement = await graph.getNodeBySelector(`.${Selectable.CSS}`, TaskManual); + expect(await selectedElement.idAttr()).toBe(await element1.idAttr()); + + const element2 = await graph.getNodeByLabel('RflWt', TaskManual); + await element2.select(); + + const selectedElements = await graph.getNodesBySelector(`.${Selectable.CSS}`, TaskManual); + expect(selectedElements).toHaveLength(1); + expect(await selectedElements[0].idAttr()).toBe(await element2.idAttr()); + }); + + test('should allow to select multiple elements', async () => { + const element1 = await graph.getNodeByLabel('Push', TaskManual); + await element1.select(); + + const selectedElement = await graph.getNodeBySelector(`.${Selectable.CSS}`, TaskManual); + expect(await selectedElement.idAttr()).toBe(await element1.idAttr()); + + const element2 = await graph.getNodeByLabel('RflWt', TaskManual); + await element2.select({ modifiers: ['Control'] }); + + const targetIds = [await element1.idAttr(), await element2.idAttr()]; + const expectIds = await Promise.all((await graph.getNodesBySelector(`.${Selectable.CSS}`, TaskManual)).map(e => e.idAttr())); + expect(expectIds).toStrictEqual(targetIds); + }); + + test('should allow to select all elements by using a shortcut', async () => { + const page = app.page; + await graph.locate().click(); + await page.keyboard.press('Control+A'); + const elements = await graph.getAllModelElements(); + + for (const element of elements) { + expect(await element.classAttr()).toContain(Selectable.CSS); + } + }); + + test.describe('', () => { + test.skip(({ integrationOptions }) => skipIntegration(integrationOptions, 'Theia', 'VSCode'), 'Not supported'); + + test('should allow to deselect a single element through a keybinding', async () => { + const page = app.page; + const element = await graph.getNodeByLabel('Push', TaskManual); + await element.select(); + const before = await graph.getNodesBySelector(`.${Selectable.CSS}`, TaskManual); + expect(before).toHaveLength(1); + + // Resize Handle + await page.keyboard.press('Escape'); + // Selection + await page.keyboard.press('Escape'); + + await Promise.all((await graph.locate().locator(`.${Selectable.CSS}`).all()).map(l => l.waitFor({ state: 'detached' }))); + + const after = await graph.getModelElementsBySelector(`.${Selectable.CSS}`, PModelElement); + expect(after).toHaveLength(0); + }); + }); + + test('should allow to deselect a single element by clicking outside', async () => { + const element = await graph.getNodeByLabel('Push', TaskManual); + await element.select(); + const before = await graph.getNodesBySelector(`.${Selectable.CSS}`, TaskManual); + expect(before).toHaveLength(1); + + await graph.locate().click(); + await Promise.all((await graph.locate().locator(`.${Selectable.CSS}`).all()).map(l => l.waitFor({ state: 'detached' }))); + + const after = await graph.getModelElementsBySelector(`.${Selectable.CSS}`, PModelElement); + expect(after).toHaveLength(0); + }); +}); diff --git a/examples/workflow-test/tests/features/tool-palette.spec.ts b/examples/workflow-test/tests/features/tool-palette/tool-palette.spec.ts similarity index 92% rename from examples/workflow-test/tests/features/tool-palette.spec.ts rename to examples/workflow-test/tests/features/tool-palette/tool-palette.spec.ts index 0fdb749..11de4bc 100644 --- a/examples/workflow-test/tests/features/tool-palette.spec.ts +++ b/examples/workflow-test/tests/features/tool-palette/tool-palette.spec.ts @@ -13,13 +13,13 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { GLSPApp, Marker, createParameterizedIntegrationData, 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'; +import { Marker, createParameterizedIntegrationData, 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 integrationData = createParameterizedIntegrationData<{ nodes: string; @@ -43,7 +43,7 @@ test.describe('The tool palette', () => { let toolPalette: WorkflowToolPalette; test.beforeEach(async ({ integration }) => { - app = await GLSPApp.loadApp(WorkflowApp, { + app = new WorkflowApp({ type: 'integration', integration }); diff --git a/examples/workflow-test/tests/features/marker.spec.ts b/examples/workflow-test/tests/features/validation/marker.spec.ts similarity index 83% rename from examples/workflow-test/tests/features/marker.spec.ts rename to examples/workflow-test/tests/features/validation/marker.spec.ts index bc07050..9b382dc 100644 --- a/examples/workflow-test/tests/features/marker.spec.ts +++ b/examples/workflow-test/tests/features/validation/marker.spec.ts @@ -13,17 +13,17 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { GLSPApp, expect, test } from '@eclipse-glsp/glsp-playwright/'; -import { WorkflowApp } from '../../src/app/workflow-app'; -import { TaskAutomated } from '../../src/graph/elements/task-automated.po'; -import { WorkflowGraph } from '../../src/graph/workflow.graph'; +import { expect, test } from '@eclipse-glsp/glsp-playwright/'; +import { WorkflowApp } from '../../../src/app/workflow-app'; +import { TaskAutomated } from '../../../src/graph/elements/task-automated.po'; +import { WorkflowGraph } from '../../../src/graph/workflow.graph'; test.describe('The marker', () => { let app: WorkflowApp; let graph: WorkflowGraph; test.beforeEach(async ({ integration }) => { - app = await GLSPApp.loadApp(WorkflowApp, { + app = new WorkflowApp({ type: 'integration', integration }); diff --git a/examples/workflow-test/tsconfig.json b/examples/workflow-test/tsconfig.json index be231c8..4d153bc 100644 --- a/examples/workflow-test/tsconfig.json +++ b/examples/workflow-test/tsconfig.json @@ -7,5 +7,5 @@ "emitDecoratorMetadata": true, "baseUrl": "." }, - "include": ["src", "tests", "scripts", "playwright.config.ts", "./server/server-config.json"] + "include": ["src", "tests", "scripts", "playwright.config.ts", "./configs/*.ts", "./server/server-config.json"] } diff --git a/packages/glsp-playwright/.eslintrc.js b/packages/glsp-playwright/.eslintrc.js index 37c2321..77030c1 100644 --- a/packages/glsp-playwright/.eslintrc.js +++ b/packages/glsp-playwright/.eslintrc.js @@ -1,5 +1,13 @@ /** @type {import('eslint').Linter.Config} */ module.exports = { extends: '../../.eslintrc.js', + settings: { + // Necessary for aliasing paths: https://www.typescriptlang.org/tsconfig#paths + 'import/resolver': { + typescript: { + project: ['packages/glsp-playwright/tsconfig.json', 'tsconfig.json'] + } + } + }, rules: {} }; diff --git a/packages/glsp-playwright/package.json b/packages/glsp-playwright/package.json index 0c33792..8b42bf5 100644 --- a/packages/glsp-playwright/package.json +++ b/packages/glsp-playwright/package.json @@ -45,14 +45,16 @@ "uuid": "^9.0.0" }, "devDependencies": { + "@playwright/test": "^1.40.1", + "@theia/playwright": "~1.45.0", "@types/uuid": "^9.0.0", "@vscode/test-electron": "^2.3.2", "concurrently": "^7.6.0", "tsc-alias": "^1.8.2" }, "peerDependencies": { - "@playwright/test": "^1.32.1", - "@theia/playwright": "^1.39.0" + "@playwright/test": "^1.40.1", + "@theia/playwright": "~1.45.0" }, "publishConfig": { "access": "public" diff --git a/packages/glsp-playwright/src/extension/flows/index.ts b/packages/glsp-playwright/src/extension/flows/index.ts index 38976d5..d7ab8ee 100644 --- a/packages/glsp-playwright/src/extension/flows/index.ts +++ b/packages/glsp-playwright/src/extension/flows/index.ts @@ -18,3 +18,4 @@ export * from './delete.flow'; export * from './drag.flow'; export * from './hover.flow'; export * from './rename.flow'; +export * from './select.flow'; diff --git a/packages/glsp-playwright/src/extension/flows/select.flow.ts b/packages/glsp-playwright/src/extension/flows/select.flow.ts new file mode 100644 index 0000000..91266a7 --- /dev/null +++ b/packages/glsp-playwright/src/extension/flows/select.flow.ts @@ -0,0 +1,44 @@ +/******************************************************************************** + * 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 { PModelElement } from '../../glsp'; +import type { ConstructorA } from '../../types'; +import type { Flow } from '../types'; +import { Clickable } from './click.flow'; + +export interface SelectableOptions { + modifiers?: Array<'Alt' | 'Control' | 'Meta' | 'Shift'>; +} + +export interface Selectable { + select(options?: SelectableOptions): Promise; +} + +export namespace Selectable { + export const CSS = 'selected'; +} + +export function useSelectableFlow>(Base: TBase): Flow { + abstract class Mixin extends Base implements Selectable { + async select(options?: SelectableOptions): Promise { + await this.click(options); + return this.locate() + .and(this.page.locator(`.${Selectable.CSS}`)) + .waitFor(); + } + } + + return Mixin; +} diff --git a/packages/glsp-playwright/src/glsp/app/app.po.ts b/packages/glsp-playwright/src/glsp/app/app.po.ts index cd55bd1..45ae99a 100644 --- a/packages/glsp-playwright/src/glsp/app/app.po.ts +++ b/packages/glsp-playwright/src/glsp/app/app.po.ts @@ -16,7 +16,6 @@ import type { Locator, Page } from 'playwright-core'; import type { Integration } from '~/integration'; import { GLSPLocator } from '~/remote/locator'; -import type { ConstructorT } from '~/types'; import { GLSPGlobalCommandPalette } from '../features/command-palette'; import { GLSPLabelEditor } from '../features/label-editor/label-editor.po'; import { GLSPPopup } from '../features/popup/popup.po'; @@ -60,7 +59,7 @@ export type GLSPAppOptions = GLSPPageOptions | GLSPIntegrationOptions; * Integration mode * ```ts * test.beforeEach(async ({ integration }) => { - * app = await GLSPApp.loadApp(WorkflowApp, { + * app = await new GLSPApp({ * type: 'integration', * integration * }); @@ -71,7 +70,7 @@ export type GLSPAppOptions = GLSPPageOptions | GLSPIntegrationOptions; * Page mode * ```ts * test.beforeEach(async ({ page }) => { - * app = await GLSPApp.loadApp(WorkflowApp, { + * app = await new GLSPApp({ * type: 'page', * page * }); @@ -80,28 +79,30 @@ export type GLSPAppOptions = GLSPPageOptions | GLSPIntegrationOptions; * ``` */ export class GLSPApp { - readonly rootLocator; - readonly locator; - readonly integration; - readonly page; - - readonly graph; - readonly labelEditor; - readonly toolPalette; - readonly popup; - readonly globalCommandPalette; - - static async load(options: GLSPAppOptions): Promise { - return this.loadApp(GLSPApp, options); - } + readonly sprottySelector = 'div.sprotty'; - static async loadApp(appConstructor: ConstructorT, options: GLSPAppOptions): Promise { - const app = new appConstructor(options); - return app; - } + rootLocator: GLSPLocator; + locator: GLSPLocator; + integration?: Integration; + page: Page; + + graph; + labelEditor; + toolPalette; + popup; + globalCommandPalette; constructor(public readonly options: GLSPAppOptions) { - const sprottySelector = 'div.sprotty'; + this.initialize(options); + + this.graph = this.createGraph(); + this.labelEditor = this.createLabelEditor(); + this.toolPalette = this.createToolPalette(); + this.popup = this.createPopup(); + this.globalCommandPalette = this.createGlobalCommandPalette(); + } + + protected initialize(options: GLSPAppOptions): void { let locate: (selector: string) => Locator; if (options.type === 'page') { @@ -119,13 +120,7 @@ export class GLSPApp { options.rootSelector === undefined ? locate('body') : locate(options.rootSelector).locator('body'), this ); - this.locator = this.rootLocator.child(sprottySelector); - - this.graph = this.createGraph(); - this.labelEditor = this.createLabelEditor(); - this.toolPalette = this.createToolPalette(); - this.popup = this.createPopup(); - this.globalCommandPalette = this.createGlobalCommandPalette(); + this.locator = this.rootLocator.child(this.sprottySelector); } protected createGraph(): GLSPGraph { diff --git a/packages/glsp-playwright/src/glsp/graph/elements/element.ts b/packages/glsp-playwright/src/glsp/graph/elements/element.ts index 775b37b..c932be8 100644 --- a/packages/glsp-playwright/src/glsp/graph/elements/element.ts +++ b/packages/glsp-playwright/src/glsp/graph/elements/element.ts @@ -13,6 +13,7 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ +import { Locator } from '@playwright/test'; import { extractMetaTree } from '~/debugger/extractor'; import { GLSPLocator, Locateable } from '~/remote'; import type { ConstructorT } from '~/types/constructor'; @@ -21,27 +22,53 @@ import { ModelElementMetadata, PMetadata } from '../decorators'; import { SVGMetadata } from '../svg-metadata-api'; export async function assertEqualType(element: PModelElement): Promise { - if ((await element.locate().count()) !== 1) { + const located = await element.locate().all(); + const count = located.length; + + if (count !== 1) { + // Provide additional information + for (const locator of located) { + console.error('=========== ACCESS RETURNS =========='); + console.error(await locator.evaluate(elem => elem.outerHTML)); + } + throw Error( - `Assertion failed. Locator of ${typeof PModelElement} did not find single element in the DOM. It found ${await element.locate() - .count} elements.` + `Assertion failed. Locator did not find single element in the DOM. + It found ${count} elements for type ${element._metadata.type}. + Executed query: ${element.locate()}` ); } - const typeAttr = await element.typeAttr(); - if (typeAttr !== element._metadata.type) { + if (!(await isEqualType(element))) { throw Error( - `Assertion failed. Expected type '${element._metadata.type}', but it was '${typeAttr}'. Id = ${await element.idAttr()}` + `Assertion failed. Expected type '${ + element._metadata.type + }', but it was '${await element.typeAttr()}'. Id = ${await element.idAttr()}` ); } } +export async function isEqualType(element: PModelElement): Promise { + const typeAttr = await element.typeAttr(); + return element._metadata.type === '*' || typeAttr === element._metadata.type; +} + +export async function isEqualLocatorType(locator: Locator, constructor: PModelElementConstructor): Promise { + const typeAttr = await locator.getAttribute(SVGMetadata.type); + const constructorType = PMetadata.getType(constructor); + + return typeAttr !== null && (constructorType === '*' || typeAttr === constructorType); +} + export interface PModelElementData { locator: GLSPLocator; } export type PModelElementConstructor = ConstructorT; +@ModelElementMetadata({ + type: '*' +}) export class PModelElement extends Locateable { readonly graph; readonly _metadata; diff --git a/packages/glsp-playwright/src/glsp/graph/graph.po.ts b/packages/glsp-playwright/src/glsp/graph/graph.po.ts index 95331e7..8938224 100644 --- a/packages/glsp-playwright/src/glsp/graph/graph.po.ts +++ b/packages/glsp-playwright/src/glsp/graph/graph.po.ts @@ -21,11 +21,11 @@ import { definedAttr } from '~/utils/ts.utils'; import { PMetadata } from './decorators'; import { assertEqualType, createTypedEdgeProxy, getPModelElementConstructorOfType } from './elements'; import type { PEdge, PEdgeConstructor } from './elements/edge'; -import type { PModelElement, PModelElementConstructor } from './elements/element'; +import { PModelElement, PModelElementConstructor, isEqualLocatorType } from './elements/element'; import type { PNode, PNodeConstructor } from './elements/node'; import type { EdgeConstructorOptions, EdgeSearchOptions, ElementQuery, TypedEdge } from './graph.type'; import { waitForElementChanges, waitForElementIncrease } from './graph.wait'; -import { SVGMetadataUtils } from './svg-metadata-api'; +import { SVGMetadata, SVGMetadataUtils } from './svg-metadata-api'; export interface GLSPGraphOptions { locator: GLSPLocator; @@ -84,19 +84,29 @@ export class GLSPGraph extends Locateable { const elements: TElement[] = []; for await (const childLocator of await locator.all()) { - const id = await definedAttr(childLocator, 'id'); - elements.push(await this.getModelElementBySelector(`#${id}`, constructor)); + if ((await childLocator.count()) > 0) { + const id = await childLocator.getAttribute('id'); + if (id !== null && (await isEqualLocatorType(childLocator, constructor))) { + elements.push(await this.getModelElementBySelector(`#${id}`, constructor)); + } + } } return elements; } + async getAllModelElements(): Promise { + return this.getModelElementsBySelector(`[${SVGMetadata.type}]`, PModelElement); + } + async getNodeBySelector(selector: string, constructor: PNodeConstructor): Promise { return this.getNodeByLocator(this.locator.child(selector).locate(), constructor); } async getNodeByLocator(locator: Locator, constructor: PNodeConstructor): Promise { - const element = new constructor({ locator: this.locator.override(locator) }); + const element = new constructor({ + locator: this.locator.override(locator.and(this.locate().locator(SVGMetadataUtils.typeAttrOf(constructor)))) + }); await assertEqualType(element); return element; } @@ -113,8 +123,12 @@ export class GLSPGraph extends Locateable { const elements: TElement[] = []; for await (const childLocator of await locator.all()) { - const id = await definedAttr(childLocator, 'id'); - elements.push(await this.getNodeBySelector(`#${id}`, constructor)); + if ((await childLocator.count()) > 0) { + const id = await childLocator.getAttribute('id'); + if (id !== null && (await isEqualLocatorType(childLocator, constructor))) { + elements.push(await this.getNodeBySelector(`#${id}`, constructor)); + } + } } return elements; @@ -124,9 +138,17 @@ export class GLSPGraph extends Locateable { selector: string, constructor: PEdgeConstructor, options?: TOptions + ): Promise> { + return this.getEdgeByLocator(this.locator.child(selector).locate(), constructor, options); + } + + async getEdgeByLocator( + locator: Locator, + constructor: PEdgeConstructor, + options?: TOptions ): Promise> { const element = new constructor({ - locator: this.locator.child(selector) + locator: this.locator.override(locator.and(this.locate().locator(SVGMetadataUtils.typeAttrOf(constructor)))) }); await assertEqualType(element); return createTypedEdgeProxy(element, options); @@ -139,43 +161,45 @@ export class GLSPGraph extends Locateable { const elements: TypedEdge[] = []; for await (const locator of await this.locate().locator(SVGMetadataUtils.typeAttrOf(constructor)).all()) { - const id = await definedAttr(locator, 'id'); - const element = await this.getEdgeBySelector(`#${id}`, constructor, options); - const sourceChecks = []; - const targetChecks = []; - - if (options?.sourceConstructor) { - const sourceId = await element.sourceId(); - sourceChecks.push( - (await this.locate() - .locator(`[id$="${sourceId}"]${SVGMetadataUtils.typeAttrOf(options.sourceConstructor)}`) - .count()) > 0 - ); - } - - if (options?.targetConstructor) { - const targetId = await element.targetId(); - targetChecks.push( - (await this.locate() - .locator(`[id$="${targetId}"]${SVGMetadataUtils.typeAttrOf(options.targetConstructor)}`) - .count()) > 0 - ); - } - - if (options?.sourceSelector) { - const sourceId = await element.sourceId(); - const expectedId = await definedAttr(this.locate().locator(options.sourceSelector), 'id'); - sourceChecks.push(expectedId.includes(sourceId)); - } - - if (options?.targetSelector) { - const targetId = await element.targetId(); - const expectedId = await definedAttr(this.locate().locator(options.targetSelector), 'id'); - sourceChecks.push(expectedId.includes(targetId)); - } - - if (sourceChecks.every(c => c) && targetChecks.every(c => c)) { - elements.push(element); + const id = await locator.getAttribute('id'); + if (id !== null && (await isEqualLocatorType(locator, constructor))) { + const element = await this.getEdgeBySelector(`#${id}`, constructor, options); + const sourceChecks = []; + const targetChecks = []; + + if (options?.sourceConstructor) { + const sourceId = await element.sourceId(); + sourceChecks.push( + (await this.locate() + .locator(`[id$="${sourceId}"]${SVGMetadataUtils.typeAttrOf(options.sourceConstructor)}`) + .count()) > 0 + ); + } + + if (options?.targetConstructor) { + const targetId = await element.targetId(); + targetChecks.push( + (await this.locate() + .locator(`[id$="${targetId}"]${SVGMetadataUtils.typeAttrOf(options.targetConstructor)}`) + .count()) > 0 + ); + } + + if (options?.sourceSelector) { + const sourceId = await element.sourceId(); + const expectedId = await definedAttr(this.locate().locator(options.sourceSelector), 'id'); + sourceChecks.push(expectedId.includes(sourceId)); + } + + if (options?.targetSelector) { + const targetId = await element.targetId(); + const expectedId = await definedAttr(this.locate().locator(options.targetSelector), 'id'); + sourceChecks.push(expectedId.includes(targetId)); + } + + if (sourceChecks.every(c => c) && targetChecks.every(c => c)) { + elements.push(element); + } } } @@ -215,7 +239,19 @@ export class GLSPGraph extends Locateable { const elementType = typeof type === 'string' ? type : PMetadata.getType(type); const query: ElementQuery = { elementType: elementType, - all: () => this.getModelElementsOfType(getPModelElementConstructorOfType(elementType)) + all: () => this.getModelElementsOfType(getPModelElementConstructorOfType(elementType)), + filter: async elements => { + const filtered: PModelElement[] = []; + + for (const element of elements) { + const css = await element.classAttr(); + if (css && !css.includes('ghost-element')) { + filtered.push(element); + } + } + + return filtered; + } }; const { before, after } = await waitForElementChanges(query, creator, b => waitForElementIncrease(this.locator, query, b.length)); diff --git a/packages/glsp-playwright/src/glsp/graph/graph.type.ts b/packages/glsp-playwright/src/glsp/graph/graph.type.ts index 915e99f..1d9f640 100644 --- a/packages/glsp-playwright/src/glsp/graph/graph.type.ts +++ b/packages/glsp-playwright/src/glsp/graph/graph.type.ts @@ -42,4 +42,12 @@ export type TypedEdge { elementType: string; all: () => Promise; + filter?: (elements: TElement[]) => Promise; +} + +export namespace ElementQuery { + export async function exec(query: ElementQuery): Promise { + const elements = await query.all(); + return query.filter?.(elements) ?? elements; + } } diff --git a/packages/glsp-playwright/src/glsp/graph/graph.wait.ts b/packages/glsp-playwright/src/glsp/graph/graph.wait.ts index 3fa7c44..e66c6bb 100644 --- a/packages/glsp-playwright/src/glsp/graph/graph.wait.ts +++ b/packages/glsp-playwright/src/glsp/graph/graph.wait.ts @@ -16,7 +16,7 @@ import { waitForFunction } from '~/integration/wait.fixes'; import type { GLSPLocator } from '~/remote/locator'; import type { PModelElement } from './elements/element'; -import type { ElementQuery } from './graph.type'; +import { ElementQuery } from './graph.type'; import { SVGMetadataUtils } from './svg-metadata-api'; /** @@ -35,12 +35,12 @@ export async function waitForElementChanges( before: TElement[]; after: TElement[]; }> { - const before = await query.all(); + const before = await ElementQuery.exec(query); await operation(); await waitForOperation(before); - const after = await query.all(); + const after = await ElementQuery.exec(query); return { before, diff --git a/packages/glsp-playwright/src/integration/integration.base.ts b/packages/glsp-playwright/src/integration/integration.base.ts index f41c10d..efcd632 100644 --- a/packages/glsp-playwright/src/integration/integration.base.ts +++ b/packages/glsp-playwright/src/integration/integration.base.ts @@ -15,7 +15,7 @@ ********************************************************************************/ import type { Locator, Page } from '@playwright/test'; import { SVGMetadataUtils } from '../glsp/index'; -import type { IntegrationType } from './integration.type'; +import type { IntegrationArgs, IntegrationType } from './integration.type'; /** * Base class for all integrations. It provides lifecycle methods and @@ -23,7 +23,11 @@ import type { IntegrationType } from './integration.type'; */ export abstract class Integration { abstract readonly page: Page; - abstract readonly type: IntegrationType; + + constructor( + protected readonly args: IntegrationArgs, + readonly type: IntegrationType + ) {} /** * Prefixes the provided selector and returns the new {@link Locator}. diff --git a/packages/glsp-playwright/src/integration/integration.type.ts b/packages/glsp-playwright/src/integration/integration.type.ts index cee3b5d..75cd6ba 100644 --- a/packages/glsp-playwright/src/integration/integration.type.ts +++ b/packages/glsp-playwright/src/integration/integration.type.ts @@ -13,6 +13,7 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ +import type { Browser, Page } from '@playwright/test'; import type { PageIntegrationOptions } from './page'; import type { StandaloneIntegrationOptions } from './standalone'; import type { TheiaIntegrationOptions } from './theia'; @@ -25,3 +26,9 @@ export interface BaseIntegrationOptions { } export type IntegrationOptions = PageIntegrationOptions | StandaloneIntegrationOptions | TheiaIntegrationOptions | VSCodeIntegrationOptions; + +export interface IntegrationArgs { + page: Page; + playwright: typeof import('playwright-core'); + browser: Browser; +} diff --git a/packages/glsp-playwright/src/integration/page/page.integration.ts b/packages/glsp-playwright/src/integration/page/page.integration.ts index f906355..7decf66 100644 --- a/packages/glsp-playwright/src/integration/page/page.integration.ts +++ b/packages/glsp-playwright/src/integration/page/page.integration.ts @@ -13,20 +13,22 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import type { Page } from '@playwright/test'; import { SVGMetadataUtils } from '~/glsp'; import { Integration } from '../integration.base'; -import type { IntegrationType } from '../integration.type'; +import type { IntegrationArgs } from '../integration.type'; import type { PageIntegrationOptions } from './page.options'; /** * The {@link PageIntegration} provides an unchanged experience of Playwright. */ export class PageIntegration extends Integration { - readonly type: IntegrationType = 'Page'; + override page = this.args.page; - constructor(public readonly page: Page, protected readonly options?: PageIntegrationOptions) { - super(); + constructor( + args: IntegrationArgs, + protected readonly options?: PageIntegrationOptions + ) { + super(args, 'Page'); } /** diff --git a/packages/glsp-playwright/src/integration/standalone/standalone.integration.ts b/packages/glsp-playwright/src/integration/standalone/standalone.integration.ts index c31bebf..10dc4cf 100644 --- a/packages/glsp-playwright/src/integration/standalone/standalone.integration.ts +++ b/packages/glsp-playwright/src/integration/standalone/standalone.integration.ts @@ -13,10 +13,9 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import type { Page } from '@playwright/test'; import { SVGMetadataUtils } from '~/glsp'; import { Integration } from '../integration.base'; -import type { IntegrationType } from '../integration.type'; +import type { IntegrationArgs } from '../integration.type'; import type { StandaloneIntegrationOptions } from './standalone.options'; /** @@ -24,10 +23,13 @@ import type { StandaloneIntegrationOptions } from './standalone.options'; * with the standalone version of the GLSP-Client. */ export class StandaloneIntegration extends Integration { - readonly type: IntegrationType = 'Standalone'; + override page = this.args.page; - constructor(public readonly page: Page, protected readonly options: StandaloneIntegrationOptions) { - super(); + constructor( + args: IntegrationArgs, + protected readonly options: StandaloneIntegrationOptions + ) { + super(args, 'Standalone'); } /** diff --git a/packages/glsp-playwright/src/integration/theia/po/theia-glsp-app.po.ts b/packages/glsp-playwright/src/integration/theia/po/theia-glsp-app.po.ts index a47fd56..a69b5f1 100644 --- a/packages/glsp-playwright/src/integration/theia/po/theia-glsp-app.po.ts +++ b/packages/glsp-playwright/src/integration/theia/po/theia-glsp-app.po.ts @@ -14,7 +14,7 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { TheiaApp } from '@theia/playwright'; +import { TheiaApp, TheiaEditor } from '@theia/playwright'; import type { TheiaIntegrationOptions } from '../theia.options'; /** @@ -30,4 +30,13 @@ export class TheiaGLSPApp extends TheiaApp { initialize(options: TheiaIntegrationOptions): void { this._options = options; } + + override openEditor( + filePath: string, + editorFactory: new (editorFilePath: string, app: TheiaGLSPApp) => T, + editorName?: string | undefined, + expectFileNodes?: boolean | undefined + ): Promise { + return super.openEditor(filePath, editorFactory as new (f: string, a: TheiaApp) => T, editorName, expectFileNodes); + } } diff --git a/packages/glsp-playwright/src/integration/theia/theia.integration.ts b/packages/glsp-playwright/src/integration/theia/theia.integration.ts index 1ce428c..67e6ebc 100644 --- a/packages/glsp-playwright/src/integration/theia/theia.integration.ts +++ b/packages/glsp-playwright/src/integration/theia/theia.integration.ts @@ -14,11 +14,11 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import type { Page } from '@playwright/test'; -import { TheiaApp, TheiaWorkspace } from '@theia/playwright'; +import { Page } from '@playwright/test'; +import { TheiaAppLoader, TheiaWorkspace } from '@theia/playwright'; import { SVGMetadataUtils } from '~/glsp'; import { Integration } from '../integration.base'; -import type { IntegrationType } from '../integration.type'; +import type { IntegrationArgs } from '../integration.type'; import { TheiaGLSPApp } from './po/theia-glsp-app.po'; import { TheiaGLSPEditor } from './po/theia-glsp-editor.po'; import { TheiaIntegrationOptions } from './theia.options'; @@ -28,24 +28,28 @@ import { TheiaIntegrationOptions } from './theia.options'; * with the Theia version of the GLSP-Client. */ export class TheiaIntegration extends Integration { - readonly type: IntegrationType = 'Theia'; + protected theiaApp: TheiaGLSPApp; - constructor(public readonly page: Page, protected readonly options: TheiaIntegrationOptions) { - super(); + override get page(): Page { + return this.theiaApp.page; + } + + constructor( + args: IntegrationArgs, + protected readonly options: TheiaIntegrationOptions + ) { + super(args, 'Theia'); } protected override async launch(): Promise { - await this.page.goto(this.options.url); + const ws = new TheiaWorkspace(this.options.workspace ? [this.options.workspace] : undefined); + this.theiaApp = await TheiaAppLoader.load(this.args, ws, TheiaGLSPApp as any); + this.theiaApp.initialize(this.options); } protected override async afterLaunch(): Promise { - const ws = this.options.workspace ? new TheiaWorkspace([this.options.workspace]) : undefined; - - const theiaApp = await TheiaApp.loadApp(this.page as any, TheiaGLSPApp, ws); - theiaApp.initialize(this.options); - if (this.options.file) { - await theiaApp.openEditor(this.options.file, TheiaGLSPEditor as any); + await this.theiaApp.openEditor(this.options.file, TheiaGLSPEditor); await this.assertMetadataAPI(); await this.page.waitForSelector(`${SVGMetadataUtils.typeAttrOf('graph')} svg.sprotty-graph > g`); } diff --git a/packages/glsp-playwright/src/integration/vscode/vscode.integration.ts b/packages/glsp-playwright/src/integration/vscode/vscode.integration.ts index e88b634..d3b9fe4 100644 --- a/packages/glsp-playwright/src/integration/vscode/vscode.integration.ts +++ b/packages/glsp-playwright/src/integration/vscode/vscode.integration.ts @@ -22,7 +22,7 @@ import * as platformPath from 'path'; import { v4 as uuidv4 } from 'uuid'; import { SVGMetadataUtils } from '~/glsp'; import { Integration } from '../integration.base'; -import type { IntegrationType } from '../integration.type'; +import type { IntegrationArgs } from '../integration.type'; import { VSCodeWorkbenchActivitybar } from './po/workbench-activitybar.po'; import type { VSCodeIntegrationOptions } from './vscode.options'; import { VSCodeStorage } from './vscode.storage'; @@ -55,15 +55,18 @@ interface RunPaths { * and the configuration will be saved in the temp folder. */ export class VSCodeIntegration extends Integration { - readonly type: IntegrationType = 'VSCode'; + protected _page: Page; workbenchActivitybar: VSCodeWorkbenchActivitybar; protected runConfig: VSCodeRunConfig; protected electronApp: ElectronApplication; protected storage: VSCodeStorage.Storage; - constructor(protected readonly options: VSCodeIntegrationOptions) { - super(); + constructor( + args: IntegrationArgs, + protected readonly options: VSCodeIntegrationOptions + ) { + super(args, 'VSCode'); } override async initialize(): Promise { @@ -76,8 +79,8 @@ export class VSCodeIntegration extends Integration { } } - get page(): Page { - return this.electronApp.windows()[0]; + override get page(): Page { + return this._page; } override prefixRootSelector(selector: string): Locator { @@ -112,25 +115,13 @@ export class VSCodeIntegration extends Integration { this.options.workspace ] }); - - this.electronApp.on('window', async page => { - page.on('pageerror', error => { - console.error(error); - }); - - page.on('console', msg => { - if (this.options.isConsoleLogEnabled) { - console.log(msg.text()); - } - }); - }); } protected override async afterLaunch(): Promise { - const ePage = await this.electronApp.firstWindow(); - await ePage.waitForLoadState('domcontentloaded'); + this._page = await this.electronApp.firstWindow(); + await this.page.waitForLoadState('domcontentloaded'); - this.workbenchActivitybar = new VSCodeWorkbenchActivitybar(ePage); + this.workbenchActivitybar = new VSCodeWorkbenchActivitybar(this.page); await this.waitForReady(); diff --git a/packages/glsp-playwright/src/integration/vscode/vscode.setup.ts b/packages/glsp-playwright/src/integration/vscode/vscode.setup.ts index d6e8199..3ca628f 100644 --- a/packages/glsp-playwright/src/integration/vscode/vscode.setup.ts +++ b/packages/glsp-playwright/src/integration/vscode/vscode.setup.ts @@ -85,6 +85,7 @@ export class VSCodeSetup { const extensionArg = defaultArgs[0].split(/=(.*)/s); const extensionDir = extensionArg[1]; + let status: number | undefined; try { const extensionsFile = `${extensionDir}/extensions.json`; @@ -94,8 +95,8 @@ export class VSCodeSetup { const entry = installedExtensions.find(e => e.identifier.id === this.integrationOptions.vsixId); if (entry === undefined) { - this.installExtension(cli, extensionArg, this.integrationOptions.vsixPath); - this.log('[Extension] Extension installed'); + const spawn = this.installExtension(cli, extensionArg, this.integrationOptions.vsixPath); + status = spawn.status ?? -1; } else { const stat = await fs.stat(this.integrationOptions.vsixPath); if (entry.metadata.installedTimestamp < stat.birthtimeMs) { @@ -105,8 +106,8 @@ export class VSCodeSetup { new Date(stat.birthtimeMs).toISOString() ); this.deleteExtension(cli, extensionArg, this.integrationOptions.vsixId); - this.installExtension(cli, extensionArg, this.integrationOptions.vsixPath); - this.log('[Extension] Extension installed'); + const spawn = this.installExtension(cli, extensionArg, this.integrationOptions.vsixPath); + status = spawn.status ?? -1; } else { this.log('[Extension] Extension already installed. Skipping install'); } @@ -114,8 +115,16 @@ export class VSCodeSetup { } catch (ex) { this.log('[Extension] Proceed with clean install.'); this.deleteExtension(cli, extensionArg, this.integrationOptions.vsixId); - this.installExtension(cli, extensionArg, this.integrationOptions.vsixPath); - this.log('[Extension] Extension installed'); + const spawn = this.installExtension(cli, extensionArg, this.integrationOptions.vsixPath); + status = spawn.status ?? -1; + } + + if (status) { + if (status === 0) { + this.log('[Extension] Extension installed'); + } else { + throw new Error('[Extension] Extension install failed - Check logs'); + } } } @@ -125,17 +134,17 @@ export class VSCodeSetup { } } - protected deleteExtension(cli: string, args: string[], vsixId: string): void { + protected deleteExtension(cli: string, args: string[], vsixId: string): cp.SpawnSyncReturns { this.log('[Extension] Delete:', vsixId); - cp.spawnSync(cli, [...args, '--uninstall-extension', vsixId], { + return cp.spawnSync(cli, [...args, '--uninstall-extension', vsixId], { encoding: 'utf-8', stdio: 'inherit' }); } - protected installExtension(cli: string, args: string[], vsixPath: string): void { + protected installExtension(cli: string, args: string[], vsixPath: string): cp.SpawnSyncReturns { this.log('[Extension] Install:', vsixPath); - cp.spawnSync(cli, [...args, '--install-extension', vsixPath], { + return cp.spawnSync(cli, [...args, '--install-extension', vsixPath], { encoding: 'utf-8', stdio: 'inherit' }); diff --git a/packages/glsp-playwright/src/test.ts b/packages/glsp-playwright/src/test.ts index 562ad3e..50b7c41 100644 --- a/packages/glsp-playwright/src/test.ts +++ b/packages/glsp-playwright/src/test.ts @@ -16,6 +16,7 @@ import { test as base } from '@playwright/test'; import { Integration, + IntegrationArgs, IntegrationOptions, IntegrationType, PageIntegration, @@ -47,7 +48,7 @@ export interface GLSPPlaywrightFixtures { * * ```ts * test.beforeEach(async ({ integration }) => { - * app = await GLSPApp.loadApp(WorkflowApp, { + * app = new GLSPApp({ * type: 'integration', * integration * }); @@ -69,22 +70,27 @@ export const test = base.extend( } }, - integration: async ({ page, integrationOptions }, use) => { + integration: async ({ playwright, browser, page, integrationOptions }, use) => { + const args: IntegrationArgs = { + playwright, + browser, + page + }; + if (integrationOptions) { let integration: Integration; - switch (integrationOptions.type) { case 'Page': - integration = new PageIntegration(page, integrationOptions); + integration = new PageIntegration(args, integrationOptions); break; case 'Standalone': - integration = new StandaloneIntegration(page, integrationOptions); + integration = new StandaloneIntegration(args, integrationOptions); break; case 'Theia': - integration = new TheiaIntegration(page, integrationOptions); + integration = new TheiaIntegration(args, integrationOptions); break; case 'VSCode': { - integration = new VSCodeIntegration(integrationOptions); + integration = new VSCodeIntegration(args, integrationOptions); break; } default: { @@ -97,7 +103,7 @@ export const test = base.extend( await integration.start(); await use(integration); } else { - const integration = new PageIntegration(page); + const integration = new PageIntegration(args); await integration.initialize(); await integration.start(); await use(integration); @@ -120,5 +126,9 @@ export function createParameterizedIntegrationData(options: { }; } +export function skipIntegration(integrationOptions?: IntegrationOptions, ...integration: IntegrationType[]): boolean { + return integrationOptions === undefined || integration.includes(integrationOptions.type); +} + export { expect } from '@playwright/test'; export { test as setup }; diff --git a/packages/glsp-playwright/src/utils/ts.utils.ts b/packages/glsp-playwright/src/utils/ts.utils.ts index bfd3540..e6669c3 100644 --- a/packages/glsp-playwright/src/utils/ts.utils.ts +++ b/packages/glsp-playwright/src/utils/ts.utils.ts @@ -45,15 +45,12 @@ export function defined(value: T | undefined | null): T { return value; } -/** - * - * @param locator Locator of the element - * @param attr - * @returns - */ export async function definedGLSPAttr(locator: GLSPLocator, attr: string): Promise { const o = await locator.locate().getAttribute(attr); if (!isDefined(o)) { + // Provide additional information + console.error('=========== Element =========='); + console.error(await locator.locate().evaluate(elem => elem.outerHTML)); throw Error(`Attribute ${attr} is not defined for the selector.`); } @@ -63,6 +60,9 @@ export async function definedGLSPAttr(locator: GLSPLocator, attr: string): Promi export async function definedAttr(locator: Locator, attr: string): Promise { const o = await locator.getAttribute(attr); if (!isDefined(o)) { + // Provide additional information + console.error('=========== Element =========='); + console.error(await locator.evaluate(elem => elem.outerHTML)); throw Error(`Attribute ${attr} is not defined for the selector`); } diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json index 10da575..bbc123f 100644 --- a/tsconfig.eslint.json +++ b/tsconfig.eslint.json @@ -9,6 +9,7 @@ "examples/*/src", "examples/*/tests", "examples/*/scripts", - "examples/*/*.config.ts" + "examples/*/*.config.ts", + "examples/*/configs/*.ts" ] } diff --git a/yarn.lock b/yarn.lock index ae63c92..062a4de 100644 --- a/yarn.lock +++ b/yarn.lock @@ -922,19 +922,12 @@ picocolors "^1.0.0" tslib "^2.6.0" -"@playwright/test@^1.32.1": - version "1.41.2" - resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.41.2.tgz#bd9db40177f8fd442e16e14e0389d23751cdfc54" - integrity sha512-qQB9h7KbibJzrDpkXkYvsmiDJK14FULCCZgEcoe2AvFAS64oCirWTwzTlAYEbKaRxWs5TFesE1Na6izMv3HfGg== +"@playwright/test@^1.37.1", "@playwright/test@^1.40.1": + version "1.40.1" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.40.1.tgz#9e66322d97b1d74b9f8718bacab15080f24cde65" + integrity sha512-EaaawMTOeEItCRvfmkI9v6rBkF1svM8wjl/YPRrg2N2Wmp+4qJYkWtJsbew1szfKKDm6fPLy4YAanBhIlf9dWw== dependencies: - playwright "1.41.2" - -"@playwright/test@^1.34.3": - version "1.39.0" - resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.39.0.tgz#d10ba8e38e44104499e25001945f07faa9fa91cd" - integrity sha512-3u1iFqgzl7zr004bGPYiN/5EZpRUSFddQBra8Rqll5N0/vfpqlP9I9EXqAoGacuAbX6c9Ulg/Cjqglp5VkK6UQ== - dependencies: - playwright "1.39.0" + playwright "1.40.1" "@sigstore/bundle@^1.1.0": version "1.1.0" @@ -1005,12 +998,12 @@ resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz#5981a8db18b56ba38ef0efb7d995b12aa7b51918" integrity sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ== -"@theia/playwright@1.39.0": - version "1.39.0" - resolved "https://registry.yarnpkg.com/@theia/playwright/-/playwright-1.39.0.tgz#e74da1ac05dafeda134f086ccc54f2cb70135625" - integrity sha512-P6e5mfYINiaA3tcQ5o7Z1Yc9vZJ0eWPTnSnFrDAwkgzP2AaDiSbr2ZUeuiyFvQBoZNzfpxJ6csheYkA/LMd/OA== +"@theia/playwright@~1.45.0": + version "1.45.1" + resolved "https://registry.yarnpkg.com/@theia/playwright/-/playwright-1.45.1.tgz#c1447ffd48bfc61dfaafbf9669bdc6f8d0feedec" + integrity sha512-Kta1ChkyHgsFppjSYvun6+NRayukHTjnIsX5SLpB8PwlWMo6ftz6oGP9XWgoeXNvg2ldoTjQGNNAjjr8Ku0YyA== dependencies: - "@playwright/test" "^1.32.1" + "@playwright/test" "^1.37.1" fs-extra "^9.0.8" "@tootallnate/once@1": @@ -4603,35 +4596,6 @@ mute-stream@~1.0.0: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-1.0.0.tgz#e31bd9fe62f0aed23520aa4324ea6671531e013e" integrity sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA== -mvn-artifact-download@5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/mvn-artifact-download/-/mvn-artifact-download-5.1.0.tgz#332c89992196e307cd00768e0fcb29cdb30a814a" - integrity sha512-8Cdikbcotum2TZDI6bn3DtYYKIyjFKTDbM9CCrSLWVkAa8fd5s5Di1ti30I0J3vFYx+a2ZCcnaqNSgfvxZFMxQ== - dependencies: - mvn-artifact-filename "^5.1.0" - mvn-artifact-name-parser "^5.0.1" - mvn-artifact-url "^5.1.0" - node-fetch "^2.6.0" - -mvn-artifact-filename@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/mvn-artifact-filename/-/mvn-artifact-filename-5.1.0.tgz#722e548b2a517a2af2ff6982e2952cde91346f55" - integrity sha512-HgChSCBgeTQhWw4ELf0SDIyE8eok2A328aPq6BkPbJc5Pv9HNoREwhw887LnNXpNKty+xaE84DR3IguW+ct5Zw== - -mvn-artifact-name-parser@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/mvn-artifact-name-parser/-/mvn-artifact-name-parser-5.0.1.tgz#e2467a4809a1cc4285e80b4344ff06daef3540a0" - integrity sha512-CyWPiWAe3Ubnf9joDLg9cIYoGu/QZXg926qXtohXdx/B/ZHTG/hXwtKt/vmezRw68irmUxAYRYCQCuolmgNLAA== - -mvn-artifact-url@^5.1.0: - version "5.1.1" - resolved "https://registry.yarnpkg.com/mvn-artifact-url/-/mvn-artifact-url-5.1.1.tgz#3634c33e7f73d3482c2a6f25faf9f4790052603a" - integrity sha512-vtVMlWaLcXkiKnc/YSE3azKwfxDCMdwoaHSb7Yj5lcX0yEsC+VwKI49V7+Rvl261BjNQrm2MzJ5NbmDxRaQJZw== - dependencies: - mvn-artifact-filename "^5.1.0" - node-fetch "^2.6.0" - xml2js "^0.4.23" - mylas@^2.1.9: version "2.1.13" resolved "https://registry.yarnpkg.com/mylas/-/mylas-2.1.13.tgz#1e23b37d58fdcc76e15d8a5ed23f9ae9fc0cbdf4" @@ -4680,7 +4644,7 @@ node-fetch@2.6.7: dependencies: whatwg-url "^5.0.0" -node-fetch@^2.6.0, node-fetch@^2.6.11, node-fetch@^2.6.7: +node-fetch@^2.6.11, node-fetch@^2.6.7: version "2.7.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== @@ -5373,31 +5337,17 @@ pkg-dir@^4.1.0, pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" -playwright-core@1.39.0: - version "1.39.0" - resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.39.0.tgz#efeaea754af4fb170d11845b8da30b2323287c63" - integrity sha512-+k4pdZgs1qiM+OUkSjx96YiKsXsmb59evFoqv8SKO067qBA+Z2s/dCzJij/ZhdQcs2zlTAgRKfeiiLm8PQ2qvw== - -playwright-core@1.41.2: - version "1.41.2" - resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.41.2.tgz#db22372c708926c697acc261f0ef8406606802d9" - integrity sha512-VaTvwCA4Y8kxEe+kfm2+uUUw5Lubf38RxF7FpBxLPmGe5sdNkSg5e3ChEigaGrX7qdqT3pt2m/98LiyvU2x6CA== - -playwright@1.39.0: - version "1.39.0" - resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.39.0.tgz#184c81cd6478f8da28bcd9e60e94fcebf566e077" - integrity sha512-naE5QT11uC/Oiq0BwZ50gDmy8c8WLPRTEWuSSFVG2egBka/1qMoSqYQcROMT9zLwJ86oPofcTH2jBY/5wWOgIw== - dependencies: - playwright-core "1.39.0" - optionalDependencies: - fsevents "2.3.2" +playwright-core@1.40.1: + version "1.40.1" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.40.1.tgz#442d15e86866a87d90d07af528e0afabe4c75c05" + integrity sha512-+hkOycxPiV534c4HhpfX6yrlawqVUzITRKwHAmYfmsVreltEl6fAZJ3DPfLMOODw0H3s1Itd6MDCWmP1fl/QvQ== -playwright@1.41.2: - version "1.41.2" - resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.41.2.tgz#4e760b1c79f33d9129a8c65cc27953be6dd35042" - integrity sha512-v0bOa6H2GJChDL8pAeLa/LZC4feoAMbSQm1/jF/ySsWWoaNItvrMP7GEkvEEFyCTUYKMxjQKaTSg5up7nR6/8A== +playwright@1.40.1: + version "1.40.1" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.40.1.tgz#a11bf8dca15be5a194851dbbf3df235b9f53d7ae" + integrity sha512-2eHI7IioIpQ0bS1Ovg/HszsN/XKNwEG1kbzSDDmADpclKc7CyqkHw7Mg2JCz/bbCxg25QUPcjksoMW7JcIFQmw== dependencies: - playwright-core "1.41.2" + playwright-core "1.40.1" optionalDependencies: fsevents "2.3.2" @@ -5795,11 +5745,6 @@ safe-regex-test@^1.0.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -sax@>=0.6.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/sax/-/sax-1.3.0.tgz#a5dbe77db3be05c9d1ee7785dbd3ea9de51593d0" - integrity sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA== - "semver@2 || 3 || 4 || 5", semver@^5.6.0: version "5.7.2" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" @@ -6817,24 +6762,11 @@ write-pkg@4.0.0: type-fest "^0.4.1" write-json-file "^3.2.0" -xml2js@^0.4.23: - version "0.4.23" - resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" - integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug== - dependencies: - sax ">=0.6.0" - xmlbuilder "~11.0.0" - xml@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5" integrity sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw== -xmlbuilder@~11.0.0: - version "11.0.1" - resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" - integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== - xtend@~4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"