diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c1d7dd6..453f47f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,6 +56,7 @@ jobs: env: THEIA_URL: 'http://localhost:3000' VSCODE_VSIX_ID: 'eclipse-glsp.workflow-vscode-example' + GLSP_SERVER_TYPE: 'node' GLSP_SERVER_DEBUG: 'true' GLSP_SERVER_PORT: '8081' GLSP_SERVER_PLAYWRIGHT_MANAGED: 'true' @@ -87,13 +88,13 @@ jobs: path: examples/workflow-test/playwright-report/ playwright-java: - if: false name: Playwright Tests (Java server) timeout-minutes: 120 runs-on: ubuntu-latest env: THEIA_URL: 'http://localhost:3000' VSCODE_VSIX_ID: 'eclipse-glsp.workflow-vscode-example' + GLSP_SERVER_TYPE: 'java' GLSP_SERVER_DEBUG: 'true' GLSP_SERVER_PORT: '8081' GLSP_SERVER_PLAYWRIGHT_MANAGED: 'true' diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 9067be9..94a863f 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -21,6 +21,7 @@ jobs: env: THEIA_URL: 'http://localhost:3000' VSCODE_VSIX_ID: 'eclipse-glsp.workflow-vscode-example' + GLSP_SERVER_TYPE: 'node' GLSP_SERVER_DEBUG: 'true' GLSP_SERVER_PORT: '8081' GLSP_SERVER_PLAYWRIGHT_MANAGED: 'true' diff --git a/examples/workflow-test/.env.example b/examples/workflow-test/.env.example index 3a83020..854c92e 100644 --- a/examples/workflow-test/.env.example +++ b/examples/workflow-test/.env.example @@ -2,6 +2,7 @@ GLSP_SERVER_DEBUG="true" # For Theia and VSCode instances to connect to an exter GLSP_SERVER_PORT="8081" GLSP_SERVER_PLAYWRIGHT_MANAGED="true" # To allow Playwright to manage/start the server GLSP_WEBSOCKET_PATH="workflow" +GLSP_SERVER_TYPE="node" # To use the node server # Configurations STANDALONE_URL="file:///.../repositories/glsp-client/examples/workflow-standalone/app/diagram.html" diff --git a/examples/workflow-test/src/server/index.ts b/examples/workflow-test/src/server/index.ts new file mode 100644 index 0000000..efcdd9e --- /dev/null +++ b/examples/workflow-test/src/server/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 const GLSP_SERVER_TYPE_NODE = 'node'; +export const GLSP_SERVER_TYPE_JAVA = 'java'; 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 0290171..581f7c5 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 @@ -14,11 +14,14 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ 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'; @@ -27,14 +30,16 @@ test.describe('The command palette', () => { let app: WorkflowApp; let graph: WorkflowGraph; let globalCommandPalette: GLSPGlobalCommandPalette; + let server: GLSPServer; - test.beforeEach(async ({ integration }) => { + test.beforeEach(async ({ integration, glspServer }) => { app = new WorkflowApp({ type: 'integration', integration }); graph = app.graph; globalCommandPalette = app.globalCommandPalette; + server = glspServer; }); test.describe('in the global context', () => { @@ -45,23 +50,43 @@ test.describe('The command palette', () => { expect(await globalCommandPalette.isVisible()).toBeTruthy(); const suggestions = await globalCommandPalette.suggestions(); - const expectedSuggestions = [ - 'Create Manual Task', - 'Create Category', - 'Create Automated Task', - 'Create Merge Node', - 'Create Decision Node', - 'Delete All', - 'Reveal Push', - 'Reveal ChkWt', - 'Reveal WtOK', - 'Reveal RflWt', - 'Reveal Brew', - 'Reveal ChkTp', - 'Reveal KeepTp', - 'Reveal PreHeat' - ]; - expect(suggestions.sort()).toEqual(expectedSuggestions.sort()); + const expectedSuggestions = new ServerVariable({ + server, + value: { + [GLSP_SERVER_TYPE_NODE]: [ + 'Create Manual Task', + 'Create Category', + 'Create Automated Task', + 'Create Merge Node', + 'Create Decision Node', + 'Delete All', + 'Reveal Push', + 'Reveal ChkWt', + 'Reveal WtOK', + 'Reveal RflWt', + 'Reveal Brew', + 'Reveal ChkTp', + 'Reveal KeepTp', + 'Reveal PreHeat' + ], + [GLSP_SERVER_TYPE_JAVA]: [ + 'Create Manual Task', + 'Create Category', + 'Create Automated Task', + 'Create Merge Node', + 'Create Decision Node', + 'Reveal Push', + 'Reveal ChkWt', + 'Reveal WtOK', + 'Reveal RflWt', + 'Reveal Brew', + 'Reveal ChkTp', + 'Reveal KeepTp', + 'Reveal PreHeat' + ] + } + }); + expect(suggestions.sort()).toEqual(expectedSuggestions.get().sort()); await globalCommandPalette.search('Create'); const createSuggestions = await globalCommandPalette.suggestions(); diff --git a/examples/workflow-test/tests/features/hover/popup.spec.ts b/examples/workflow-test/tests/features/hover/popup.spec.ts index d2b5238..bfe3394 100644 --- a/examples/workflow-test/tests/features/hover/popup.spec.ts +++ b/examples/workflow-test/tests/features/hover/popup.spec.ts @@ -23,37 +23,64 @@ import { 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'; import { dedent } from 'ts-dedent'; import { WorkflowApp } from '../../../src/app/workflow-app'; 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 manualLabel = 'Push'; -const expectedManualPopupText = dedent`Push -Type: manual -Duration: undefined -Reference: undefined +const expectedManualPopupText = new ServerVariable({ + value: { + [GLSP_SERVER_TYPE_NODE]: dedent`Push + Type: manual + Duration: undefined + Reference: undefined + + `, + [GLSP_SERVER_TYPE_JAVA]: dedent`Push + + Type: manual + Duration: 0 + Reference: null + + ` + } +}); -`; const automatedLabel = 'ChkWt'; -const expectedAutomatedPopupText = dedent`ChkWt -Type: automated -Duration: undefined -Reference: undefined - -`; +const expectedAutomatedPopupText = new ServerVariable({ + value: { + [GLSP_SERVER_TYPE_NODE]: dedent`ChkWt + Type: automated + Duration: undefined + Reference: undefined + + `, + [GLSP_SERVER_TYPE_JAVA]: dedent`ChkWt + + Type: automated + Duration: 0 + Reference: null + + ` + } +}); test.describe('The popup', () => { let app: WorkflowApp; let graph: WorkflowGraph; - test.beforeEach(async ({ integration }) => { + test.beforeEach(async ({ integration, glspServer }) => { app = new WorkflowApp({ type: 'integration', integration }); graph = app.graph; + expectedManualPopupText.setServer(glspServer); + expectedAutomatedPopupText.setServer(glspServer); }); test('should be shown on hovering a task manual', async () => { @@ -65,7 +92,7 @@ test.describe('The popup', () => { await expect(app.popup.locate()).toBeVisible(); const popup = task.popup(); - expect(await popup.innerText()).toBe(expectedManualPopupText); + expect(await popup.innerText()).toBe(expectedManualPopupText.get()); }); test('should allow to access the text directly in elements', async () => { @@ -73,13 +100,13 @@ test.describe('The popup', () => { await expect(app.popup.locate()).toBeHidden(); const text = await task.popupText(); await expect(app.popup.locate()).toBeVisible(); - expect(text).toBe(expectedManualPopupText); + expect(text).toBe(expectedManualPopupText.get()); }); test.describe('should be closed on', () => { test('escape', async () => { await app.graph.focus(); - await assertPopup(app, manualLabel, TaskManual, expectedManualPopupText); + await assertPopup(app, manualLabel, TaskManual, expectedManualPopupText.get()); await app.page.keyboard.press('Escape'); await app.popup.waitForHidden(); @@ -88,15 +115,15 @@ test.describe('The popup', () => { }); test('new hover', async () => { - await assertPopup(app, manualLabel, TaskManual, expectedManualPopupText); + await assertPopup(app, manualLabel, TaskManual, expectedManualPopupText.get()); await app.popup.close(); - await assertPopup(app, automatedLabel, TaskAutomated, expectedAutomatedPopupText); + await assertPopup(app, automatedLabel, TaskAutomated, expectedAutomatedPopupText.get()); }); test('mouse moved away', async () => { - await assertPopup(app, manualLabel, TaskManual, expectedManualPopupText); + await assertPopup(app, manualLabel, TaskManual, expectedManualPopupText.get()); const bounds = await app.graph.bounds(); await bounds.position('middle_center').move(); @@ -106,7 +133,7 @@ test.describe('The popup', () => { }); test('focus lost', async () => { - const task = await assertPopup(app, manualLabel, TaskManual, expectedManualPopupText); + const task = await assertPopup(app, manualLabel, TaskManual, expectedManualPopupText.get()); await app.graph.locate().click(); await app.popup.waitForHidden(); @@ -116,7 +143,7 @@ test.describe('The popup', () => { test('context menu', async ({ integrationOptions }) => { test.skip(skipNonIntegration(integrationOptions, 'Theia'), 'Only within Theia supported'); - await assertPopup(app, manualLabel, TaskManual, expectedManualPopupText); + await assertPopup(app, manualLabel, TaskManual, expectedManualPopupText.get()); await app.contextMenu.open(); await app.popup.waitForHidden(); @@ -125,7 +152,7 @@ test.describe('The popup', () => { }); test('center command', async ({ integration }) => { - await assertPopup(app, manualLabel, TaskManual, expectedManualPopupText); + await assertPopup(app, manualLabel, TaskManual, expectedManualPopupText.get()); await graph.focus(); await runInIntegration( integration, @@ -143,7 +170,7 @@ test.describe('The popup', () => { }); test('fit to screen command', async ({ integration }) => { - await assertPopup(app, manualLabel, TaskManual, expectedManualPopupText); + await assertPopup(app, manualLabel, TaskManual, expectedManualPopupText.get()); await graph.focus(); await runInIntegration( integration, @@ -161,7 +188,7 @@ test.describe('The popup', () => { }); test('layout command', async ({ integration }) => { - await assertPopup(app, manualLabel, TaskManual, expectedManualPopupText); + await assertPopup(app, manualLabel, TaskManual, expectedManualPopupText.get()); await graph.focus(); await runInIntegration( integration, 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 62c2f0d..c27ffee 100644 --- a/examples/workflow-test/tests/features/validation/marker-navigator.spec.ts +++ b/examples/workflow-test/tests/features/validation/marker-navigator.spec.ts @@ -13,15 +13,9 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { - MarkerNavigator, - StandaloneMarkerNavigator, - TheiaMarkerNavigator, - createParameterizedIntegrationData, - expect, - test -} from '@eclipse-glsp/glsp-playwright/'; +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 { WorkflowApp } from '../../../src/app/workflow-app'; import { TaskAutomated } from '../../../src/graph/elements/task-automated.po'; import { WorkflowGraph } from '../../../src/graph/workflow.graph'; @@ -47,24 +41,24 @@ test.describe('The marker navigator', () => { integration }); graph = app.graph; - navigator = createParameterizedIntegrationData<() => MarkerNavigator>({ - default: () => { - throw new Error('Integration not supported for marker navigator'); + const navigatorProvider = new IntegrationVariable({ + value: { + Standalone: new StandaloneMarkerNavigator(app), + Theia: new TheiaMarkerNavigator(app) }, - override: { - Standalone: () => new StandaloneMarkerNavigator(app), - Theia: () => new TheiaMarkerNavigator(app) - } - })[integration.type](); + integration + }); + + navigator = navigatorProvider.get(); await navigator.trigger(); }); // VSCode has no support for navigation - test.skip(({ integrationOptions }) => skipIntegration(integrationOptions, 'VSCode')); + test.skip(({ integrationOptions }) => skipIntegration(integrationOptions, 'VSCode'), 'Not supported'); test('should navigate to the first element', async ({ integrationOptions }) => { - test.skip(skipIntegration(integrationOptions, 'VSCode')); + test.skip(skipIntegration(integrationOptions, 'VSCode'), 'Not supported'); await navigator.navigateForward(); await app.page.pause(); diff --git a/packages/glsp-playwright/src/glsp-server/index.ts b/packages/glsp-playwright/src/glsp-server/index.ts new file mode 100644 index 0000000..ddb9881 --- /dev/null +++ b/packages/glsp-playwright/src/glsp-server/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 './server'; diff --git a/packages/glsp-playwright/src/glsp-server/server.ts b/packages/glsp-playwright/src/glsp-server/server.ts new file mode 100644 index 0000000..e64f08a --- /dev/null +++ b/packages/glsp-playwright/src/glsp-server/server.ts @@ -0,0 +1,21 @@ +/******************************************************************************** + * 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 interface GLSPServer { + type: string; +} + +export const GLSP_SERVER_TYPE_UNKNWON = 'unknown'; diff --git a/packages/glsp-playwright/src/index.ts b/packages/glsp-playwright/src/index.ts index 0956412..c942647 100644 --- a/packages/glsp-playwright/src/index.ts +++ b/packages/glsp-playwright/src/index.ts @@ -17,6 +17,7 @@ export * from './debug'; export * from './extension'; export * from './glsp'; +export * from './glsp-server'; export * from './integration'; export * from './remote'; export * from './test'; diff --git a/packages/glsp-playwright/src/test.ts b/packages/glsp-playwright/src/test.ts index c464909..a4aab90 100644 --- a/packages/glsp-playwright/src/test.ts +++ b/packages/glsp-playwright/src/test.ts @@ -25,6 +25,7 @@ import { VSCodeIntegration, VSCodeSetup } from '~/integration'; +import { GLSP_SERVER_TYPE_UNKNWON, GLSPServer } from './glsp-server'; /** * GLSP-Playwright specific options @@ -56,6 +57,7 @@ export interface GLSPPlaywrightFixtures { * ``` */ integration: Integration; + glspServer: GLSPServer; vscodeSetup?: VSCodeSetup; } @@ -70,6 +72,14 @@ export const test = base.extend( } }, + glspServer: async ({ page }, use) => { + const server: GLSPServer = { + type: process.env.GLSP_SERVER_TYPE ?? GLSP_SERVER_TYPE_UNKNWON + }; + + await use(server); + }, + integration: async ({ playwright, browser, page, integrationOptions }, use) => { const args: IntegrationArgs = { playwright, @@ -111,21 +121,6 @@ export const test = base.extend( } }); -// https://playwright.dev/docs/test-parameterize - -export type ParameterizedIntegrationData = Record; -export function createParameterizedIntegrationData(options: { - default: T; - override?: Partial>; -}): ParameterizedIntegrationData { - return { - Page: options.override?.Page ?? options.default, - Standalone: options.override?.Standalone ?? options.default, - Theia: options.override?.Theia ?? options.default, - VSCode: options.override?.VSCode ?? options.default - }; -} - /** * Runs the given callback if the active integration is the same as the provided integration type */ @@ -173,4 +168,5 @@ export function skipIntegration(integrationOptions?: IntegrationOptions, ...inte } export { expect } from './test/assertions'; +export { DynamicVariable } from './test/dynamic-variable'; export { test as setup }; diff --git a/packages/glsp-playwright/src/test/dynamic-variable.ts b/packages/glsp-playwright/src/test/dynamic-variable.ts new file mode 100644 index 0000000..e5624cb --- /dev/null +++ b/packages/glsp-playwright/src/test/dynamic-variable.ts @@ -0,0 +1,89 @@ +/******************************************************************************** + * 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 { Integration, IntegrationType } from '~/integration'; +import type { GLSPServer } from '../glsp-server'; + +type RecordKey = string | number | symbol; + +export interface DynamicVariableOptions { + defaultValue?: TValue; + value?: Partial>; +} + +export interface DynamicVariable { + getOrThrow(key: TKey): TValue; +} + +export abstract class BaseDynamicVariable { + protected readonly value: Partial>; + protected readonly defaultValue?: TValue; + + constructor(protected readonly options: DynamicVariableOptions) { + this.defaultValue = options?.defaultValue; + this.value = options?.value ?? {}; + } + + getOrThrow(key: TKey): TValue { + const value = this.value[key] ?? this.defaultValue; + if (value === undefined) { + throw new Error(`No value found for key ${String(key)}`); + } + return value; + } +} + +export class IntegrationVariable extends BaseDynamicVariable { + protected integration?: Integration; + + constructor(options: DynamicVariableOptions & { integration?: Integration }) { + super(options); + this.integration = options.integration; + } + + get(): TValue { + if (this.integration === undefined) { + throw new Error('No integration set'); + } + + return super.getOrThrow(this.integration.type); + } + + setIntegration(integration: Integration): void { + this.integration = integration; + } +} + +export class ServerVariable extends BaseDynamicVariable { + protected server?: GLSPServer; + + constructor(options: DynamicVariableOptions & { server?: GLSPServer }) { + super(options); + this.server = options.server; + } + + get(): TValue { + if (this.server === undefined) { + throw new Error('No GLSP server set'); + } + + return super.getOrThrow(this.server.type); + } + + setServer(server: GLSPServer): void { + this.server = server; + } +}