Skip to content

Commit

Permalink
Refactor and improve E2E Test environment
Browse files Browse the repository at this point in the history
- 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
- Cleanup Add/Edit/Delete system diagram test
- Add Page objects for froms/property views
  • Loading branch information
tortmayr committed Nov 5, 2024
1 parent 82fbeed commit a7e39a5
Show file tree
Hide file tree
Showing 17 changed files with 615 additions and 184 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@
********************************************************************************/
import { IntegrationArgs, TheiaGLSPApp } from '@eclipse-glsp/glsp-playwright';
import { TheiaEditor } from '@theia/playwright';
import { CMCompositeEditor, IntegratedEditorType } from './cm-composite-editor';
import { CMExplorerView } from './cm-explorer-view';
import { CMTheiaIntegration } from './cm-theia-integration';
import { CrossModelExplorerView } from './crossmodel-explorer-view';

export interface CMAppArgs extends Omit<IntegrationArgs, 'page'> {
workspaceUrl?: string;
baseUrl?: string;
}
export class CrossModelApp extends TheiaGLSPApp {
public static async load(args: CMAppArgs): Promise<CrossModelApp> {
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 },
{
Expand Down Expand Up @@ -40,15 +41,34 @@ export class CrossModelApp extends TheiaGLSPApp {
return this._integration;
}

async openExplorerView(): Promise<CrossModelExplorerView> {
const explorer = await this.openView(CrossModelExplorerView);
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: CrossModelApp) => T,
editorFactory: new (editorFilePath: string, app: CMApp) => T,
editorName?: string | undefined,
expectFileNodes?: boolean | undefined
): Promise<T> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,25 @@
********************************************************************************/

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 { CMTheiaIntegration } from './cm-theia-integration';
import { CrossModelApp } from './crossmodel-app';
import { SystemDiagram } from './system-diagram/system-diagram';

export type CompositeEditorName = 'Code Editor' | 'Form Editor' | 'System Diagram' | 'Mapping Diagram';
import { CMApp } from './cm-app';
import { IntegratedEditor, IntegratedTextEditor } from './cm-integrated-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 class CrossModelCompositeEditor extends TheiaEditor {
export class CMCompositeEditor extends TheiaEditor {
constructor(
protected filePath: string,
app: CrossModelApp
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 @@ -55,102 +61,74 @@ 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'));
const textEditor = new IntegratedCodeEditor(this.filePath, this, this.editorTabSelector('Code Editor'));
await textEditor.waitForVisible();
return textEditor;
}

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

async switchToSystemDiagram(): Promise<IntegratedSystemDiagramEditor> {
await this.switchToEditor('System Diagram');
const diagramEditor = new IntegratedSystemDiagramEditor(
this.filePath,
this.app as CrossModelApp,
this.editorTabSelector('System Diagram')
);
const diagramEditor = new IntegratedSystemDiagramEditor(this.filePath, this, this.editorTabSelector('System Diagram'));
await diagramEditor.diagram.graph.waitForVisible();
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();
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);
}
}

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

async hasError(errorMessage: string): Promise<boolean> {
return hasViewError(this.page, this.viewSelector, errorMessage);
this.monacoEditor = new TheiaMonacoEditor(this.viewSelector, parent.app);
}
}

export class IntegratedSystemDiagramEditor extends TheiaEditor {
diagram: SystemDiagram;
constructor(filePath: string, app: CrossModelApp, tabSelector: string) {
export class IntegratedFormEditor extends IntegratedEditor {
constructor(filePath: string, parent: CMCompositeEditor, tabSelector: string) {
super(
{
tabSelector,
viewSelector: normalizeId(
`#system-diagram:file://${urlEncodePath(join(app.workspace.escapedPath, OSUtil.fileSeparator, filePath))}`
`#form-editor-opener:file://${urlEncodePath(join(parent.app.workspace.escapedPath, OSUtil.fileSeparator, filePath))}`
)
},
app
parent
);
this.diagram = this.createSystemDiagram(app.integration);
}

protected createSystemDiagram(integration: CMTheiaIntegration): SystemDiagram {
return new SystemDiagram({ type: 'integration', integration });
}

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) {
export class IntegratedMappingDiagramEditor extends IntegratedEditor {
constructor(filePath: string, parent: CMCompositeEditor, tabSelector: string) {
super(
{
tabSelector,
viewSelector: normalizeId(
`#mapping-diagram:file://${urlEncodePath(join(app.workspace.escapedPath, OSUtil.fileSeparator, filePath))}`
`#mapping-diagram:file://${urlEncodePath(join(parent.app.workspace.escapedPath, OSUtil.fileSeparator, filePath))}`
)
},
app
parent
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import { TheiaApp, TheiaExplorerView } from '@theia/playwright';
import { TheiaTabBarToolbar } from './theia-tabbar-toolbar';

export class CrossModelExplorerView extends TheiaExplorerView {
export class CMExplorerView extends TheiaExplorerView {
public readonly tabBarToolbar: TheiaTabBarToolbar;

constructor(app: TheiaApp) {
Expand Down
126 changes: 126 additions & 0 deletions e2e-tests/src/page-objects/cm-integrated-editor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/********************************************************************************
* Copyright (c) 2024 CrossBreeze.
********************************************************************************/

import { TheiaEditor, TheiaTextEditor, TheiaViewData } from '@theia/playwright';
import { CMCompositeEditor } from './cm-composite-editor';

export abstract class IntegratedEditor extends TheiaEditor {
constructor(
data: TheiaViewData,
protected readonly parent: CMCompositeEditor
) {
super(data, parent.app);
}

override async activate(): Promise<void> {
await this.parent.activate();
return super.activate();
}

override close(waitForClosed?: boolean | undefined): Promise<void> {
return this.parent.close(waitForClosed);
}

override closeWithoutSave(): Promise<void> {
return this.parent.closeWithoutSave();
}

override async focus(): Promise<void> {
await this.parent.focus();
return super.focus();
}

override async save(): Promise<void> {
await this.parent.save();
}

override async saveAndClose(): Promise<void> {
await this.parent.saveAndClose();
}

override async undo(times?: number | undefined): Promise<void> {
await this.parent.undo(times);
}

override async redo(times?: number | undefined): Promise<void> {
await this.parent.redo(times);
}

override async isDirty(): Promise<boolean> {
return this.parent.isDirty();
}

override async waitForVisible(): Promise<void> {
await this.parent.waitForVisible();
return super.waitForVisible();
}

override isClosable(): Promise<boolean> {
return this.parent.isClosable();
}

override title(): Promise<string | undefined> {
return this.parent.title();
}
}

export abstract class IntegratedTextEditor extends TheiaTextEditor {
constructor(
filePath: string,
protected readonly parent: CMCompositeEditor
) {
super(filePath, parent.app);
}

override async activate(): Promise<void> {
await this.parent.activate();
return super.activate();
}

override close(waitForClosed?: boolean | undefined): Promise<void> {
return this.parent.close(waitForClosed);
}

override closeWithoutSave(): Promise<void> {
return this.parent.closeWithoutSave();
}

override async focus(): Promise<void> {
await this.parent.focus();
return super.focus();
}

override async save(): Promise<void> {
await this.parent.save();
}

override async saveAndClose(): Promise<void> {
await this.parent.saveAndClose();
}

override async undo(times?: number | undefined): Promise<void> {
await this.parent.undo(times);
}

override async redo(times?: number | undefined): Promise<void> {
await this.parent.redo(times);
}

override async isDirty(): Promise<boolean> {
return this.parent.isDirty();
}

override async waitForVisible(): Promise<void> {
await this.parent.waitForVisible();
return super.waitForVisible();
}

override isClosable(): Promise<boolean> {
return this.parent.isClosable();
}

override title(): Promise<string | undefined> {
return this.parent.title();
}
}
12 changes: 6 additions & 6 deletions e2e-tests/src/page-objects/cm-theia-integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@
import { ContextMenuIntegration, Integration, IntegrationArgs, TheiaIntegrationOptions } from '@eclipse-glsp/glsp-playwright';
import { Locator, Page } from '@playwright/test';
import { TheiaAppFactory, TheiaAppLoader } from '@theia/playwright';
import { CrossModelApp } from './crossmodel-app';
import { CrossModelWorkspace } from './crossmodel-workspace';
import { CMApp } from './cm-app';
import { CMWorkspace } from './cm-workspace';

export class CMTheiaIntegration extends Integration implements ContextMenuIntegration {
protected theiaApp: CrossModelApp;
protected theiaApp: CMApp;

override get page(): Page {
return this.theiaApp.page;
}

get app(): CrossModelApp {
get app(): CMApp {
return this.theiaApp;
}

Expand All @@ -31,8 +31,8 @@ export class CMTheiaIntegration extends Integration implements ContextMenuIntegr
}

protected override async launch(): Promise<void> {
const ws = new CrossModelWorkspace(this.options.workspace ? [this.options.workspace] : undefined);
this.theiaApp = await TheiaAppLoader.load(this.args, ws, CrossModelApp as TheiaAppFactory<CrossModelApp>);
const ws = new CMWorkspace(this.options.workspace ? [this.options.workspace] : undefined);
this.theiaApp = await TheiaAppLoader.load(this.args, ws, CMApp as TheiaAppFactory<CMApp>);
this.theiaApp.integration = this;
this.theiaApp.initialize(this.options);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
********************************************************************************/
import { TheiaWorkspace } from '@theia/playwright';

export class CrossModelWorkspace extends TheiaWorkspace {}
export class CMWorkspace extends TheiaWorkspace {}
Loading

0 comments on commit a7e39a5

Please sign in to comment.