Skip to content

Commit

Permalink
E2E tests for system diagram
Browse files Browse the repository at this point in the history
- Update dependency to GLSP playwright
- Implement  System Diagram PO based on glsp-playwright
- Adapt tool-palette providers and put all items in a default group. The group is hidden via css.
  This change makes it possible to easily resuse the toolbar PO of glsp-playwright
- Add e2e tests for system diagram and form editor scenarios
 - And rename all test components to use CM prefix instead of CrossModel
- Improve CompositeEditor and related POs to ensure that saving, dirty state handling, closing etc. are properly delegated to the parent editor
- Add Page objects for forms/property views
- Refactor e2e example workspace
- Extend graph metadata to allow waiting for a model update (i.e. model revision is increased) during e2e testing
  • Loading branch information
tortmayr committed Nov 25, 2024
1 parent 2c6314d commit fce5763
Show file tree
Hide file tree
Showing 48 changed files with 2,032 additions and 203 deletions.
5 changes: 3 additions & 2 deletions e2e-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@
"ui-test": "yarn build && yarn playwright test"
},
"dependencies": {
"@eclipse-glsp/glsp-playwright": "2.2.1",
"@eclipse-glsp/glsp-playwright": " 2.3.0-next.6",
"@playwright/test": "^1.37.1",
"@theia/playwright": "1.49.1"
"@theia/playwright": "1.49.1",
"ts-dedent": "^2.2.0"
},
"devDependencies": {
"allure-playwright": "^2.9.2"
Expand Down
2 changes: 2 additions & 0 deletions e2e-tests/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/********************************************************************************
* Copyright (c) 2023 CrossBreeze.
********************************************************************************/
import 'reflect-metadata';

import { defineConfig } from '@playwright/test';

export default defineConfig({
Expand Down
16 changes: 0 additions & 16 deletions e2e-tests/src/fixtures/crossmodel-fixture.ts

This file was deleted.

91 changes: 91 additions & 0 deletions e2e-tests/src/page-objects/cm-app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/********************************************************************************
* Copyright (c) 2023 CrossBreeze.
********************************************************************************/
import { IntegrationArgs, TheiaGLSPApp } from '@eclipse-glsp/glsp-playwright';
import { Page } from '@playwright/test';
import { TheiaEditor, TheiaNotificationIndicator, TheiaNotificationOverlay, TheiaWorkspace } from '@theia/playwright';
import { CMCompositeEditor, IntegratedEditorType } from './cm-composite-editor';
import { CMExplorerView } from './cm-explorer-view';
import { CMTheiaIntegration } from './cm-theia-integration';

export interface CMAppArgs extends Omit<IntegrationArgs, 'page'> {
workspaceUrl?: string;
baseUrl?: string;
}
export class CMApp extends TheiaGLSPApp {
public static async load(args: CMAppArgs): Promise<CMApp> {
const integration = new CMTheiaIntegration(
{ browser: args.browser, page: {} as any, playwright: args.playwright },
{
type: 'Theia',
workspace: args.workspaceUrl ?? 'src/resources/sample-workspace',
widgetId: '',
url: args.baseUrl ?? 'http://localhost:3000'
}
);
await integration.initialize();
await integration.start();
await integration.app.notificationOverlay.waitForEntry('Connected to Model Server on port');
await integration.app.notificationOverlay.waitForEntry('Connected to Graphical Server on port');
await integration.app.notificationOverlay.clearAllNotifications();

return integration.app;
}

readonly notificationIndicator: TheiaNotificationIndicator;
readonly notificationOverlay: TheiaNotificationOverlay;

public constructor(page: Page, workspace: TheiaWorkspace, isElectron: boolean) {
super(page, workspace, isElectron);
this.notificationIndicator = this.notificationIndicator = new TheiaNotificationIndicator(this);
this.notificationOverlay = this.notificationOverlay = new TheiaNotificationOverlay(this, this.notificationIndicator);
}

protected _integration: CMTheiaIntegration;

set integration(integration: CMTheiaIntegration) {
if (!this._integration) {
this._integration = integration;
} else {
console.warn('Integration already set');
}
}

get integration(): CMTheiaIntegration {
return this._integration;
}

async openExplorerView(): Promise<CMExplorerView> {
const explorer = await this.openView(CMExplorerView);
await explorer.waitForVisibleFileNodes();
return explorer;
}

async openCompositeEditor<T extends keyof IntegratedEditorType>(filePath: string, editorType: T): Promise<IntegratedEditorType[T]> {
const editor = await this.openEditor(filePath, CMCompositeEditor);
await editor.waitForVisible();
let integratedEditor: TheiaEditor | undefined = undefined;
if (editorType === 'Code Editor') {
integratedEditor = await editor.switchToCodeEditor();
} else if (editorType === 'Form Editor') {
integratedEditor = await editor.switchToFormEditor();
} else if (editorType === 'System Diagram') {
integratedEditor = await editor.switchToSystemDiagram();
} else if (editorType === 'Mapping Diagram') {
integratedEditor = await editor.switchToMappingDiagram();
}
if (integratedEditor === undefined) {
throw new Error(`Unknown editor type: ${editorType}`);
}
return integratedEditor as IntegratedEditorType[T];
}

override openEditor<T extends TheiaEditor>(
filePath: string,
editorFactory: new (editorFilePath: string, app: CMApp) => T,
editorName?: string | undefined,
expectFileNodes?: boolean | undefined
): Promise<T> {
return super.openEditor(filePath, editorFactory as new (f: string, a: TheiaGLSPApp) => T, editorName, expectFileNodes);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,26 @@
********************************************************************************/

import { ElementHandle, Page } from '@playwright/test';
import { isElementVisible, normalizeId, OSUtil, TheiaApp, TheiaEditor, TheiaTextEditor, urlEncodePath } from '@theia/playwright';
import { OSUtil, TheiaEditor, isElementVisible, normalizeId, urlEncodePath } from '@theia/playwright';
import { TheiaMonacoEditor } from '@theia/playwright/lib/theia-monaco-editor';
import { join } from 'path';
import { CMApp } from './cm-app';
import { IntegratedEditor, IntegratedTextEditor } from './cm-integrated-editor';
import { IntegratedFormEditor } from './form/integrated-form-editor';
import { IntegratedSystemDiagramEditor } from './system-diagram/integrated-system-diagram-editor';

export type CompositeEditorName = keyof IntegratedEditorType;
export interface IntegratedEditorType {
'Code Editor': IntegratedCodeEditor;
'Form Editor': IntegratedFormEditor;
'System Diagram': IntegratedSystemDiagramEditor;
'Mapping Diagram': IntegratedMappingDiagramEditor;
}

export type CompositeEditorName = 'Code Editor' | 'Form Editor' | 'System Diagram' | 'Mapping Diagram';

export class CrossModelCompositeEditor extends TheiaEditor {
export class CMCompositeEditor extends TheiaEditor {
constructor(
protected filePath: string,
app: TheiaApp
public override app: CMApp
) {
// shell-tab-code-editor-opener:file:///c%3A/Users/user/AppData/Local/Temp/cloud-ws-JBUhb6/sample.txt:1
// code-editor-opener:file:///c%3A/Users/user/AppData/Local/Temp/cloud-ws-JBUhb6/sample.txt:1
Expand Down Expand Up @@ -52,92 +62,60 @@ export class CrossModelCompositeEditor extends TheiaEditor {

async switchToCodeEditor(): Promise<IntegratedCodeEditor> {
await this.switchToEditor('Code Editor');
const textEditor = new IntegratedCodeEditor(this.filePath, this.app, this.editorTabSelector('Code Editor'));
await textEditor.waitForVisible();
const textEditor = new IntegratedCodeEditor(this.filePath, this, this.editorTabSelector('Code Editor'));
await textEditor.activate();
return textEditor;
}

async switchToFormEditor(): Promise<IntegratedFormEditor> {
await this.switchToEditor('Form Editor');
const formEditor = new IntegratedFormEditor(this.filePath, this.app, this.editorTabSelector('Form Editor'));
await formEditor.waitForVisible();
const formEditor = new IntegratedFormEditor(this.filePath, this, this.editorTabSelector('Form Editor'));
await formEditor.activate();

return formEditor;
}

async switchToSystemDiagram(): Promise<IntegratedSystemDiagramEditor> {
await this.switchToEditor('System Diagram');
const diagramEditor = new IntegratedSystemDiagramEditor(this.filePath, this.app, this.editorTabSelector('System Diagram'));
const diagramEditor = new IntegratedSystemDiagramEditor(this.filePath, this, this.editorTabSelector('System Diagram'));
await diagramEditor.waitForVisible();
await diagramEditor.activate();

return diagramEditor;
}

async switchToMappingDiagram(): Promise<IntegratedMappingDiagramEditor> {
await this.switchToEditor('Mapping Diagram');
const diagramEditor = new IntegratedMappingDiagramEditor(this.filePath, this.app, this.editorTabSelector('Mapping Diagram'));
const diagramEditor = new IntegratedMappingDiagramEditor(this.filePath, this, this.editorTabSelector('Mapping Diagram'));
await diagramEditor.waitForVisible();
await diagramEditor.activate();
return diagramEditor;
}
}

export class IntegratedCodeEditor extends TheiaTextEditor {
constructor(filePath: string, app: TheiaApp, tabSelector: string) {
export class IntegratedCodeEditor extends IntegratedTextEditor {
constructor(filePath: string, parent: CMCompositeEditor, tabSelector: string) {
// shell-tab-code-editor-opener:file:///c%3A/Users/user/AppData/Local/Temp/cloud-ws-JBUhb6/sample.txt:1
// code-editor-opener:file:///c%3A/Users/user/AppData/Local/Temp/cloud-ws-JBUhb6/sample.txt:1
super(filePath, app);
super(filePath, parent);
this.data.viewSelector = normalizeId(
`#code-editor-opener:file://${urlEncodePath(join(app.workspace.escapedPath, OSUtil.fileSeparator, filePath))}`
`#code-editor-opener:file://${urlEncodePath(join(this.app.workspace.escapedPath, OSUtil.fileSeparator, filePath))}`
);
this.data.tabSelector = tabSelector;
this.monacoEditor = new TheiaMonacoEditor(this.viewSelector, app);
this.monacoEditor = new TheiaMonacoEditor(this.viewSelector, parent.app);
}
}

export class IntegratedFormEditor extends TheiaEditor {
constructor(filePath: string, app: TheiaApp, tabSelector: string) {
export class IntegratedMappingDiagramEditor extends IntegratedEditor {
constructor(filePath: string, parent: CMCompositeEditor, tabSelector: string) {
super(
{
tabSelector,
viewSelector: normalizeId(
`#form-editor-opener:file://${urlEncodePath(join(app.workspace.escapedPath, OSUtil.fileSeparator, filePath))}`
`#mapping-diagram:file://${urlEncodePath(join(parent.app.workspace.escapedPath, OSUtil.fileSeparator, filePath))}`
)
},
app
);
}

async hasError(errorMessage: string): Promise<boolean> {
return hasViewError(this.page, this.viewSelector, errorMessage);
}
}

export class IntegratedSystemDiagramEditor extends TheiaEditor {
constructor(filePath: string, app: TheiaApp, tabSelector: string) {
super(
{
tabSelector,
viewSelector: normalizeId(
`#system-diagram:file://${urlEncodePath(join(app.workspace.escapedPath, OSUtil.fileSeparator, filePath))}`
)
},
app
);
}

async hasError(errorMessage: string): Promise<boolean> {
return hasViewError(this.page, this.viewSelector, errorMessage);
}
}

export class IntegratedMappingDiagramEditor extends TheiaEditor {
constructor(filePath: string, app: TheiaApp, tabSelector: string) {
super(
{
tabSelector,
viewSelector: normalizeId(
`#mapping-diagram:file://${urlEncodePath(join(app.workspace.escapedPath, OSUtil.fileSeparator, filePath))}`
)
},
app
parent
);
}

Expand Down
38 changes: 38 additions & 0 deletions e2e-tests/src/page-objects/cm-explorer-view.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/********************************************************************************
* Copyright (c) 2023 CrossBreeze.
********************************************************************************/
import { TheiaApp, TheiaExplorerFileStatNode, TheiaExplorerView } from '@theia/playwright';
import { CMMTabBarToolbar } from './cm-tab-bar-toolbar';

export class CMExplorerView extends TheiaExplorerView {
public readonly tabBarToolbar: CMMTabBarToolbar;

constructor(app: TheiaApp) {
super(app);
this.tabBarToolbar = new CMMTabBarToolbar(this);
}

/**
* The `existsFileNode` method implementation of the `TheiaExplorerView` PO don't
* behave as expected. If a node does not exist they will throw an errors instead of
* returning `false`.
* This method is a workaround and allows us to quickly check if a file node is visible
*/
async findTreeNode(path: string): Promise<TheiaExplorerFileStatNode | undefined> {
const fullPathSelector = this.treeNodeSelector(path);
const treeNodeElement = await this.page.$(fullPathSelector);
if (treeNodeElement) {
return new TheiaExplorerFileStatNode(treeNodeElement, this);
}
return undefined;
}

/**
* Override the `deleteNode` method to wait for the file nodes to decrease after a node is deleted
*/
override async deleteNode(path: string, confirm?: boolean | undefined, nodeSegmentLabel?: string | undefined): Promise<void> {
const fileStatElements = await this.visibleFileStatNodes();
await super.deleteNode(path, confirm, nodeSegmentLabel);
await this.waitForFileNodesToDecrease(fileStatElements.length);
}
}
Loading

0 comments on commit fce5763

Please sign in to comment.