diff --git a/apps/workbench-client-testing-app/src/app/activator/activator.module.ts b/apps/workbench-client-testing-app/src/app/activator/activator.module.ts index 39527657f..4fb6f4ab7 100644 --- a/apps/workbench-client-testing-app/src/app/activator/activator.module.ts +++ b/apps/workbench-client-testing-app/src/app/activator/activator.module.ts @@ -10,14 +10,20 @@ import {Inject, NgModule} from '@angular/core'; import {Beans} from '@scion/toolkit/bean-manager'; -import {APP_IDENTITY, ManifestService, MessageClient} from '@scion/microfrontend-platform'; +import {APP_IDENTITY, Capability, Intention, ManifestService, MessageClient} from '@scion/microfrontend-platform'; import {WorkbenchCapabilities, WorkbenchDialogCapability, WorkbenchMessageBoxCapability, WorkbenchPopupCapability, WorkbenchViewCapability} from '@scion/workbench-client'; +import {firstValueFrom} from 'rxjs'; @NgModule({}) export default class ActivatorModule { - constructor(private _manifestService: ManifestService, @Inject(APP_IDENTITY) symbolicName: string) { - this.registerManifestObjects(symbolicName).then(() => Beans.get(MessageClient).publish('activator-ready')); + constructor(private _manifestService: ManifestService, + private _messageClient: MessageClient, + @Inject(APP_IDENTITY) private _symbolicName: string) { + this.registerManifestObjects(this._symbolicName).then(() => Beans.get(MessageClient).publish('activator-ready')); + this.installCapabilityRegisterRequestHandler(); + this.installCapabilityUnregisterRequestHandler(); + this.installIntentionRegisterRequestHandler(); } private async registerManifestObjects(appSymbolicName: string): Promise { @@ -25,7 +31,7 @@ export default class ActivatorModule { const heading = `${app}: Workbench Client E2E Testpage`; // Register view to interact with the workbench view object. - await this._manifestService.registerCapability({ + await this._manifestService.registerCapability({ type: WorkbenchCapabilities.View, qualifier: { component: 'view', @@ -47,15 +53,18 @@ export default class ActivatorModule { properties: { path: 'test-view', showSplash: true, - pinToStartPage: true, title: 'Workbench View', heading, cssClass: 'e2e-test-view', + tile: { + label: 'Workbench View', + cssClass: 'e2e-test-view', + }, }, }); // Register view to navigate using the workbench router. - await this._manifestService.registerCapability({ + await this._manifestService.registerCapability({ type: WorkbenchCapabilities.View, qualifier: { component: 'router', @@ -66,72 +75,18 @@ export default class ActivatorModule { properties: { path: 'test-router', showSplash: true, - pinToStartPage: true, title: 'Workbench Router', heading, cssClass: 'e2e-test-router', - }, - }); - - // Register view to register workbench capabilities dynamically at runtime. - await this._manifestService.registerCapability({ - type: WorkbenchCapabilities.View, - qualifier: { - component: 'register-workbench-capability', - app, - }, - description: '[e2e] Allows registering workbench capabilities', - private: false, - properties: { - path: 'register-workbench-capability', - showSplash: true, - pinToStartPage: true, - title: 'Register Capability', - heading, - cssClass: 'e2e-register-workbench-capability', - }, - }); - - // Register view to unregister workbench capabilities dynamically at runtime. - await this._manifestService.registerCapability({ - type: WorkbenchCapabilities.View, - qualifier: { - component: 'unregister-workbench-capability', - app, - }, - description: '[e2e] Allows unregistering workbench capabilities', - private: false, - properties: { - path: 'unregister-workbench-capability', - showSplash: true, - pinToStartPage: true, - title: 'Unregister Capability', - heading, - cssClass: 'e2e-unregister-workbench-capability', - }, - }); - - // Register view to register view intentions dynamically at runtime. - await this._manifestService.registerCapability({ - type: WorkbenchCapabilities.View, - qualifier: { - component: 'register-workbench-intention', - app, - }, - description: '[e2e] Allows registering view intentions', - private: false, - properties: { - path: 'register-workbench-intention', - showSplash: true, - pinToStartPage: true, - title: 'Register Intention', - heading, - cssClass: 'e2e-register-workbench-intention', + tile: { + label: 'Workbench Router', + cssClass: 'e2e-test-router', + }, }, }); // Register view to open a workbench popup. - await this._manifestService.registerCapability({ + await this._manifestService.registerCapability({ type: WorkbenchCapabilities.View, qualifier: { component: 'popup', @@ -142,10 +97,13 @@ export default class ActivatorModule { properties: { path: 'test-popup-opener', showSplash: true, - pinToStartPage: true, title: 'Workbench Popup', heading, cssClass: 'e2e-test-popup-opener', + tile: { + label: 'Workbench Popup', + cssClass: 'e2e-test-popup-opener', + }, }, }); @@ -165,7 +123,7 @@ export default class ActivatorModule { }); // Register view to open a workbench dialog. - await this._manifestService.registerCapability({ + await this._manifestService.registerCapability({ type: WorkbenchCapabilities.View, qualifier: { component: 'dialog', @@ -176,10 +134,13 @@ export default class ActivatorModule { properties: { path: 'test-dialog-opener', showSplash: true, - pinToStartPage: true, title: 'Workbench Dialog', heading, cssClass: 'e2e-test-dialog-opener', + tile: { + label: 'Workbench Dialog', + cssClass: 'e2e-test-dialog-opener', + }, }, }); @@ -203,7 +164,7 @@ export default class ActivatorModule { }); // Register view to open a workbench message box. - await this._manifestService.registerCapability({ + await this._manifestService.registerCapability({ type: WorkbenchCapabilities.View, qualifier: { component: 'messagebox', @@ -214,10 +175,13 @@ export default class ActivatorModule { properties: { path: 'test-message-box-opener', showSplash: true, - pinToStartPage: true, title: 'Workbench Message Box', heading, cssClass: 'e2e-test-message-box-opener', + tile: { + label: 'Workbench Message Box', + cssClass: 'e2e-test-message-box-opener', + }, }, }); @@ -241,7 +205,7 @@ export default class ActivatorModule { }); // Register view to display a workbench notification. - await this._manifestService.registerCapability({ + await this._manifestService.registerCapability({ type: WorkbenchCapabilities.View, qualifier: { component: 'notification', @@ -252,32 +216,43 @@ export default class ActivatorModule { properties: { path: 'test-notification-opener', showSplash: true, - pinToStartPage: true, title: 'Workbench Notification', heading, cssClass: 'e2e-test-notification-opener', + tile: { + label: 'Workbench Notification', + cssClass: 'e2e-test-notification-opener', + }, }, }); + } - // Register view to exchange messages via @scion/microfrontend-platform. - await this._manifestService.registerCapability({ - type: WorkbenchCapabilities.View, - qualifier: { - component: 'messaging', - app, - }, - description: '[e2e] Allows exchanging messages via @scion/microfrontend-platform', - private: false, - properties: { - path: 'messaging', - showSplash: true, - pinToStartPage: true, - title: 'Messaging', - heading, - cssClass: 'e2e-messaging', - }, + private installCapabilityRegisterRequestHandler(): void { + this._messageClient.onMessage(`application/${this._symbolicName}/capability/register`, async ({body: capability}) => { + const capabilityId = await this._manifestService.registerCapability(capability!); + return (await firstValueFrom(this._manifestService.lookupCapabilities$({id: capabilityId})))[0]; + }); + } + + private installCapabilityUnregisterRequestHandler(): void { + this._messageClient.onMessage(`application/${this._symbolicName}/capability/:capabilityId/unregister`, async message => { + await this._manifestService.unregisterCapabilities({id: message.params?.get('capabilityId')}); + return true; + }); + } + + private installIntentionRegisterRequestHandler(): void { + this._messageClient.onMessage(`application/${this._symbolicName}/intention/register`, async ({body: intention}) => { + return this._manifestService.registerIntention(intention!); }); } } -type TestingAppViewCapability = WorkbenchViewCapability & {properties: {pinToStartPage?: boolean}}; +type WorkbenchViewTestingAppCapability = WorkbenchViewCapability & { + properties: { + tile: { + label: string; + cssClass: string; + }; + }; +}; diff --git a/apps/workbench-client-testing-app/src/app/app.routes.ts b/apps/workbench-client-testing-app/src/app/app.routes.ts index 896c7b40c..2f58e3085 100644 --- a/apps/workbench-client-testing-app/src/app/app.routes.ts +++ b/apps/workbench-client-testing-app/src/app/app.routes.ts @@ -51,22 +51,6 @@ export const routes: Routes = [ path: 'test-notification-opener', loadComponent: () => import('./notification-opener-page/notification-opener-page.component'), }, - { - path: 'register-workbench-capability', - loadComponent: () => import('./register-workbench-capability-page/register-workbench-capability-page.component'), - }, - { - path: 'unregister-workbench-capability', - loadComponent: () => import('./unregister-workbench-capability-page/unregister-workbench-capability-page.component'), - }, - { - path: 'register-workbench-intention', - loadComponent: () => import('./register-workbench-intention-page/register-workbench-intention-page.component'), - }, - { - path: 'messaging', - loadComponent: () => import('./messaging-page/messaging-page.component'), - }, { path: 'test-pages', loadChildren: () => import('./test-pages/routes'), diff --git a/apps/workbench-client-testing-app/src/app/messaging-page/messaging-page.component.html b/apps/workbench-client-testing-app/src/app/messaging-page/messaging-page.component.html deleted file mode 100644 index b08b21474..000000000 --- a/apps/workbench-client-testing-app/src/app/messaging-page/messaging-page.component.html +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/apps/workbench-client-testing-app/src/app/messaging-page/messaging-page.component.ts b/apps/workbench-client-testing-app/src/app/messaging-page/messaging-page.component.ts deleted file mode 100644 index 2a5132545..000000000 --- a/apps/workbench-client-testing-app/src/app/messaging-page/messaging-page.component.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (c) 2018-2023 Swiss Federal Railways - * - * This program and the accompanying materials are made - * available under the terms of the Eclipse Public License 2.0 - * which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - */ - -import {Component} from '@angular/core'; -import {SciTabbarComponent, SciTabDirective} from '@scion/components.internal/tabbar'; -import PublishMesagePageComponent from './publish-message-page/publish-message-page.component'; -import {WorkbenchView} from '@scion/workbench-client'; - -@Component({ - selector: 'app-messaging-page', - templateUrl: './messaging-page.component.html', - styleUrls: ['./messaging-page.component.scss'], - standalone: true, - imports: [ - SciTabDirective, - SciTabbarComponent, - PublishMesagePageComponent, - ], -}) -export default class MessagingPageComponent { - - constructor(view: WorkbenchView) { - view.signalReady(); - } -} diff --git a/apps/workbench-client-testing-app/src/app/register-workbench-intention-page/register-workbench-intention-page.component.html b/apps/workbench-client-testing-app/src/app/register-workbench-intention-page/register-workbench-intention-page.component.html deleted file mode 100644 index 4943319d3..000000000 --- a/apps/workbench-client-testing-app/src/app/register-workbench-intention-page/register-workbench-intention-page.component.html +++ /dev/null @@ -1,31 +0,0 @@ -
-
- - - - - - - -
- - - - @if (intentionId) { - - Intention ID: {{intentionId}} - - } - - @if (registerError) { - - {{registerError}} - - } -
diff --git a/apps/workbench-client-testing-app/src/app/register-workbench-intention-page/register-workbench-intention-page.component.ts b/apps/workbench-client-testing-app/src/app/register-workbench-intention-page/register-workbench-intention-page.component.ts deleted file mode 100644 index 487a51df6..000000000 --- a/apps/workbench-client-testing-app/src/app/register-workbench-intention-page/register-workbench-intention-page.component.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright (c) 2018-2024 Swiss Federal Railways - * - * This program and the accompanying materials are made - * available under the terms of the Eclipse Public License 2.0 - * which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - */ - -import {Component} from '@angular/core'; -import {FormGroup, NonNullableFormBuilder, ReactiveFormsModule, Validators} from '@angular/forms'; -import {Intention, ManifestService} from '@scion/microfrontend-platform'; -import {WorkbenchCapabilities, WorkbenchView} from '@scion/workbench-client'; -import {SciFormFieldComponent} from '@scion/components.internal/form-field'; -import {stringifyError} from '../common/stringify-error.util'; -import {KeyValueEntry, SciKeyValueFieldComponent} from '@scion/components.internal/key-value-field'; - -@Component({ - selector: 'app-register-workbench-intention-page', - templateUrl: './register-workbench-intention-page.component.html', - styleUrls: ['./register-workbench-intention-page.component.scss'], - standalone: true, - imports: [ - ReactiveFormsModule, - SciFormFieldComponent, - SciKeyValueFieldComponent, - ], -}) -export default class RegisterWorkbenchIntentionPageComponent { - - public form = this._formBuilder.group({ - type: this._formBuilder.control('', Validators.required), - qualifier: this._formBuilder.array>([]), - }); - - public intentionId: string | undefined; - public registerError: string | undefined; - public WorkbenchCapabilities = WorkbenchCapabilities; - - constructor(view: WorkbenchView, - private _manifestService: ManifestService, - private _formBuilder: NonNullableFormBuilder) { - view.signalReady(); - } - - public async onRegister(): Promise { - const intention: Intention = { - type: this.form.controls.type.value, - qualifier: SciKeyValueFieldComponent.toDictionary(this.form.controls.qualifier) ?? undefined, - }; - - this.intentionId = undefined; - this.registerError = undefined; - - await this._manifestService.registerIntention(intention) - .then(id => { - this.intentionId = id; - this.form.reset(); - this.form.setControl('qualifier', this._formBuilder.array>([])); - }) - .catch(error => this.registerError = stringifyError(error)); - } -} diff --git a/apps/workbench-client-testing-app/src/app/unregister-workbench-capability-page/unregister-workbench-capability-page.component.html b/apps/workbench-client-testing-app/src/app/unregister-workbench-capability-page/unregister-workbench-capability-page.component.html deleted file mode 100644 index 089e71bc1..000000000 --- a/apps/workbench-client-testing-app/src/app/unregister-workbench-capability-page/unregister-workbench-capability-page.component.html +++ /dev/null @@ -1,14 +0,0 @@ -
-
- - - -
- - - - - {{unregisterError}} - - -
diff --git a/apps/workbench-client-testing-app/src/app/unregister-workbench-capability-page/unregister-workbench-capability-page.component.ts b/apps/workbench-client-testing-app/src/app/unregister-workbench-capability-page/unregister-workbench-capability-page.component.ts deleted file mode 100644 index 214ed7efe..000000000 --- a/apps/workbench-client-testing-app/src/app/unregister-workbench-capability-page/unregister-workbench-capability-page.component.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright (c) 2018-2022 Swiss Federal Railways - * - * This program and the accompanying materials are made - * available under the terms of the Eclipse Public License 2.0 - * which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - */ - -import {Component} from '@angular/core'; -import {NonNullableFormBuilder, ReactiveFormsModule, Validators} from '@angular/forms'; -import {ManifestService} from '@scion/microfrontend-platform'; -import {NgIf} from '@angular/common'; -import {stringifyError} from '../common/stringify-error.util'; -import {SciFormFieldComponent} from '@scion/components.internal/form-field'; -import {WorkbenchView} from '@scion/workbench-client'; - -/** - * Allows unregistering workbench capabilities. - */ -@Component({ - selector: 'app-unregister-workbench-capability-page', - templateUrl: './unregister-workbench-capability-page.component.html', - styleUrls: ['./unregister-workbench-capability-page.component.scss'], - standalone: true, - imports: [ - NgIf, - ReactiveFormsModule, - SciFormFieldComponent, - ], -}) -export default class UnregisterWorkbenchCapabilityPageComponent { - - public form = this._formBuilder.group({ - id: this._formBuilder.control('', Validators.required), - }); - public unregisterError: string | undefined; - public unregistered: boolean | undefined; - - constructor(view: WorkbenchView, - private _manifestService: ManifestService, - private _formBuilder: NonNullableFormBuilder) { - view.signalReady(); - } - - public async onUnregister(): Promise { - this.unregisterError = undefined; - this.unregistered = undefined; - - await this._manifestService.unregisterCapabilities({id: this.form.controls.id.value}) - .then(() => { - this.unregistered = true; - this.form.reset(); - }) - .catch(error => this.unregisterError = stringifyError(error)); - } -} diff --git a/apps/workbench-testing-app/src/app/app.config.ts b/apps/workbench-testing-app/src/app/app.config.ts index 3da87dcb3..3445cc868 100644 --- a/apps/workbench-testing-app/src/app/app.config.ts +++ b/apps/workbench-testing-app/src/app/app.config.ts @@ -21,6 +21,7 @@ import {Perspectives} from './workbench.perspectives'; import {environment} from '../environments/environment'; import {provideAnimations, provideNoopAnimations} from '@angular/platform-browser/animations'; import {provideWorkbench} from '@scion/workbench'; +import {provideWorkbenchHostCapabilityRegistrator} from './microfrontend-platform-page/microfrontend-platform.service'; /** * Central place to configure the workbench-testing-app. @@ -35,6 +36,7 @@ export const appConfig: ApplicationConfig = { provideDevToolsInterceptor(), provideNotificationPage(), provideAnimationsIfEnabled(), + provideWorkbenchHostCapabilityRegistrator(), Perspectives.provideRoutes(), ], }; diff --git a/apps/workbench-testing-app/src/app/app.routes.ts b/apps/workbench-testing-app/src/app/app.routes.ts index 22e80427e..ab8c376cb 100644 --- a/apps/workbench-testing-app/src/app/app.routes.ts +++ b/apps/workbench-testing-app/src/app/app.routes.ts @@ -131,6 +131,11 @@ export const routes: Routes = [ path: 'test-host-message-box', loadComponent: () => import('./host-message-box-page/host-message-box-page.component'), }, + { + path: 'microfrontend-platform', + loadComponent: () => import('./microfrontend-platform-page/microfrontend-platform-page.component'), + data: {[WorkbenchRouteData.title]: 'Microfrontend Platform', [WorkbenchRouteData.heading]: 'Workbench E2E Testpage', [WorkbenchRouteData.cssClass]: 'e2e-microfrontend-platform'}, + }, { path: 'test-pages', loadChildren: () => import('./test-pages/routes'), diff --git a/apps/workbench-testing-app/src/app/common/array-concat.pipe.ts b/apps/workbench-testing-app/src/app/common/array-concat.pipe.ts deleted file mode 100644 index 49cc0c01e..000000000 --- a/apps/workbench-testing-app/src/app/common/array-concat.pipe.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (c) 2018-2023 Swiss Federal Railways - * - * This program and the accompanying materials are made - * available under the terms of the Eclipse Public License 2.0 - * which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - */ - -import {Pipe, PipeTransform} from '@angular/core'; - -/** - * Concats the given arrays. - * - * Usage: `array | wbConcatArray:array1:array2:array3` - */ -@Pipe({name: 'appConcatArray', standalone: true}) -export class ArrayConcatPipe implements PipeTransform { - - public transform(input: string[], ...arrays: Array): string[] { - return arrays.reduce( - (acc: string[], array: string[] | string | undefined | null): string[] => acc.concat(array ?? []), - input, - ); - } -} diff --git a/apps/workbench-testing-app/src/app/devtools/devtools-capability-interceptor.service.ts b/apps/workbench-testing-app/src/app/devtools/devtools-capability-interceptor.service.ts index e4bbc31af..2c3d1cc44 100644 --- a/apps/workbench-testing-app/src/app/devtools/devtools-capability-interceptor.service.ts +++ b/apps/workbench-testing-app/src/app/devtools/devtools-capability-interceptor.service.ts @@ -41,7 +41,9 @@ class DevToolsViewCapabilityInterceptor implements CapabilityInterceptor, Workbe // Add property to pin DevTools to the start page. capability.properties = { ...capability.properties, - pinToStartPage: true, + tile: { + label: 'Developer Tools', + }, }; return capability; diff --git a/apps/workbench-testing-app/src/app/microfrontend-platform-page/microfrontend-platform-page.component.html b/apps/workbench-testing-app/src/app/microfrontend-platform-page/microfrontend-platform-page.component.html new file mode 100644 index 000000000..f4555abab --- /dev/null +++ b/apps/workbench-testing-app/src/app/microfrontend-platform-page/microfrontend-platform-page.component.html @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/apps/workbench-client-testing-app/src/app/messaging-page/messaging-page.component.scss b/apps/workbench-testing-app/src/app/microfrontend-platform-page/microfrontend-platform-page.component.scss similarity index 100% rename from apps/workbench-client-testing-app/src/app/messaging-page/messaging-page.component.scss rename to apps/workbench-testing-app/src/app/microfrontend-platform-page/microfrontend-platform-page.component.scss diff --git a/apps/workbench-testing-app/src/app/microfrontend-platform-page/microfrontend-platform-page.component.ts b/apps/workbench-testing-app/src/app/microfrontend-platform-page/microfrontend-platform-page.component.ts new file mode 100644 index 000000000..f84deef52 --- /dev/null +++ b/apps/workbench-testing-app/src/app/microfrontend-platform-page/microfrontend-platform-page.component.ts @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2018-2024 Swiss Federal Railways + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import {Component} from '@angular/core'; +import {SciTabbarComponent, SciTabDirective} from '@scion/components.internal/tabbar'; +import RegisterCapabilityPageComponent from './register-capability-page/register-capability-page.component'; +import {PublishMesagePageComponent} from './publish-message-page/publish-message-page.component'; +import {RegisterIntentionPageComponent} from './register-intention-page/register-intention-page.component'; +import {UnregisterCapabilityPageComponent} from './unregister-capability-page/unregister-capability-page.component'; + +@Component({ + selector: 'app-microfrontend-platform-page', + templateUrl: './microfrontend-platform-page.component.html', + styleUrls: ['./microfrontend-platform-page.component.scss'], + standalone: true, + imports: [ + SciTabDirective, + SciTabbarComponent, + PublishMesagePageComponent, + RegisterCapabilityPageComponent, + RegisterIntentionPageComponent, + UnregisterCapabilityPageComponent, + ], +}) +export default class MicrofrontendPlatformPageComponent { +} diff --git a/apps/workbench-testing-app/src/app/microfrontend-platform-page/microfrontend-platform.service.ts b/apps/workbench-testing-app/src/app/microfrontend-platform-page/microfrontend-platform.service.ts new file mode 100644 index 000000000..9a4c26b0b --- /dev/null +++ b/apps/workbench-testing-app/src/app/microfrontend-platform-page/microfrontend-platform.service.ts @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2018-2024 Swiss Federal Railways + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import {EnvironmentProviders, Inject, Injectable, makeEnvironmentProviders} from '@angular/core'; +import {firstValueFrom} from 'rxjs'; +import {APP_IDENTITY, Capability, Intention, ManifestService, MessageClient} from '@scion/microfrontend-platform'; +import {MICROFRONTEND_PLATFORM_POST_STARTUP} from '@scion/workbench'; + +/** + * Provides settings for the workbench testing application. + */ +@Injectable(/* DO NOT PROVIDE via 'providedIn' metadata as associated with the {@link MICROFRONTEND_PLATFORM_POST_STARTUP} DI token. */) +export class WorkbenchHostCapabilityRegistrator { + + constructor(private _manifestService: ManifestService, + private _messageClient: MessageClient, + @Inject(APP_IDENTITY) private _symbolicName: string) { + this.installCapabilityRegisterRequestHandler(); + this.installCapabilityUnregisterRequestHandler(); + this.installIntentionRegisterRequestHandler(); + } + + private installCapabilityRegisterRequestHandler(): void { + this._messageClient.onMessage(`application/${this._symbolicName}/capability/register`, async ({body: capability}) => { + const capabilityId = await this._manifestService.registerCapability(capability!); + return (await firstValueFrom(this._manifestService.lookupCapabilities$({id: capabilityId})))[0]; + }); + } + + private installCapabilityUnregisterRequestHandler(): void { + this._messageClient.onMessage(`application/${this._symbolicName}/capability/:capabilityId/unregister`, async message => { + await this._manifestService.unregisterCapabilities({id: message.params?.get('capabilityId')}); + return true; + }); + } + + private installIntentionRegisterRequestHandler(): void { + this._messageClient.onMessage(`application/${this._symbolicName}/intention/register`, async ({body: intention}) => { + return this._manifestService.registerIntention(intention!); + }); + } +} + +export function provideWorkbenchHostCapabilityRegistrator(): EnvironmentProviders { + return makeEnvironmentProviders([ + { + provide: MICROFRONTEND_PLATFORM_POST_STARTUP, + useClass: WorkbenchHostCapabilityRegistrator, + multi: true, + }, + ]); +} diff --git a/apps/workbench-client-testing-app/src/app/messaging-page/publish-message-page/publish-message-page.component.html b/apps/workbench-testing-app/src/app/microfrontend-platform-page/publish-message-page/publish-message-page.component.html similarity index 100% rename from apps/workbench-client-testing-app/src/app/messaging-page/publish-message-page/publish-message-page.component.html rename to apps/workbench-testing-app/src/app/microfrontend-platform-page/publish-message-page/publish-message-page.component.html diff --git a/apps/workbench-client-testing-app/src/app/messaging-page/publish-message-page/publish-message-page.component.scss b/apps/workbench-testing-app/src/app/microfrontend-platform-page/publish-message-page/publish-message-page.component.scss similarity index 100% rename from apps/workbench-client-testing-app/src/app/messaging-page/publish-message-page/publish-message-page.component.scss rename to apps/workbench-testing-app/src/app/microfrontend-platform-page/publish-message-page/publish-message-page.component.scss diff --git a/apps/workbench-client-testing-app/src/app/messaging-page/publish-message-page/publish-message-page.component.ts b/apps/workbench-testing-app/src/app/microfrontend-platform-page/publish-message-page/publish-message-page.component.ts similarity index 85% rename from apps/workbench-client-testing-app/src/app/messaging-page/publish-message-page/publish-message-page.component.ts rename to apps/workbench-testing-app/src/app/microfrontend-platform-page/publish-message-page/publish-message-page.component.ts index 00dc81056..9639c42c8 100644 --- a/apps/workbench-client-testing-app/src/app/messaging-page/publish-message-page/publish-message-page.component.ts +++ b/apps/workbench-testing-app/src/app/microfrontend-platform-page/publish-message-page/publish-message-page.component.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2023 Swiss Federal Railways + * Copyright (c) 2018-2024 Swiss Federal Railways * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -26,18 +26,18 @@ import {NgIf} from '@angular/common'; NgIf, ], }) -export default class PublishMesagePageComponent { +export class PublishMesagePageComponent { - public publishError: string | false | undefined; + protected publishError: string | false | undefined; - public form = this._formBuilder.group({ + protected form = this._formBuilder.group({ topic: this._formBuilder.control('', Validators.required), }); constructor(private _formBuilder: NonNullableFormBuilder, private _messageClient: MessageClient) { } - public onPublish(): void { + protected onPublish(): void { this.publishError = undefined; this._messageClient .publish(this.form.controls.topic.value) diff --git a/apps/workbench-client-testing-app/src/app/register-workbench-capability-page/register-workbench-capability-page.component.html b/apps/workbench-testing-app/src/app/microfrontend-platform-page/register-capability-page/register-capability-page.component.html similarity index 86% rename from apps/workbench-client-testing-app/src/app/register-workbench-capability-page/register-workbench-capability-page.component.html rename to apps/workbench-testing-app/src/app/microfrontend-platform-page/register-capability-page/register-capability-page.component.html index a260c54ad..d6d742c18 100644 --- a/apps/workbench-client-testing-app/src/app/register-workbench-capability-page/register-workbench-capability-page.component.html +++ b/apps/workbench-testing-app/src/app/microfrontend-platform-page/register-capability-page/register-capability-page.component.html @@ -1,14 +1,23 @@
- - + @for (application of applications; track application) { + + } + + + + + + + + + + @@ -29,10 +38,10 @@
-
-
Properties
+ @if (form.controls.type.value === WorkbenchCapabilities.View) { +
+
Properties
- @if (form.controls.type.value === WorkbenchCapabilities.View) { @@ -56,13 +65,13 @@ +
+ } - - - - } + @if (form.controls.type.value === WorkbenchCapabilities.Popup) { +
+
Properties
- @if (form.controls.type.value === WorkbenchCapabilities.Popup) { @@ -95,16 +104,15 @@ - - - - - } +
+ } - @if (form.controls.type.value === WorkbenchCapabilities.Dialog) { + @if (form.controls.type.value === WorkbenchCapabilities.Dialog) { +
+
Properties
@@ -156,9 +164,12 @@ - } +
+ } - @if (form.controls.type.value === WorkbenchCapabilities.MessageBox) { + @if (form.controls.type.value === WorkbenchCapabilities.MessageBox) { +
+
Properties
@@ -198,8 +209,8 @@ - } -
+
+ } diff --git a/apps/workbench-client-testing-app/src/app/register-workbench-capability-page/register-workbench-capability-page.component.scss b/apps/workbench-testing-app/src/app/microfrontend-platform-page/register-capability-page/register-capability-page.component.scss similarity index 100% rename from apps/workbench-client-testing-app/src/app/register-workbench-capability-page/register-workbench-capability-page.component.scss rename to apps/workbench-testing-app/src/app/microfrontend-platform-page/register-capability-page/register-capability-page.component.scss diff --git a/apps/workbench-client-testing-app/src/app/register-workbench-capability-page/register-workbench-capability-page.component.ts b/apps/workbench-testing-app/src/app/microfrontend-platform-page/register-capability-page/register-capability-page.component.ts similarity index 80% rename from apps/workbench-client-testing-app/src/app/register-workbench-capability-page/register-workbench-capability-page.component.ts rename to apps/workbench-testing-app/src/app/microfrontend-platform-page/register-capability-page/register-capability-page.component.ts index e68f0896c..d5d0a9c56 100644 --- a/apps/workbench-client-testing-app/src/app/register-workbench-capability-page/register-workbench-capability-page.component.ts +++ b/apps/workbench-testing-app/src/app/microfrontend-platform-page/register-capability-page/register-capability-page.component.ts @@ -11,25 +11,24 @@ import {Component} from '@angular/core'; import {FormGroup, NonNullableFormBuilder, ReactiveFormsModule, Validators} from '@angular/forms'; import {KeyValueEntry, SciKeyValueFieldComponent} from '@scion/components.internal/key-value-field'; -import {Capability, ManifestService, ParamDefinition} from '@scion/microfrontend-platform'; -import {PopupSize, ViewParamDefinition, WorkbenchCapabilities, WorkbenchDialogCapability, WorkbenchDialogSize, WorkbenchMessageBoxCapability, WorkbenchMessageBoxSize, WorkbenchPopupCapability, WorkbenchView, WorkbenchViewCapability} from '@scion/workbench-client'; +import {Capability, ManifestService, mapToBody, MessageClient, ParamDefinition} from '@scion/microfrontend-platform'; +import {PopupSize, ViewParamDefinition, WorkbenchCapabilities, WorkbenchDialogCapability, WorkbenchDialogSize, WorkbenchMessageBoxCapability, WorkbenchMessageBoxSize, WorkbenchPopupCapability, WorkbenchViewCapability} from '@scion/workbench-client'; import {firstValueFrom} from 'rxjs'; -import {undefinedIfEmpty} from '../common/undefined-if-empty.util'; +import {undefinedIfEmpty} from '../../common/undefined-if-empty.util'; import {SciViewportComponent} from '@scion/components/viewport'; import {JsonPipe} from '@angular/common'; -import {stringifyError} from '../common/stringify-error.util'; import {SciFormFieldComponent} from '@scion/components.internal/form-field'; import {SciCheckboxComponent} from '@scion/components.internal/checkbox'; -import {parseTypedString} from '../common/parse-typed-value.util'; -import {CssClassComponent} from '../css-class/css-class.component'; +import {parseTypedString} from '../../common/parse-typed-value.util'; +import {CssClassComponent} from '../../css-class/css-class.component'; +import {UUID} from '@scion/toolkit/uuid'; +import {stringifyError} from '../../common/stringify-error.util'; +import {SettingsService} from '../../settings.service'; -/** - * Allows registering workbench capabilities. - */ @Component({ - selector: 'app-register-workbench-capability-page', - templateUrl: './register-workbench-capability-page.component.html', - styleUrls: ['./register-workbench-capability-page.component.scss'], + selector: 'app-register-capability-page', + templateUrl: './register-capability-page.component.html', + styleUrls: ['./register-capability-page.component.scss'], standalone: true, imports: [ JsonPipe, @@ -41,9 +40,10 @@ import {CssClassComponent} from '../css-class/css-class.component'; CssClassComponent, ], }) -export default class RegisterWorkbenchCapabilityPageComponent { +export default class RegisterCapabilityPageComponent { - public form = this._formBuilder.group({ + protected form = this._formBuilder.group({ + application: this._formBuilder.control('', Validators.required), type: this._formBuilder.control('', Validators.required), qualifier: this._formBuilder.array>([]), requiredParams: this._formBuilder.control(''), @@ -57,7 +57,6 @@ export default class RegisterWorkbenchCapabilityPageComponent { closable: this._formBuilder.control(null), showSplash: this._formBuilder.control(null), cssClass: this._formBuilder.control(undefined), - pinToStartPage: this._formBuilder.control(false), }), popupProperties: this._formBuilder.group({ path: this._formBuilder.control(''), @@ -70,7 +69,6 @@ export default class RegisterWorkbenchCapabilityPageComponent { maxWidth: this._formBuilder.control(''), }), showSplash: this._formBuilder.control(null), - pinToStartPage: this._formBuilder.control(false), cssClass: this._formBuilder.control(undefined), }), dialogProperties: this._formBuilder.group({ @@ -105,17 +103,22 @@ export default class RegisterWorkbenchCapabilityPageComponent { }), }); - public capability: Capability | undefined; - public registerError: string | undefined; - public WorkbenchCapabilities = WorkbenchCapabilities; + protected capability: Capability | undefined; + protected registerError: string | undefined; + protected applications: string[]; + protected WorkbenchCapabilities = WorkbenchCapabilities; + protected capabilityTypeList = `capability-type-list-${UUID.randomUUID()}`; - constructor(view: WorkbenchView, - private _manifestService: ManifestService, + constructor(manifestService: ManifestService, + private _messageClient: MessageClient, + private _settingsService: SettingsService, private _formBuilder: NonNullableFormBuilder) { - view.signalReady(); + this.applications = manifestService.applications + .filter(application => application.symbolicName.startsWith('workbench')) + .map(application => application.symbolicName); } - public async onRegister(): Promise { + protected async onRegister(): Promise { const capability: Capability = ((): Capability => { switch (this.form.controls.type.value) { case WorkbenchCapabilities.View: @@ -127,23 +130,36 @@ export default class RegisterWorkbenchCapabilityPageComponent { case WorkbenchCapabilities.MessageBox: return this.readMessageBoxCapabilityFromUI(); default: - throw Error('Capability expected to be a workbench capability, but was not.'); + return this.readCapabilityFromUI(); } })(); this.capability = undefined; this.registerError = undefined; + try { + this.capability = await firstValueFrom(this._messageClient.request$(`application/${this.form.controls.application.value}/capability/register`, capability).pipe(mapToBody())); + this.resetForm(); + } + catch (error) { + this.registerError = stringifyError(error); + } + } - await this._manifestService.registerCapability(capability) - .then(async id => { - this.capability = (await firstValueFrom(this._manifestService.lookupCapabilities$({id})))[0]; - this.form.reset(); - this.form.setControl('qualifier', this._formBuilder.array>([])); - }) - .catch(error => this.registerError = stringifyError(error)); + private readCapabilityFromUI(): Capability { + const requiredParams: ParamDefinition[] = this.form.controls.requiredParams.value.split(/,\s*/).filter(Boolean).map(param => ({name: param, required: true})); + const optionalParams: ParamDefinition[] = this.form.controls.optionalParams.value.split(/,\s*/).filter(Boolean).map(param => ({name: param, required: false})); + return { + type: this.form.controls.type.value, + qualifier: SciKeyValueFieldComponent.toDictionary(this.form.controls.qualifier)!, + params: [ + ...requiredParams, + ...optionalParams, + ], + private: this.form.controls.private.value, + }; } - private readViewCapabilityFromUI(): WorkbenchViewCapability & {properties: {pinToStartPage: boolean}} { + private readViewCapabilityFromUI(): WorkbenchViewCapability { const requiredParams: ViewParamDefinition[] = this.form.controls.requiredParams.value.split(/,\s*/).filter(Boolean).map(param => ({name: param, required: true})); const optionalParams: ViewParamDefinition[] = this.form.controls.optionalParams.value.split(/,\s*/).filter(Boolean).map(param => ({name: param, required: false})); const transientParams: ViewParamDefinition[] = this.form.controls.transientParams.value?.split(/,\s*/).filter(Boolean).map(param => ({name: param, required: false, transient: true})); @@ -163,12 +179,11 @@ export default class RegisterWorkbenchCapabilityPageComponent { cssClass: this.form.controls.viewProperties.controls.cssClass.value, closable: this.form.controls.viewProperties.controls.closable.value ?? undefined, showSplash: this.form.controls.viewProperties.controls.showSplash.value ?? undefined, - pinToStartPage: this.form.controls.viewProperties.controls.pinToStartPage.value, }, }; } - private readPopupCapabilityFromUI(): WorkbenchPopupCapability & {properties: {pinToStartPage: boolean}} { + private readPopupCapabilityFromUI(): WorkbenchPopupCapability { const requiredParams: ParamDefinition[] = this.form.controls.requiredParams.value.split(/,\s*/).filter(Boolean).map(param => ({name: param, required: true})); const optionalParams: ParamDefinition[] = this.form.controls.optionalParams.value.split(/,\s*/).filter(Boolean).map(param => ({name: param, required: false})); return { @@ -190,7 +205,6 @@ export default class RegisterWorkbenchCapabilityPageComponent { maxHeight: this.form.controls.popupProperties.controls.size.controls.maxHeight.value || undefined, }), showSplash: this.form.controls.popupProperties.controls.showSplash.value ?? undefined, - pinToStartPage: this.form.controls.popupProperties.controls.pinToStartPage.value, cssClass: this.form.controls.popupProperties.controls.cssClass.value, }, }; @@ -253,4 +267,11 @@ export default class RegisterWorkbenchCapabilityPageComponent { }, }; } + + private resetForm(): void { + if (this._settingsService.isEnabled('resetFormsOnSubmit')) { + this.form.reset(); + this.form.setControl('qualifier', this._formBuilder.array>([])); + } + } } diff --git a/apps/workbench-testing-app/src/app/microfrontend-platform-page/register-intention-page/register-intention-page.component.html b/apps/workbench-testing-app/src/app/microfrontend-platform-page/register-intention-page/register-intention-page.component.html new file mode 100644 index 000000000..d05b4406c --- /dev/null +++ b/apps/workbench-testing-app/src/app/microfrontend-platform-page/register-intention-page/register-intention-page.component.html @@ -0,0 +1,39 @@ + +
+ + + + + + + + + + + + + + + + + +
+ + + + @if (intentionId) { + + Intention ID: {{intentionId}} + + } + + @if (registerError) { + + {{registerError}} + + } + diff --git a/apps/workbench-client-testing-app/src/app/register-workbench-intention-page/register-workbench-intention-page.component.scss b/apps/workbench-testing-app/src/app/microfrontend-platform-page/register-intention-page/register-intention-page.component.scss similarity index 100% rename from apps/workbench-client-testing-app/src/app/register-workbench-intention-page/register-workbench-intention-page.component.scss rename to apps/workbench-testing-app/src/app/microfrontend-platform-page/register-intention-page/register-intention-page.component.scss diff --git a/apps/workbench-testing-app/src/app/microfrontend-platform-page/register-intention-page/register-intention-page.component.ts b/apps/workbench-testing-app/src/app/microfrontend-platform-page/register-intention-page/register-intention-page.component.ts new file mode 100644 index 000000000..c17147196 --- /dev/null +++ b/apps/workbench-testing-app/src/app/microfrontend-platform-page/register-intention-page/register-intention-page.component.ts @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2018-2024 Swiss Federal Railways + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import {Component} from '@angular/core'; +import {FormGroup, NonNullableFormBuilder, ReactiveFormsModule, Validators} from '@angular/forms'; +import {Intention, ManifestService, mapToBody, MessageClient} from '@scion/microfrontend-platform'; +import {WorkbenchCapabilities} from '@scion/workbench-client'; +import {SciFormFieldComponent} from '@scion/components.internal/form-field'; +import {stringifyError} from '../../common/stringify-error.util'; +import {KeyValueEntry, SciKeyValueFieldComponent} from '@scion/components.internal/key-value-field'; +import {UUID} from '@scion/toolkit/uuid'; +import {SettingsService} from '../../settings.service'; +import {firstValueFrom} from 'rxjs'; + +@Component({ + selector: 'app-register-intention-page', + templateUrl: './register-intention-page.component.html', + styleUrls: ['./register-intention-page.component.scss'], + standalone: true, + imports: [ + ReactiveFormsModule, + SciFormFieldComponent, + SciKeyValueFieldComponent, + ], +}) +export class RegisterIntentionPageComponent { + + protected form = this._formBuilder.group({ + application: this._formBuilder.control('', Validators.required), + type: this._formBuilder.control('', Validators.required), + qualifier: this._formBuilder.array>([]), + }); + + public intentionId: string | undefined; + public registerError: string | undefined; + protected applications: string[]; + public WorkbenchCapabilities = WorkbenchCapabilities; + protected intentionTypeList = `intention-type-list-${UUID.randomUUID()}`; + + constructor(manifestService: ManifestService, + private _messageClient: MessageClient, + private _settingsService: SettingsService, + private _formBuilder: NonNullableFormBuilder) { + this.applications = manifestService.applications + .filter(application => application.symbolicName.startsWith('workbench')) + .map(application => application.symbolicName); + } + + protected async onRegister(): Promise { + const intention: Intention = { + type: this.form.controls.type.value, + qualifier: SciKeyValueFieldComponent.toDictionary(this.form.controls.qualifier) ?? undefined, + }; + + this.intentionId = undefined; + this.registerError = undefined; + try { + this.intentionId = await firstValueFrom(this._messageClient.request$(`application/${this.form.controls.application.value}/intention/register`, intention).pipe(mapToBody())); + this.resetForm(); + } + catch (error) { + this.registerError = stringifyError(error); + } + } + + private resetForm(): void { + if (this._settingsService.isEnabled('resetFormsOnSubmit')) { + this.form.reset(); + this.form.setControl('qualifier', this._formBuilder.array>([])); + } + } +} diff --git a/apps/workbench-testing-app/src/app/microfrontend-platform-page/unregister-capability-page/unregister-capability-page.component.html b/apps/workbench-testing-app/src/app/microfrontend-platform-page/unregister-capability-page/unregister-capability-page.component.html new file mode 100644 index 000000000..b7fdada74 --- /dev/null +++ b/apps/workbench-testing-app/src/app/microfrontend-platform-page/unregister-capability-page/unregister-capability-page.component.html @@ -0,0 +1,26 @@ +
+
+ + + + + + + +
+ + + + @if (unregisterError) { + + {{unregisterError}} + + } + @if (unregistered) { + + } +
diff --git a/apps/workbench-client-testing-app/src/app/unregister-workbench-capability-page/unregister-workbench-capability-page.component.scss b/apps/workbench-testing-app/src/app/microfrontend-platform-page/unregister-capability-page/unregister-capability-page.component.scss similarity index 100% rename from apps/workbench-client-testing-app/src/app/unregister-workbench-capability-page/unregister-workbench-capability-page.component.scss rename to apps/workbench-testing-app/src/app/microfrontend-platform-page/unregister-capability-page/unregister-capability-page.component.scss diff --git a/apps/workbench-testing-app/src/app/microfrontend-platform-page/unregister-capability-page/unregister-capability-page.component.ts b/apps/workbench-testing-app/src/app/microfrontend-platform-page/unregister-capability-page/unregister-capability-page.component.ts new file mode 100644 index 000000000..ad01f2a49 --- /dev/null +++ b/apps/workbench-testing-app/src/app/microfrontend-platform-page/unregister-capability-page/unregister-capability-page.component.ts @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2018-2024 Swiss Federal Railways + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import {Component} from '@angular/core'; +import {NonNullableFormBuilder, ReactiveFormsModule, Validators} from '@angular/forms'; +import {ManifestService, mapToBody, MessageClient} from '@scion/microfrontend-platform'; +import {stringifyError} from '../../common/stringify-error.util'; +import {SciFormFieldComponent} from '@scion/components.internal/form-field'; +import {SettingsService} from '../../settings.service'; +import {firstValueFrom} from 'rxjs'; + +@Component({ + selector: 'app-unregister-capability-page', + templateUrl: './unregister-capability-page.component.html', + styleUrls: ['./unregister-capability-page.component.scss'], + standalone: true, + imports: [ + ReactiveFormsModule, + SciFormFieldComponent, + ], +}) +export class UnregisterCapabilityPageComponent { + + protected form = this._formBuilder.group({ + application: this._formBuilder.control('', Validators.required), + id: this._formBuilder.control('', Validators.required), + }); + protected unregisterError: string | undefined; + protected unregistered: boolean | undefined; + protected applications: string[]; + + constructor(manifestService: ManifestService, + private _messageClient: MessageClient, + private _settingsService: SettingsService, + private _formBuilder: NonNullableFormBuilder) { + this.applications = manifestService.applications + .filter(application => application.symbolicName.startsWith('workbench')) + .map(application => application.symbolicName); + } + + protected async onUnregister(): Promise { + this.unregisterError = undefined; + this.unregistered = undefined; + try { + const capabilityId = this.form.controls.id.value; + this.unregistered = await firstValueFrom(this._messageClient.request$(`application/${this.form.controls.application.value}/capability/${capabilityId}/unregister`).pipe(mapToBody())); + this.resetForm(); + } + catch (error) { + this.unregisterError = stringifyError(error); + } + } + + private resetForm(): void { + if (this._settingsService.isEnabled('resetFormsOnSubmit')) { + this.form.reset(); + } + } +} diff --git a/apps/workbench-testing-app/src/app/start-page/start-page.component.html b/apps/workbench-testing-app/src/app/start-page/start-page.component.html index cb4fb71ac..043b87919 100644 --- a/apps/workbench-testing-app/src/app/start-page/start-page.component.html +++ b/apps/workbench-testing-app/src/app/start-page/start-page.component.html @@ -26,30 +26,15 @@ - {{viewCapability.properties.title}} + {{viewCapability.properties.tile.label}} {{viewCapability.metadata!.appSymbolicName}} - - -
- - - {{testCapability.properties?.['cssClass']}} - - {{testCapability.metadata!.appSymbolicName}} - - -
-
diff --git a/apps/workbench-testing-app/src/app/start-page/start-page.component.scss b/apps/workbench-testing-app/src/app/start-page/start-page.component.scss index 31eb85623..7f96f2e0d 100644 --- a/apps/workbench-testing-app/src/app/start-page/start-page.component.scss +++ b/apps/workbench-testing-app/src/app/start-page/start-page.component.scss @@ -56,17 +56,17 @@ font-size: .5em; } - &.microfrontend.devtools { + &.microfrontend[data-app="devtools"] { background-color: var(--devtools-app-color); border-color: unset; } - &:is(.microfrontend, .test-capability).workbench-client-testing-app1 { + &.microfrontend[data-app="workbench-client-testing-app1"] { background-color: var(--workbench-client-testing-app1-color); border-color: unset; } - &:is(.microfrontend, .test-capability).workbench-client-testing-app2 { + &.microfrontend[data-app="workbench-client-testing-app2"] { background-color: var(--workbench-client-testing-app2-color); border-color: unset; } diff --git a/apps/workbench-testing-app/src/app/start-page/start-page.component.ts b/apps/workbench-testing-app/src/app/start-page/start-page.component.ts index 1da7e9e3c..b8f0fc7f9 100644 --- a/apps/workbench-testing-app/src/app/start-page/start-page.component.ts +++ b/apps/workbench-testing-app/src/app/start-page/start-page.component.ts @@ -22,7 +22,6 @@ import {AsyncPipe, NgClass, NgFor, NgIf} from '@angular/common'; import {NullIfEmptyPipe} from '../common/null-if-empty.pipe'; import {FilterPipe} from '../common/filter.pipe'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; -import {ArrayConcatPipe} from '../common/array-concat.pipe'; import {SciTabbarComponent, SciTabDirective} from '@scion/components.internal/tabbar'; @Component({ @@ -42,7 +41,6 @@ import {SciTabbarComponent, SciTabDirective} from '@scion/components.internal/ta SciTabbarComponent, SciTabDirective, FilterPipe, - ArrayConcatPipe, WorkbenchRouterLinkDirective, ], }) @@ -56,8 +54,7 @@ export default class StartPageComponent { public filterControl = this._formBuilder.control(''); public workbenchViewRoutes$: Observable; - public microfrontendViewCapabilities$: Observable | undefined; - public testCapabilities$: Observable | undefined; + public microfrontendViewCapabilities$: Observable | undefined; public WorkbenchRouteData = WorkbenchRouteData; @@ -83,17 +80,9 @@ export default class StartPageComponent { if (workbenchConfig.microfrontendPlatform) { // Read microfrontend views to be pinned to the start page. - this.microfrontendViewCapabilities$ = this._manifestService!.lookupCapabilities$({type: WorkbenchCapabilities.View}) + this.microfrontendViewCapabilities$ = this._manifestService!.lookupCapabilities$({type: WorkbenchCapabilities.View}) .pipe( - filterArray(viewCapability => 'pinToStartPage' in viewCapability.properties && !!viewCapability.properties['pinToStartPage']), - filterArray(viewCapability => !isTestCapability(viewCapability)), - sortArray((a, b) => a.metadata!.appSymbolicName.localeCompare(b.metadata!.appSymbolicName)), - ); - // Read test capabilities to be pinned to the start page. - this.testCapabilities$ = this._manifestService!.lookupCapabilities$() - .pipe( - filterArray(capability => !!capability.properties && 'pinToStartPage' in capability.properties && !!capability.properties['pinToStartPage']), - filterArray(viewCapability => isTestCapability(viewCapability)), + filterArray(viewCapability => !!viewCapability.properties.tile), sortArray((a, b) => a.metadata!.appSymbolicName.localeCompare(b.metadata!.appSymbolicName)), ); } @@ -183,19 +172,20 @@ export default class StartPageComponent { }); } - public selectViewCapabilityText = (viewCapability: WorkbenchViewCapability): string | undefined => { + public selectViewCapabilityText = (viewCapability: WorkbenchViewTestingAppCapability): string | undefined => { return viewCapability.properties!.title; }; - public selectTestCapabilityText = (testCapability: Capability): string | undefined => { - return testCapability.properties?.['cssClass']; - }; - public selectViewRouteText = (route: Route): string | undefined => { return route.data?.[WorkbenchRouteData.title]; }; } -function isTestCapability(capability: Capability): boolean { - return Object.keys(capability.qualifier ?? {}).includes('test'); -} +type WorkbenchViewTestingAppCapability = WorkbenchViewCapability & { + properties: { + tile: { + label: string; + cssClass: string; + }; + }; +}; diff --git a/apps/workbench-testing-app/src/app/workbench.manifest.ts b/apps/workbench-testing-app/src/app/workbench.manifest.ts index aea4c9303..a480d9f6a 100644 --- a/apps/workbench-testing-app/src/app/workbench.manifest.ts +++ b/apps/workbench-testing-app/src/app/workbench.manifest.ts @@ -1,4 +1,4 @@ -import {WorkbenchCapabilities, WorkbenchDialogCapability, WorkbenchNotificationCapability, WorkbenchPopupCapability} from '@scion/workbench-client'; +import {WorkbenchCapabilities, WorkbenchDialogCapability, WorkbenchNotificationCapability, WorkbenchPopupCapability, WorkbenchViewCapability} from '@scion/workbench-client'; import {Manifest} from '@scion/microfrontend-platform'; /** @@ -7,6 +7,19 @@ import {Manifest} from '@scion/microfrontend-platform'; export const workbenchManifest: Manifest = { name: 'Workbench Host App', capabilities: [ + { + type: WorkbenchCapabilities.View, + qualifier: {component: 'microfrontend-platform'}, + private: false, + description: 'Allows interacting with SCION Microfrontend Platform.', + properties: { + path: 'microfrontend-platform', + tile: { + label: 'Microfrontend Platform', + cssClass: 'e2e-microfrontend-platform', + }, + }, + } satisfies WorkbenchViewCapability, { type: WorkbenchCapabilities.Notification, qualifier: {component: 'notification-page'}, diff --git a/apps/workbench-testing-app/src/styles.scss b/apps/workbench-testing-app/src/styles.scss index ea158c4fc..c7212e31a 100644 --- a/apps/workbench-testing-app/src/styles.scss +++ b/apps/workbench-testing-app/src/styles.scss @@ -37,6 +37,6 @@ button:not([class*="material-icons"]):not([class*="material-symbols"]) { :root { --workbench-client-testing-app1-color: rgb(0, 49, 83); - --workbench-client-testing-app2-color: rgb(0, 112, 187); + --workbench-client-testing-app2-color: rgb(71, 137, 180); --devtools-app-color: rgb(72, 72, 72); } diff --git a/projects/scion/e2e-testing/src/start-page.po.ts b/projects/scion/e2e-testing/src/start-page.po.ts index 33059d12a..a979d3f9d 100644 --- a/projects/scion/e2e-testing/src/start-page.po.ts +++ b/projects/scion/e2e-testing/src/start-page.po.ts @@ -16,6 +16,7 @@ import {SciRouterOutletPO} from './workbench-client/page-object/sci-router-outle import {WorkbenchViewPagePO} from './workbench/page-object/workbench-view-page.po'; import {ViewId} from '@scion/workbench'; import {waitForCondition} from './helper/testing.util'; +import {Application} from './workbench/page-object/microfrontend-platform-page/application'; /** * Page object to interact with {@link StartPageComponent}. @@ -72,24 +73,18 @@ export class StartPagePO implements WorkbenchViewPagePO { /** * Clicks the microfrontend view tile with specified CSS class set. */ - public async openMicrofrontendView(cssClass: string, app: string): Promise { + public async openMicrofrontendView(cssClass: string, app: Application): Promise { const viewId = await this.view.getViewId(); const navigationId = await this._appPO.getCurrentNavigationId(); await this._tabbar.selectTab('e2e-microfrontend-views'); - await this._tabbarLocator.locator(`.e2e-microfrontend-view-tiles a.${cssClass}.workbench-client-testing-${app}`).click(); + await this._tabbarLocator.locator(`.e2e-microfrontend-view-tiles a.${cssClass}[data-app="${app}"]`).click(); await this._appPO.view({viewId, cssClass}).waitUntilAttached(); - // Wait for microfrontend to be loaded. - const frameLocator = new SciRouterOutletPO(this._appPO, {name: viewId}).frameLocator; - await frameLocator.locator('app-root').waitFor({state: 'visible'}); + if (app !== 'workbench-host-app') { + // Wait for microfrontend to be loaded. + const frameLocator = new SciRouterOutletPO(this._appPO, {name: viewId}).frameLocator; + await frameLocator.locator('app-root').waitFor({state: 'visible'}); + } // Wait until completed navigation. await waitForCondition(async () => (await this._appPO.getCurrentNavigationId()) !== navigationId); } - - /** - * Clicks the test capability tile with specified CSS class set. - */ - public async clickTestCapability(capabilityCssClass: string, app: string): Promise { - await this._tabbar.selectTab('e2e-test-capabilities'); - await this._tabbarLocator.locator(`.e2e-test-capability-tiles a.${capabilityCssClass}.workbench-client-testing-${app}`).click(); - } } diff --git a/projects/scion/e2e-testing/src/workbench-client/dialog/dialog-splash.e2e-spec.ts b/projects/scion/e2e-testing/src/workbench-client/dialog/dialog-splash.e2e-spec.ts index 0e11cb3ac..757dba77d 100644 --- a/projects/scion/e2e-testing/src/workbench-client/dialog/dialog-splash.e2e-spec.ts +++ b/projects/scion/e2e-testing/src/workbench-client/dialog/dialog-splash.e2e-spec.ts @@ -12,7 +12,7 @@ import {test} from '../../fixtures'; import {expect} from '@playwright/test'; import {DialogOpenerPagePO} from '../page-object/dialog-opener-page.po'; import {DialogPagePO} from '../page-object/dialog-page.po'; -import {MessagingPagePO} from '../page-object/messaging-page.po'; +import {MicrofrontendPlatformPagePO} from '../../workbench/page-object/microfrontend-platform-page/microfrontend-platform-page.po'; test.describe('Workbench Dialog Splash', () => { @@ -41,9 +41,9 @@ test.describe('Workbench Dialog Splash', () => { await expect(dialogPage.outlet.splash).toBeVisible(); // Publish message to dispose splash. - const messagingPage = await microfrontendNavigator.openInNewTab(MessagingPagePO, 'app1'); - await messagingPage.publishMessage(`signal-ready/${dialogCapability.metadata!.id}`); - await messagingPage.view.tab.close(); + const microfrontendPlatformPage = await microfrontendNavigator.openInNewTab(MicrofrontendPlatformPagePO); + await microfrontendPlatformPage.publishMessage(`signal-ready/${dialogCapability.metadata!.id}`); + await microfrontendPlatformPage.view.tab.close(); // Expect splash not to display. await expect(dialogPage.outlet.splash).not.toBeVisible(); diff --git a/projects/scion/e2e-testing/src/workbench-client/host-view.e2e-spec.ts b/projects/scion/e2e-testing/src/workbench-client/host-view.e2e-spec.ts new file mode 100644 index 000000000..3f252229b --- /dev/null +++ b/projects/scion/e2e-testing/src/workbench-client/host-view.e2e-spec.ts @@ -0,0 +1,333 @@ +/* + * Copyright (c) 2018-2024 Swiss Federal Railways + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import {test} from '../fixtures'; +import {expect} from '@playwright/test'; +import {RouterPagePO} from './page-object/router-page.po'; +import {ViewPagePO} from '../workbench/page-object/view-page.po'; +import {expectView} from '../matcher/view-matcher'; +import {WorkbenchViewCapability} from '../workbench/page-object/microfrontend-platform-page/register-capability-page.po'; +import {ViewInfo} from '../workbench/page-object/view-info-dialog.po'; + +test.describe('Workbench Host View', () => { + + test('should open a view contributed by the host app', async ({appPO, microfrontendNavigator}) => { + await appPO.navigateTo({microfrontendSupport: true}); + + await microfrontendNavigator.registerCapability('host', { + type: 'view', + qualifier: {component: 'testee'}, + private: false, + properties: { + path: 'test-view', + }, + }); + await microfrontendNavigator.registerIntention('app1', {type: 'view', qualifier: {component: 'testee'}}); + + // Open the view. + const routerPage = await microfrontendNavigator.openInNewTab(RouterPagePO, 'app1'); + await routerPage.enterQualifier({component: 'testee'}); + await routerPage.enterCssClass('testee'); + await routerPage.clickNavigate(); + + const viewPage = new ViewPagePO(appPO, {cssClass: 'testee'}); + + // Expect view page to be displayed. + await expectView(viewPage).toBeActive(); + }); + + test('should substitute placeholders in the path', async ({appPO, microfrontendNavigator}) => { + await appPO.navigateTo({microfrontendSupport: true}); + + await microfrontendNavigator.registerCapability('host', { + type: 'view', + qualifier: {component: 'testee'}, + private: false, + params: [ + {name: 'param1', required: true}, + {name: 'param2', required: false}, + ], + properties: { + path: 'test-view;p1=:param1;p2=:param2', + }, + }); + await microfrontendNavigator.registerIntention('app1', {type: 'view', qualifier: {component: 'testee'}}); + + // Open the view. + const routerPage = await microfrontendNavigator.openInNewTab(RouterPagePO, 'app1'); + await routerPage.enterQualifier({component: 'testee'}); + await routerPage.enterParams({param1: 'PARAM 1', param2: 'PARAM 2'}); + await routerPage.enterCssClass('testee'); + await routerPage.clickNavigate(); + + // Expect params to be available in route params. + const view = appPO.view({cssClass: 'testee'}); + await expect.poll(() => view.getInfo()).toMatchObject( + { + urlSegments: encodeURI('test-view;p1=PARAM 1;p2=PARAM 2'), + routeParams: ({p1: 'PARAM 1', p2: 'PARAM 2'}), + } satisfies Partial, + ); + }); + + test('should provide transient params as state', async ({appPO, microfrontendNavigator}) => { + await appPO.navigateTo({microfrontendSupport: true}); + + await microfrontendNavigator.registerCapability('host', { + type: 'view', + qualifier: {component: 'testee'}, + private: false, + params: [ + {name: 'param', required: true, transient: true}, + ], + properties: { + path: 'test-view;param=:param', + }, + }); + await microfrontendNavigator.registerIntention('app1', {type: 'view', qualifier: {component: 'testee'}}); + + // Open the view. + const routerPage = await microfrontendNavigator.openInNewTab(RouterPagePO, 'app1'); + await routerPage.enterQualifier({component: 'testee'}); + await routerPage.enterParams({param: 'TRANSIENT PARAM'}); + await routerPage.enterCssClass('testee'); + await routerPage.clickNavigate(); + + // Expect transient params to be available in state. + const view = appPO.view({cssClass: 'testee'}); + await expect.poll(() => view.getInfo()).toMatchObject( + { + urlSegments: 'test-view;param=:param', + routeParams: ({param: ':param'}), + state: {'transientParams.param': 'TRANSIENT PARAM'}, + } satisfies Partial, + ); + }); + + test('should provide capability id for placeholder substitution in path', async ({appPO, microfrontendNavigator}) => { + await appPO.navigateTo({microfrontendSupport: true}); + + const capability = await microfrontendNavigator.registerCapability('host', { + type: 'view', + qualifier: {component: 'testee'}, + private: false, + properties: { + path: 'test-view;capabilityId=:capabilityId', + }, + }); + const capabilityId = capability.metadata!.id; + await microfrontendNavigator.registerIntention('app1', {type: 'view', qualifier: {component: 'testee'}}); + + // Open the view. + const routerPage = await microfrontendNavigator.openInNewTab(RouterPagePO, 'app1'); + await routerPage.enterQualifier({component: 'testee'}); + await routerPage.enterCssClass('testee'); + await routerPage.clickNavigate(); + + // Expect capability id to be substituted. + const view = appPO.view({cssClass: 'testee'}); + await expect.poll(() => view.getInfo()).toMatchObject( + { + urlSegments: `test-view;capabilityId=${capabilityId}`, + routeParams: ({capabilityId: capabilityId}), + } satisfies Partial, + ); + }); + + test('should navigate existing view of same path (optional param does not match)', async ({appPO, microfrontendNavigator}) => { + await appPO.navigateTo({microfrontendSupport: true}); + + await microfrontendNavigator.registerCapability('host', { + type: 'view', + qualifier: {component: 'testee'}, + params: [ + {name: 'optionalParam', required: false}, + ], + private: false, + properties: { + path: 'test-pages/navigation-test-page;param=:optionalParam', + }, + }); + await microfrontendNavigator.registerIntention('app1', {type: 'view', qualifier: {component: 'testee'}}); + + const routerPage = await microfrontendNavigator.openInNewTab(RouterPagePO, 'app1'); + + // Open the view. + await routerPage.enterQualifier({component: 'testee'}); + await routerPage.enterCssClass(['testee', 'testee-1']); + await routerPage.enterParams({optionalParam: 'param1'}); + await routerPage.enterTarget('view.100'); + await routerPage.clickNavigate(); + + // Expect view to be opened. + const view1 = appPO.view({cssClass: 'testee-1'}); + await expect.poll(() => view1.getInfo()).toMatchObject( + { + urlSegments: 'test-pages/navigation-test-page;param=param1', + routeParams: ({param: 'param1'}), + } satisfies Partial, + ); + await expect(appPO.views({cssClass: 'testee'})).toHaveCount(1); + + // Navigate with different optional param. + await routerPage.view.tab.click(); + await routerPage.enterQualifier({component: 'testee'}); + await routerPage.enterCssClass(['testee', 'testee-2']); + await routerPage.enterParams({optionalParam: 'param2'}); + await routerPage.clickNavigate(); + + // Expect existing view to be navigated. + const view2 = appPO.view({cssClass: 'testee-2'}); + await expect.poll(() => view2.getInfo()).toMatchObject( + { + viewId: 'view.100', + urlSegments: 'test-pages/navigation-test-page;param=param2', + routeParams: ({param: 'param2'}), + } satisfies Partial, + ); + await expect(appPO.views({cssClass: 'testee'})).toHaveCount(1); + }); + + test('should navigate existing view of same path (required param does not match)', async ({appPO, microfrontendNavigator}) => { + await appPO.navigateTo({microfrontendSupport: true}); + + await microfrontendNavigator.registerCapability('host', { + type: 'view', + qualifier: {component: 'testee'}, + params: [ + {name: 'requiredParam', required: true}, + ], + private: false, + properties: { + path: 'test-pages/navigation-test-page/:requiredParam', + }, + }); + await microfrontendNavigator.registerIntention('app1', {type: 'view', qualifier: {component: 'testee'}}); + + const routerPage = await microfrontendNavigator.openInNewTab(RouterPagePO, 'app1'); + + // Open the view. + await routerPage.enterQualifier({component: 'testee'}); + await routerPage.enterCssClass(['testee', 'testee-1']); + await routerPage.enterParams({requiredParam: 'param1'}); + await routerPage.enterTarget('view.100'); + await routerPage.clickNavigate(); + + // Expect view to be opened. + const view1 = appPO.view({cssClass: 'testee-1'}); + await expect.poll(() => view1.getInfo()).toMatchObject( + { + urlSegments: 'test-pages/navigation-test-page/param1', + routeParams: ({segment1: 'param1'}), + } satisfies Partial, + ); + await expect(appPO.views({cssClass: 'testee'})).toHaveCount(1); + + // Navigate with different required param. + await routerPage.view.tab.click(); + await routerPage.enterQualifier({component: 'testee'}); + await routerPage.enterCssClass(['testee', 'testee-2']); + await routerPage.enterParams({requiredParam: 'param2'}); + await routerPage.enterTarget(undefined); + await routerPage.clickNavigate(); + + // Expect new view to be opened. + await expect.poll(() => view1.getInfo()).toMatchObject( + { + viewId: expect.stringMatching('view.100') as any, + urlSegments: 'test-pages/navigation-test-page/param1', + routeParams: ({segment1: 'param1'}), + } satisfies Partial, + ); + const view2 = appPO.view({cssClass: 'testee-2'}); + await expect.poll(() => view2.getInfo()).toMatchObject( + { + viewId: expect.not.stringMatching('view.100') as any, + urlSegments: 'test-pages/navigation-test-page/param2', + routeParams: ({segment1: 'param2'}), + } satisfies Partial, + ); + await expect(appPO.views({cssClass: 'testee'})).toHaveCount(2); + }); + + // TODO: + // ERROR when setting title or css class on capability --> or associate it with data in layout + // Handle für Hosts entfernen (geht bei view nicht, ist vielleicht verwirrend) + + test('should error if host capability defines the "title" property', async ({appPO, microfrontendNavigator}) => { + await appPO.navigateTo({microfrontendSupport: true}); + + const registeredCapability = microfrontendNavigator.registerCapability('host', { + type: 'view', + qualifier: {component: 'testee'}, + properties: { + path: 'path/to/view', + title: 'unsupported', + }, + }); + await expect(registeredCapability).rejects.toThrow(/UnsupportedCapabilityProperty/); + }); + + test('should error if host capability defines the "heading" property', async ({appPO, microfrontendNavigator}) => { + await appPO.navigateTo({microfrontendSupport: true}); + + const registeredCapability = microfrontendNavigator.registerCapability('host', { + type: 'view', + qualifier: {component: 'testee'}, + properties: { + path: 'path/to/view', + heading: 'unsupported', + }, + }); + await expect(registeredCapability).rejects.toThrow(/UnsupportedCapabilityProperty/); + }); + + test('should error if host capability defines the "closable" property', async ({appPO, microfrontendNavigator}) => { + await appPO.navigateTo({microfrontendSupport: true}); + + const registeredCapability = microfrontendNavigator.registerCapability('host', { + type: 'view', + qualifier: {component: 'testee'}, + properties: { + path: 'path/to/view', + closable: true, // unsupported + }, + }); + await expect(registeredCapability).rejects.toThrow(/UnsupportedCapabilityProperty/); + }); + + test('should error if host capability defines the "cssClass" property', async ({appPO, microfrontendNavigator}) => { + await appPO.navigateTo({microfrontendSupport: true}); + + const registeredCapability = microfrontendNavigator.registerCapability('host', { + type: 'view', + qualifier: {component: 'testee'}, + properties: { + path: 'path/to/view', + cssClass: 'unsupported', + }, + }); + await expect(registeredCapability).rejects.toThrow(/UnsupportedCapabilityProperty/); + }); + + test('should error if host capability defines the "showSplash" property', async ({appPO, microfrontendNavigator}) => { + await appPO.navigateTo({microfrontendSupport: true}); + + const registeredCapability = microfrontendNavigator.registerCapability('host', { + type: 'view', + qualifier: {component: 'testee'}, + properties: { + path: 'path/to/view', + showSplash: true, // unsupported + }, + }); + await expect(registeredCapability).rejects.toThrow(/UnsupportedCapabilityProperty/); + }); +}); diff --git a/projects/scion/e2e-testing/src/workbench-client/message-box/message-box-splash.e2e-spec.ts b/projects/scion/e2e-testing/src/workbench-client/message-box/message-box-splash.e2e-spec.ts index 01fa417a3..05e6b2801 100644 --- a/projects/scion/e2e-testing/src/workbench-client/message-box/message-box-splash.e2e-spec.ts +++ b/projects/scion/e2e-testing/src/workbench-client/message-box/message-box-splash.e2e-spec.ts @@ -10,9 +10,9 @@ import {test} from '../../fixtures'; import {expect} from '@playwright/test'; -import {MessagingPagePO} from '../page-object/messaging-page.po'; import {MessageBoxOpenerPagePO} from '../page-object/message-box-opener-page.po'; import {MessageBoxPagePO} from '../page-object/message-box-page.po'; +import {MicrofrontendPlatformPagePO} from '../../workbench/page-object/microfrontend-platform-page/microfrontend-platform-page.po'; test.describe('Workbench Message Box Splash', () => { @@ -41,9 +41,9 @@ test.describe('Workbench Message Box Splash', () => { await expect(messageBoxPage.outlet.splash).toBeVisible(); // Publish message to dispose splash. - const messagingPage = await microfrontendNavigator.openInNewTab(MessagingPagePO, 'app1'); - await messagingPage.publishMessage(`signal-ready/${messageBoxCapability.metadata!.id}`); - await messagingPage.view.tab.close(); + const microfrontendPlatformPage = await microfrontendNavigator.openInNewTab(MicrofrontendPlatformPagePO); + await microfrontendPlatformPage.publishMessage(`signal-ready/${messageBoxCapability.metadata!.id}`); + await microfrontendPlatformPage.view.tab.close(); // Expect splash not to display. await expect(messageBoxPage.outlet.splash).not.toBeVisible(); diff --git a/projects/scion/e2e-testing/src/workbench-client/microfrontend-navigator.ts b/projects/scion/e2e-testing/src/workbench-client/microfrontend-navigator.ts index 16cd2d9b2..c90d4e38b 100644 --- a/projects/scion/e2e-testing/src/workbench-client/microfrontend-navigator.ts +++ b/projects/scion/e2e-testing/src/workbench-client/microfrontend-navigator.ts @@ -10,16 +10,14 @@ import {AppPO} from '../app.po'; import {MessageBoxOpenerPagePO} from './page-object/message-box-opener-page.po'; -import {RegisterWorkbenchIntentionPagePO} from './page-object/register-workbench-intention-page.po'; -import {RegisterWorkbenchCapabilityPagePO, WorkbenchDialogCapability, WorkbenchMessageBoxCapability, WorkbenchPopupCapability, WorkbenchViewCapability} from './page-object/register-workbench-capability-page.po'; import {ViewPagePO} from './page-object/view-page.po'; -import {UnregisterWorkbenchCapabilityPagePO} from './page-object/unregister-workbench-capability-page.po'; import {NotificationOpenerPagePO} from './page-object/notification-opener-page.po'; import {RouterPagePO} from './page-object/router-page.po'; import {PopupOpenerPagePO} from './page-object/popup-opener-page.po'; -import {MessagingPagePO} from './page-object/messaging-page.po'; import {Capability, Intention} from '@scion/microfrontend-platform'; import {DialogOpenerPagePO} from './page-object/dialog-opener-page.po'; +import {MicrofrontendPlatformPagePO} from '../workbench/page-object/microfrontend-platform-page/microfrontend-platform-page.po'; +import {Application} from '../workbench/page-object/microfrontend-platform-page/application'; export interface Type extends Function { // eslint-disable-line @typescript-eslint/ban-types new(...args: any[]): T; @@ -41,18 +39,6 @@ export class MicrofrontendNavigator { * Opens the page to test the notification in a new workbench tab. */ public openInNewTab(page: Type, app: 'app1' | 'app2'): Promise; - /** - * Opens the page to register intentions in a new workbench tab. - */ - public openInNewTab(page: Type, app: 'app1' | 'app2'): Promise; - /** - * Opens the page to register capabilities in a new workbench tab. - */ - public openInNewTab(page: Type, app: 'app1' | 'app2'): Promise; - /** - * Opens the page to unregister capabilities in a new workbench tab. - */ - public openInNewTab(page: Type, app: 'app1' | 'app2'): Promise; /** * Opens the page to inspect view properties in a new workbench tab. */ @@ -70,54 +56,43 @@ export class MicrofrontendNavigator { */ public openInNewTab(page: Type, app: 'app1' | 'app2'): Promise; /** - * Opens the page to exchange messages in a new workbench tab. + * Opens the page to interact with the SCION Microfrontend Platform */ - public openInNewTab(page: Type, app: 'app1' | 'app2'): Promise; + public openInNewTab(page: Type): Promise; - public async openInNewTab(page: Type, app: 'app1' | 'app2'): Promise { + public async openInNewTab(page: Type, app?: 'app1' | 'app2' | 'host'): Promise { + const application = this.resolveApplication(app ?? 'host'); const startPage = await this._appPO.openNewViewTab(); const viewId = await startPage.view.getViewId(); switch (page) { case MessageBoxOpenerPagePO: { - await startPage.openMicrofrontendView('e2e-test-message-box-opener', app); + await startPage.openMicrofrontendView('e2e-test-message-box-opener', application); return new MessageBoxOpenerPagePO(this._appPO, {viewId, cssClass: 'e2e-test-message-box-opener'}); } - case RegisterWorkbenchIntentionPagePO: { - await startPage.openMicrofrontendView('e2e-register-workbench-intention', app); - return new RegisterWorkbenchIntentionPagePO(this._appPO, {viewId, cssClass: 'e2e-register-workbench-intention'}); - } - case RegisterWorkbenchCapabilityPagePO: { - await startPage.openMicrofrontendView('e2e-register-workbench-capability', app); - return new RegisterWorkbenchCapabilityPagePO(this._appPO, {viewId, cssClass: 'e2e-register-workbench-capability'}); - } case ViewPagePO: { - await startPage.openMicrofrontendView('e2e-test-view', app); + await startPage.openMicrofrontendView('e2e-test-view', application); return new ViewPagePO(this._appPO, {viewId, cssClass: 'e2e-test-view'}); } - case UnregisterWorkbenchCapabilityPagePO: { - await startPage.openMicrofrontendView('e2e-unregister-workbench-capability', app); - return new UnregisterWorkbenchCapabilityPagePO(this._appPO, {viewId, cssClass: 'e2e-unregister-workbench-capability'}); - } case NotificationOpenerPagePO: { - await startPage.openMicrofrontendView('e2e-test-notification-opener', app); + await startPage.openMicrofrontendView('e2e-test-notification-opener', application); return new NotificationOpenerPagePO(this._appPO, {viewId, cssClass: 'e2e-test-notification-opener'}); } case RouterPagePO: { - await startPage.openMicrofrontendView('e2e-test-router', app); + await startPage.openMicrofrontendView('e2e-test-router', application); return new RouterPagePO(this._appPO, {viewId, cssClass: 'e2e-test-router'}); } case PopupOpenerPagePO: { - await startPage.openMicrofrontendView('e2e-test-popup-opener', app); + await startPage.openMicrofrontendView('e2e-test-popup-opener', application); return new PopupOpenerPagePO(this._appPO, {viewId, cssClass: 'e2e-test-popup-opener'}); } case DialogOpenerPagePO: { - await startPage.openMicrofrontendView('e2e-test-dialog-opener', app); + await startPage.openMicrofrontendView('e2e-test-dialog-opener', application); return new DialogOpenerPagePO(this._appPO, {viewId, cssClass: 'e2e-test-dialog-opener'}); } - case MessagingPagePO: { - await startPage.openMicrofrontendView('e2e-messaging', app); - return new MessagingPagePO(this._appPO, {viewId, cssClass: 'e2e-messaging'}); + case MicrofrontendPlatformPagePO: { + await startPage.openMicrofrontendView('e2e-microfrontend-platform', application); + return new MicrofrontendPlatformPagePO(this._appPO, {viewId, cssClass: 'e2e-microfrontend-platform'}); } default: { throw Error(`[TestError] Page not supported to be opened in a new tab. [page=${page}]`); @@ -128,13 +103,13 @@ export class MicrofrontendNavigator { /** * Use to register a workbench capability. */ - public async registerCapability(app: 'app1' | 'app2', capability: T): Promise { - const registerCapabilityPage = await this.openInNewTab(RegisterWorkbenchCapabilityPagePO, app); + public async registerCapability(app: 'app1' | 'app2' | 'host', capability: T): Promise { + const microfrontendPlatformPage = await this.openInNewTab(MicrofrontendPlatformPagePO); try { - return await registerCapabilityPage.registerCapability(capability); + return await microfrontendPlatformPage.registerCapability(this.resolveApplication(app), capability); } finally { - await registerCapabilityPage.view.tab.close(); + await microfrontendPlatformPage.view.tab.close(); } } @@ -142,12 +117,12 @@ export class MicrofrontendNavigator { * Use to register a workbench intention. */ public async registerIntention(app: 'app1' | 'app2', intention: Intention & {type: 'view' | 'dialog' | 'popup' | 'messagebox' | 'notification'}): Promise { - const registerIntentionPage = await this.openInNewTab(RegisterWorkbenchIntentionPagePO, app); + const microfrontendPlatformPage = await this.openInNewTab(MicrofrontendPlatformPagePO); try { - return await registerIntentionPage.registerIntention(intention); + return await microfrontendPlatformPage.registerIntention(this.resolveApplication(app), intention); } finally { - await registerIntentionPage.view.tab.close(); + await microfrontendPlatformPage.view.tab.close(); } } @@ -155,12 +130,26 @@ export class MicrofrontendNavigator { * Use to unregister a workbench capability. */ public async unregisterCapability(app: 'app1' | 'app2', id: string): Promise { - const unregisterCapabilityPage = await this.openInNewTab(UnregisterWorkbenchCapabilityPagePO, app); + const microfrontendPlatformPage = await this.openInNewTab(MicrofrontendPlatformPagePO); try { - return await unregisterCapabilityPage.unregisterCapability(id); + return await microfrontendPlatformPage.unregisterCapability(this.resolveApplication(app), id); } finally { - await unregisterCapabilityPage.view.tab.close(); + await microfrontendPlatformPage.view.tab.close(); + } + } + + private resolveApplication(app: 'host' | 'app1' | 'app2'): Application { + switch (app) { + case 'app1': + return 'workbench-client-testing-app1'; + case 'app2': + return 'workbench-client-testing-app2'; + case 'host': + return 'workbench-host-app'; + default: { + throw Error('[PageObjectError] Unkown application. Known applications are: ...'); + } } } } diff --git a/projects/scion/e2e-testing/src/workbench-client/page-object/messaging-page.po.ts b/projects/scion/e2e-testing/src/workbench-client/page-object/messaging-page.po.ts deleted file mode 100644 index e967d99a6..000000000 --- a/projects/scion/e2e-testing/src/workbench-client/page-object/messaging-page.po.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (c) 2018-2023 Swiss Federal Railways - * - * This program and the accompanying materials are made - * available under the terms from the Eclipse Public License 2.0 - * which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - */ - -import {AppPO} from '../../app.po'; -import {Locator} from '@playwright/test'; -import {ViewPO} from '../../view.po'; -import {SciTabbarPO} from '../../@scion/components.internal/tabbar.po'; -import {SciRouterOutletPO} from './sci-router-outlet.po'; -import {rejectWhenAttached, waitUntilAttached} from '../../helper/testing.util'; -import {MicrofrontendViewPagePO} from '../../workbench/page-object/workbench-view-page.po'; -import {ViewId} from '@scion/workbench-client'; - -/** - * Page object to interact with {@link MessagingPageComponent}. - */ -export class MessagingPagePO implements MicrofrontendViewPagePO { - - public readonly locator: Locator; - private readonly _tabbar: SciTabbarPO; - - public readonly view: ViewPO; - public readonly outlet: SciRouterOutletPO; - - constructor(appPO: AppPO, locateBy: {viewId?: ViewId; cssClass?: string}) { - this.view = appPO.view({viewId: locateBy.viewId, cssClass: locateBy.cssClass}); - this.outlet = new SciRouterOutletPO(appPO, {name: locateBy.viewId, cssClass: locateBy.cssClass}); - this.locator = this.outlet.frameLocator.locator('app-messaging-page'); - this._tabbar = new SciTabbarPO(this.locator.locator('sci-tabbar.e2e-messaging')); - } - - public async publishMessage(topic: string): Promise { - await this.view.tab.click(); - await this._tabbar.selectTab('e2e-publish-message'); - - const locator = this.locator.locator('app-publish-message-page'); - await locator.locator('input.e2e-topic').fill(topic); - await locator.locator('button.e2e-publish').click(); - - // Evaluate the response: resolve the promise on success, or reject it on error. - const successLocator = locator.locator('output.e2e-publish-success'); - const errorLocator = locator.locator('output.e2e-publish-error'); - await Promise.race([ - waitUntilAttached(successLocator), - rejectWhenAttached(errorLocator), - ]); - } -} diff --git a/projects/scion/e2e-testing/src/workbench-client/page-object/register-workbench-intention-page.po.ts b/projects/scion/e2e-testing/src/workbench-client/page-object/register-workbench-intention-page.po.ts deleted file mode 100644 index a738fc7e8..000000000 --- a/projects/scion/e2e-testing/src/workbench-client/page-object/register-workbench-intention-page.po.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright (c) 2018-2024 Swiss Federal Railways - * - * This program and the accompanying materials are made - * available under the terms from the Eclipse Public License 2.0 - * which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - */ - -import {AppPO} from '../../app.po'; -import {Intention, Qualifier} from '@scion/microfrontend-platform'; -import {SciKeyValueFieldPO} from '../../@scion/components.internal/key-value-field.po'; -import {Locator} from '@playwright/test'; -import {rejectWhenAttached} from '../../helper/testing.util'; -import {SciRouterOutletPO} from './sci-router-outlet.po'; -import {MicrofrontendViewPagePO} from '../../workbench/page-object/workbench-view-page.po'; -import {ViewPO} from '../../view.po'; -import {ViewId} from '@scion/workbench-client'; - -/** - * Page object to interact with {@link RegisterWorkbenchIntentionPageComponent}. - */ -export class RegisterWorkbenchIntentionPagePO implements MicrofrontendViewPagePO { - - public readonly locator: Locator; - public readonly outlet: SciRouterOutletPO; - public readonly view: ViewPO; - - constructor(appPO: AppPO, locateBy: {viewId?: ViewId; cssClass?: string}) { - this.view = appPO.view({viewId: locateBy.viewId, cssClass: locateBy.cssClass}); - this.outlet = new SciRouterOutletPO(appPO, {name: locateBy.viewId, cssClass: locateBy.cssClass}); - this.locator = this.outlet.frameLocator.locator('app-register-workbench-intention-page'); - } - - /** - * Registers the given workbench intention. - * - * This method exists as a convenience method to not have to enter all fields separately. - * - * Returns a Promise that resolves to the intention ID upon successful registration, or that rejects on registration error. - */ - public async registerIntention(intention: Intention & {type: 'view' | 'dialog' | 'popup' | 'messagebox' | 'notification'}): Promise { - await this.selectType(intention.type); - await this.enterQualifier(intention.qualifier); - await this.clickRegister(); - - // Evaluate the response: resolve the promise on success, or reject it on error. - const responseLocator = this.locator.locator('output.e2e-register-response'); - const errorLocator = this.locator.locator('output.e2e-register-error'); - return Promise.race([ - responseLocator.waitFor({state: 'attached'}).then(() => responseLocator.locator('span.e2e-intention-id').innerText()), - rejectWhenAttached(errorLocator), - ]); - } - - public async selectType(type: 'view' | 'dialog' | 'popup' | 'messagebox' | 'notification'): Promise { - await this.locator.locator('select.e2e-type').selectOption(type); - } - - public async enterQualifier(qualifier: Qualifier | undefined): Promise { - const keyValueField = new SciKeyValueFieldPO(this.locator.locator('sci-key-value-field.e2e-qualifier')); - await keyValueField.clear(); - if (qualifier && Object.keys(qualifier).length) { - await keyValueField.addEntries(qualifier); - } - } - - public async clickRegister(): Promise { - await this.locator.locator('button.e2e-register').click(); - } -} - diff --git a/projects/scion/e2e-testing/src/workbench-client/page-object/test-pages/angular-zone-test-page.po.ts b/projects/scion/e2e-testing/src/workbench-client/page-object/test-pages/angular-zone-test-page.po.ts index 1f5bcfc33..7f575822e 100644 --- a/projects/scion/e2e-testing/src/workbench-client/page-object/test-pages/angular-zone-test-page.po.ts +++ b/projects/scion/e2e-testing/src/workbench-client/page-object/test-pages/angular-zone-test-page.po.ts @@ -16,7 +16,7 @@ import {MicrofrontendNavigator} from '../../microfrontend-navigator'; import {SciRouterOutletPO} from '../sci-router-outlet.po'; import {MicrofrontendViewPagePO} from '../../../workbench/page-object/workbench-view-page.po'; import {ViewPO} from '../../../view.po'; -import {ViewId} from '@scion/workbench-client'; +import {RouterPagePO} from '../router-page.po'; export class AngularZoneTestPagePO implements MicrofrontendViewPagePO { @@ -30,9 +30,9 @@ export class AngularZoneTestPagePO implements MicrofrontendViewPagePO { activePanel: PanelPO; }; - constructor(appPO: AppPO, viewId: ViewId) { - this.view = appPO.view({viewId}); - this.outlet = new SciRouterOutletPO(appPO, {name: viewId}); + constructor(appPO: AppPO, cssClass: string) { + this.view = appPO.view({cssClass}); + this.outlet = new SciRouterOutletPO(appPO, {cssClass}); this.locator = this.outlet.frameLocator.locator('app-angular-zone-test-page'); this.workbenchView = { @@ -48,21 +48,16 @@ export class AngularZoneTestPagePO implements MicrofrontendViewPagePO { qualifier: {test: 'angular-zone'}, properties: { path: 'test-pages/angular-zone-test-page', - cssClass: 'e2e-test-angular-zone', title: 'Angular Zone Test Page', - pinToStartPage: true, }, }); - // Navigate to the view. - const startPage = await appPO.openNewViewTab(); - const viewId = await startPage.view.getViewId(); - await startPage.clickTestCapability('e2e-test-angular-zone', 'app1'); - - // Create the page object. - const view = appPO.view({cssClass: 'e2e-test-angular-zone', viewId: viewId}); - await view.waitUntilAttached(); - return new AngularZoneTestPagePO(appPO, viewId); + const cssClass = `testee-${crypto.randomUUID()}`; + const routerPage = await microfrontendNavigator.openInNewTab(RouterPagePO, 'app1'); + await routerPage.enterQualifier({test: 'angular-zone'}); + await routerPage.enterCssClass(cssClass); + await routerPage.clickNavigate(); + return new AngularZoneTestPagePO(appPO, cssClass); } } diff --git a/projects/scion/e2e-testing/src/workbench-client/page-object/test-pages/bulk-navigation-test-page.po.ts b/projects/scion/e2e-testing/src/workbench-client/page-object/test-pages/bulk-navigation-test-page.po.ts index baa076746..9abfd7880 100644 --- a/projects/scion/e2e-testing/src/workbench-client/page-object/test-pages/bulk-navigation-test-page.po.ts +++ b/projects/scion/e2e-testing/src/workbench-client/page-object/test-pages/bulk-navigation-test-page.po.ts @@ -15,7 +15,7 @@ import {MicrofrontendNavigator} from '../../microfrontend-navigator'; import {SciRouterOutletPO} from '../sci-router-outlet.po'; import {MicrofrontendViewPagePO} from '../../../workbench/page-object/workbench-view-page.po'; import {ViewPO} from '../../../view.po'; -import {ViewId} from '@scion/workbench-client'; +import {RouterPagePO} from '../router-page.po'; export class BulkNavigationTestPagePO implements MicrofrontendViewPagePO { @@ -23,9 +23,9 @@ export class BulkNavigationTestPagePO implements MicrofrontendViewPagePO { public readonly view: ViewPO; public readonly outlet: SciRouterOutletPO; - constructor(private _appPO: AppPO, viewId: ViewId) { - this.view = this._appPO.view({viewId}); - this.outlet = new SciRouterOutletPO(this._appPO, {name: viewId}); + constructor(private _appPO: AppPO, cssClass: string) { + this.view = this._appPO.view({cssClass}); + this.outlet = new SciRouterOutletPO(this._appPO, {cssClass}); this.locator = this.outlet.frameLocator.locator('app-bulk-navigation-test-page'); } @@ -61,20 +61,15 @@ export class BulkNavigationTestPagePO implements MicrofrontendViewPagePO { qualifier: {test: 'bulk-navigation'}, properties: { path: 'test-pages/bulk-navigation-test-page', - cssClass: 'e2e-test-bulk-navigation', title: 'Bulk Navigation Test', - pinToStartPage: true, }, }); - // Navigate to the view. - const startPage = await appPO.openNewViewTab(); - const viewId = await startPage.view.getViewId(); - await startPage.clickTestCapability('e2e-test-bulk-navigation', 'app1'); - - // Create the page object. - const view = appPO.view({cssClass: 'e2e-test-bulk-navigation', viewId: viewId}); - await view.waitUntilAttached(); - return new BulkNavigationTestPagePO(appPO, viewId); + const cssClass = `testee-${crypto.randomUUID()}`; + const routerPage = await microfrontendNavigator.openInNewTab(RouterPagePO, 'app1'); + await routerPage.enterQualifier({test: 'bulk-navigation'}); + await routerPage.enterCssClass(cssClass); + await routerPage.clickNavigate(); + return new BulkNavigationTestPagePO(appPO, cssClass); } } diff --git a/projects/scion/e2e-testing/src/workbench-client/page-object/test-pages/input-field-test-page.po.ts b/projects/scion/e2e-testing/src/workbench-client/page-object/test-pages/input-field-test-page.po.ts index 6677ed7a3..9ee149a5e 100644 --- a/projects/scion/e2e-testing/src/workbench-client/page-object/test-pages/input-field-test-page.po.ts +++ b/projects/scion/e2e-testing/src/workbench-client/page-object/test-pages/input-field-test-page.po.ts @@ -16,6 +16,7 @@ import {PopupPO} from '../../../popup.po'; import {PopupOpenerPagePO} from '../popup-opener-page.po'; import {SciRouterOutletPO} from '../sci-router-outlet.po'; import {MicrofrontendViewPagePO} from '../../../workbench/page-object/workbench-view-page.po'; +import {RouterPagePO} from '../router-page.po'; export class InputFieldTestPagePO implements MicrofrontendViewPagePO { @@ -55,22 +56,18 @@ export class InputFieldTestPagePO implements MicrofrontendViewPagePO { qualifier: {test: 'input-field'}, properties: { path: 'test-pages/input-field-test-page', - cssClass: 'test-input-field', title: 'Input Field Test Page', - pinToStartPage: true, }, }); - // Navigate to the view. - const startPage = await appPO.openNewViewTab(); - const viewId = await startPage.view.getViewId(); - await startPage.clickTestCapability('test-input-field', 'app1'); + const cssClass = `testee-${crypto.randomUUID()}`; + const routerPage = await microfrontendNavigator.openInNewTab(RouterPagePO, 'app1'); + await routerPage.enterQualifier({test: 'input-field'}); + await routerPage.enterCssClass(cssClass); + await routerPage.clickNavigate(); - // Create the page object. - const view = appPO.view({cssClass: 'test-input-field', viewId: viewId}); - await view.waitUntilAttached(); - - const outlet = new SciRouterOutletPO(appPO, {name: viewId}); + const view = appPO.view({cssClass}); + const outlet = new SciRouterOutletPO(appPO, {cssClass}); return new InputFieldTestPagePO(outlet, view); } @@ -80,21 +77,18 @@ export class InputFieldTestPagePO implements MicrofrontendViewPagePO { qualifier: {test: 'input-field'}, properties: { path: 'test-pages/input-field-test-page', - cssClass: 'test-input-field', }, }); - // Open the popup. + const cssClass = `testee-${crypto.randomUUID()}`; const popupOpenerPage = await microfrontendNavigator.openInNewTab(PopupOpenerPagePO, 'app1'); await popupOpenerPage.enterQualifier({test: 'input-field'}); await popupOpenerPage.enterCloseStrategy({closeOnFocusLost: popupOptions?.closeOnFocusLost}); + await popupOpenerPage.enterCssClass(cssClass); await popupOpenerPage.open(); - // Create the page object. - const popup = appPO.popup({cssClass: 'test-input-field'}); - await popup.locator.waitFor({state: 'attached'}); - - const outlet = new SciRouterOutletPO(appPO, {cssClass: ['e2e-popup', 'test-input-field']}); + const popup = appPO.popup({cssClass}); + const outlet = new SciRouterOutletPO(appPO, {cssClass}); return new InputFieldTestPagePO(outlet, popup); } } diff --git a/projects/scion/e2e-testing/src/workbench-client/page-object/test-pages/legacy-message-box-opener-page.po.ts b/projects/scion/e2e-testing/src/workbench-client/page-object/test-pages/legacy-message-box-opener-page.po.ts index dc0d27bec..221c0b8ce 100644 --- a/projects/scion/e2e-testing/src/workbench-client/page-object/test-pages/legacy-message-box-opener-page.po.ts +++ b/projects/scion/e2e-testing/src/workbench-client/page-object/test-pages/legacy-message-box-opener-page.po.ts @@ -60,23 +60,20 @@ export class LegacyMessageBoxOpenerPagePO implements MicrofrontendViewPagePO { } public static async openInNewTab(appPO: AppPO, microfrontendNavigator: MicrofrontendNavigator): Promise { - // Register view capability. await microfrontendNavigator.registerCapability('app1', { type: 'view', qualifier: {test: 'legacy-message-box-opener'}, properties: { path: 'test-pages/legacy-message-box-opener-page', - cssClass: 'legacy-message-box-opener', title: 'Legacy Message Box Opener', - pinToStartPage: true, }, }); - // Navigate to view. + const cssClass = `testee-${crypto.randomUUID()}`; const routerPage = await microfrontendNavigator.openInNewTab(RouterPagePO, 'app1'); await routerPage.enterQualifier({test: 'legacy-message-box-opener'}); - await routerPage.enterCssClass('legacy-message-box-opener'); + await routerPage.enterCssClass(cssClass); await routerPage.clickNavigate(); - return new LegacyMessageBoxOpenerPagePO(appPO, {cssClass: 'legacy-message-box-opener'}); + return new LegacyMessageBoxOpenerPagePO(appPO, {cssClass}); } } diff --git a/projects/scion/e2e-testing/src/workbench-client/page-object/test-pages/workbench-theme-test-page.po.ts b/projects/scion/e2e-testing/src/workbench-client/page-object/test-pages/workbench-theme-test-page.po.ts index c14b5c492..a17813195 100644 --- a/projects/scion/e2e-testing/src/workbench-client/page-object/test-pages/workbench-theme-test-page.po.ts +++ b/projects/scion/e2e-testing/src/workbench-client/page-object/test-pages/workbench-theme-test-page.po.ts @@ -14,7 +14,7 @@ import {MicrofrontendNavigator} from '../../microfrontend-navigator'; import {SciRouterOutletPO} from '../sci-router-outlet.po'; import {MicrofrontendViewPagePO} from '../../../workbench/page-object/workbench-view-page.po'; import {ViewPO} from '../../../view.po'; -import {ViewId} from '@scion/workbench-client'; +import {RouterPagePO} from '../router-page.po'; export class WorkbenchThemeTestPagePO implements MicrofrontendViewPagePO { @@ -25,9 +25,9 @@ export class WorkbenchThemeTestPagePO implements MicrofrontendViewPagePO { public readonly theme: Locator; public readonly colorScheme: Locator; - constructor(appPO: AppPO, viewId: ViewId) { - this.view = appPO.view({viewId}); - this.outlet = new SciRouterOutletPO(appPO, {name: viewId}); + constructor(appPO: AppPO, cssClass: string) { + this.view = appPO.view({cssClass}); + this.outlet = new SciRouterOutletPO(appPO, {cssClass}); this.locator = this.outlet.frameLocator.locator('app-workbench-theme-test-page'); this.theme = this.locator.locator('span.e2e-theme'); @@ -40,20 +40,15 @@ export class WorkbenchThemeTestPagePO implements MicrofrontendViewPagePO { qualifier: {test: 'workbench-theme'}, properties: { path: 'test-pages/workbench-theme-test-page', - cssClass: 'e2e-test-workbench-theme', title: 'Workbench Theme Test Page', - pinToStartPage: true, }, }); - // Navigate to the view. - const startPage = await appPO.openNewViewTab(); - const viewId = await startPage.view.getViewId(); - await startPage.clickTestCapability('e2e-test-workbench-theme', 'app1'); - - // Create the page object. - const view = appPO.view({cssClass: 'e2e-test-workbench-theme', viewId: viewId}); - await view.waitUntilAttached(); - return new WorkbenchThemeTestPagePO(appPO, viewId); + const cssClass = `testee-${crypto.randomUUID()}`; + const routerPage = await microfrontendNavigator.openInNewTab(RouterPagePO, 'app1'); + await routerPage.enterQualifier({test: 'workbench-theme'}); + await routerPage.enterCssClass(cssClass); + await routerPage.clickNavigate(); + return new WorkbenchThemeTestPagePO(appPO, cssClass); } } diff --git a/projects/scion/e2e-testing/src/workbench-client/page-object/unregister-workbench-capability-page.po.ts b/projects/scion/e2e-testing/src/workbench-client/page-object/unregister-workbench-capability-page.po.ts deleted file mode 100644 index 8caa7f6c7..000000000 --- a/projects/scion/e2e-testing/src/workbench-client/page-object/unregister-workbench-capability-page.po.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (c) 2018-2022 Swiss Federal Railways - * - * This program and the accompanying materials are made - * available under the terms from the Eclipse Public License 2.0 - * which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - */ - -import {AppPO} from '../../app.po'; -import {Locator} from '@playwright/test'; -import {rejectWhenAttached, waitUntilAttached} from '../../helper/testing.util'; -import {SciRouterOutletPO} from './sci-router-outlet.po'; -import {MicrofrontendViewPagePO} from '../../workbench/page-object/workbench-view-page.po'; -import {ViewPO} from '../../view.po'; -import {ViewId} from '@scion/workbench-client'; - -/** - * Page object to interact with {@link UnregisterWorkbenchCapabilityPageComponent}. - */ -export class UnregisterWorkbenchCapabilityPagePO implements MicrofrontendViewPagePO { - - public readonly locator: Locator; - public readonly view: ViewPO; - public readonly outlet: SciRouterOutletPO; - - constructor(appPO: AppPO, locateBy: {viewId?: ViewId; cssClass?: string}) { - this.view = appPO.view({viewId: locateBy.viewId, cssClass: locateBy.cssClass}); - this.outlet = new SciRouterOutletPO(appPO, {name: locateBy.viewId, cssClass: locateBy.cssClass}); - this.locator = this.outlet.frameLocator.locator('app-unregister-workbench-capability-page'); - } - - /** - * Unregisters the given workbench capability. - * - * This method exists as a convenience method to not have to enter all fields separately. - * - * Returns a Promise that resolves upon successful unregistration, or that rejects on error. - */ - public async unregisterCapability(id: string): Promise { - await this.enterId(id); - await this.clickUnregister(); - - // Evaluate the response: resolve the promise on success, or reject it on error. - const responseLocator = this.locator.locator('output.e2e-unregistered'); - const errorLocator = this.locator.locator('output.e2e-unregister-error'); - return Promise.race([ - waitUntilAttached(responseLocator), - rejectWhenAttached(errorLocator), - ]); - } - - public async enterId(id: string): Promise { - await this.locator.locator('input.e2e-id').fill(id); - } - - public async clickUnregister(): Promise { - await this.locator.locator('button.e2e-unregister').click(); - } -} - diff --git a/projects/scion/e2e-testing/src/workbench-client/popup-size.e2e-spec.ts b/projects/scion/e2e-testing/src/workbench-client/popup-size.e2e-spec.ts index 01c5ff19d..5ee148be8 100644 --- a/projects/scion/e2e-testing/src/workbench-client/popup-size.e2e-spec.ts +++ b/projects/scion/e2e-testing/src/workbench-client/popup-size.e2e-spec.ts @@ -58,42 +58,6 @@ test.describe('Workbench Popup', () => { height: 450, })); }); - - /** - * In this test, we do not open the popup from within a microfrontend because opening the popup from within a microfrontend causes that - * microfrontend to lose focus, which would trigger a change detection cycle in the host, causing the popup to be displayed at the correct size. - * - * This test verifies that the popup is displayed at the correct size even without an "additional" change detection cycle, i.e., is opened - * inside the Angular zone. - */ - test('should size the popup as configured in the popup capability (insideAngularZone)', async ({appPO, microfrontendNavigator}) => { - await appPO.navigateTo({microfrontendSupport: true}); - - await microfrontendNavigator.registerCapability('app1', { - type: 'popup', - qualifier: {test: 'popup'}, - properties: { - path: 'test-popup', - cssClass: 'testee', - pinToStartPage: true, - size: { - width: '350px', - height: '450px', - }, - }, - }); - - // open the popup directly from the start page - const startPage = await appPO.openNewViewTab(); - await startPage.clickTestCapability('testee', 'app1'); - - const popup = appPO.popup({cssClass: 'testee'}); - - await expect.poll(() => popup.getBoundingBox({box: 'content-box'})).toEqual(expect.objectContaining({ - width: 350, - height: 450, - })); - }); }); test('should adapt its size to the microfrontend', async ({appPO, microfrontendNavigator}) => { diff --git a/projects/scion/e2e-testing/src/workbench-client/popup-splash.e2e-spec.ts b/projects/scion/e2e-testing/src/workbench-client/popup-splash.e2e-spec.ts index 9fc804a86..2ce664461 100644 --- a/projects/scion/e2e-testing/src/workbench-client/popup-splash.e2e-spec.ts +++ b/projects/scion/e2e-testing/src/workbench-client/popup-splash.e2e-spec.ts @@ -10,9 +10,9 @@ import {test} from '../fixtures'; import {expect} from '@playwright/test'; -import {MessagingPagePO} from './page-object/messaging-page.po'; import {PopupOpenerPagePO} from './page-object/popup-opener-page.po'; import {PopupPagePO} from './page-object/popup-page.po'; +import {MicrofrontendPlatformPagePO} from '../workbench/page-object/microfrontend-platform-page/microfrontend-platform-page.po'; test.describe('Workbench Popup', () => { @@ -42,9 +42,9 @@ test.describe('Workbench Popup', () => { await expect(popupPage.outlet.splash).toBeVisible(); // Publish message to dispose splash. - const messagingPage = await microfrontendNavigator.openInNewTab(MessagingPagePO, 'app1'); - await messagingPage.publishMessage(`signal-ready/${popupCapability.metadata!.id}`); - await messagingPage.view.tab.close(); + const microfrontendPlatformPage = await microfrontendNavigator.openInNewTab(MicrofrontendPlatformPagePO); + await microfrontendPlatformPage.publishMessage(`signal-ready/${popupCapability.metadata!.id}`); + await microfrontendPlatformPage.view.tab.close(); // Expect splash not to display. await expect(popupPage.outlet.splash).not.toBeVisible(); diff --git a/projects/scion/e2e-testing/src/workbench-client/view-capability.e2e-spec.ts b/projects/scion/e2e-testing/src/workbench-client/view-capability.e2e-spec.ts index e0b0f90c9..1fe2c5118 100644 --- a/projects/scion/e2e-testing/src/workbench-client/view-capability.e2e-spec.ts +++ b/projects/scion/e2e-testing/src/workbench-client/view-capability.e2e-spec.ts @@ -9,8 +9,8 @@ */ import {test} from '../fixtures'; -import {WorkbenchViewCapability} from './page-object/register-workbench-capability-page.po'; import {expect} from '@playwright/test'; +import {WorkbenchViewCapability} from '../workbench/page-object/microfrontend-platform-page/register-capability-page.po'; test.describe('Workbench View Capability', () => { diff --git a/projects/scion/e2e-testing/src/workbench-client/view-splash.e2e-spec.ts b/projects/scion/e2e-testing/src/workbench-client/view-splash.e2e-spec.ts index 14285dafa..279fa4ba0 100644 --- a/projects/scion/e2e-testing/src/workbench-client/view-splash.e2e-spec.ts +++ b/projects/scion/e2e-testing/src/workbench-client/view-splash.e2e-spec.ts @@ -12,7 +12,7 @@ import {test} from '../fixtures'; import {RouterPagePO} from './page-object/router-page.po'; import {expect} from '@playwright/test'; import {ViewPagePO} from './page-object/view-page.po'; -import {MessagingPagePO} from './page-object/messaging-page.po'; +import {MicrofrontendPlatformPagePO} from '../workbench/page-object/microfrontend-platform-page/microfrontend-platform-page.po'; test.describe('Workbench View', () => { @@ -46,9 +46,9 @@ test.describe('Workbench View', () => { await expect(testeeViewPage.outlet.splash).toBeVisible(); // Publish message to dispose splash. - const messagingPage = await microfrontendNavigator.openInNewTab(MessagingPagePO, 'app1'); - await messagingPage.publishMessage('signal-ready/view.101'); - await messagingPage.view.tab.close(); + const microfrontendPlatformPage = await microfrontendNavigator.openInNewTab(MicrofrontendPlatformPagePO); + await microfrontendPlatformPage.publishMessage('signal-ready/view.101'); + await microfrontendPlatformPage.view.tab.close(); // Expect splash not to display. await expect(testeeViewPage.outlet.splash).not.toBeVisible(); diff --git a/projects/scion/e2e-testing/src/workbench/page-object/microfrontend-platform-page/application.ts b/projects/scion/e2e-testing/src/workbench/page-object/microfrontend-platform-page/application.ts new file mode 100644 index 000000000..09157e2e6 --- /dev/null +++ b/projects/scion/e2e-testing/src/workbench/page-object/microfrontend-platform-page/application.ts @@ -0,0 +1,11 @@ +/* + * Copyright (c) 2018-2024 Swiss Federal Railways + * + * This program and the accompanying materials are made + * available under the terms from the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ + +export type Application = 'workbench-host-app' | 'workbench-client-testing-app1' | 'workbench-client-testing-app2'; diff --git a/projects/scion/e2e-testing/src/workbench/page-object/microfrontend-platform-page/microfrontend-platform-page.po.ts b/projects/scion/e2e-testing/src/workbench/page-object/microfrontend-platform-page/microfrontend-platform-page.po.ts new file mode 100644 index 000000000..7690698aa --- /dev/null +++ b/projects/scion/e2e-testing/src/workbench/page-object/microfrontend-platform-page/microfrontend-platform-page.po.ts @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2018-2024 Swiss Federal Railways + * + * This program and the accompanying materials are made + * available under the terms from the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import {AppPO} from '../../../app.po'; +import {ViewPO} from '../../../view.po'; +import {Locator} from '@playwright/test'; +import {ViewId} from '@scion/workbench'; +import {SciTabbarPO} from '../../../@scion/components.internal/tabbar.po'; +import {WorkbenchViewPagePO} from '../workbench-view-page.po'; +import {RegisterCapabilityPagePO} from './register-capability-page.po'; +import {Capability, Intention} from '@scion/microfrontend-platform'; +import {RegisterIntentionPagePO} from './register-intention-page.po'; +import {UnregisterCapabilityPagePO} from './unregister-capability-page.po'; +import {PublishMessagePagePO} from './publish-message-page.po'; +import {Application} from './application'; + +/** + * Page object to interact with {@link MicrofrontendPlatformPageComponent}. + */ +export class MicrofrontendPlatformPagePO implements WorkbenchViewPagePO { + + public readonly locator: Locator; + public readonly view: ViewPO; + + private readonly _tabbar: SciTabbarPO; + + constructor(appPO: AppPO, locateBy: {viewId?: ViewId; cssClass?: string}) { + this.view = appPO.view({viewId: locateBy?.viewId, cssClass: locateBy?.cssClass}); + this.locator = this.view.locator.locator('app-microfrontend-platform-page'); + this._tabbar = new SciTabbarPO(this.locator.locator('sci-tabbar')); + } + + /** + * Registers the given capability. + * + * Returns a Promise that resolves to the registered capability upon successful registration, or that rejects on registration error. + */ + public async registerCapability(application: Application, capability: T): Promise { + await this.view.tab.click(); + await this._tabbar.selectTab('e2e-register-capability'); + + const registerCapabilityPage = new RegisterCapabilityPagePO(this.locator.locator('app-register-capability-page')); + return registerCapabilityPage.registerCapability(application, capability); + } + + /** + * Registers the given intention. + * + * Returns a Promise that resolves to the registered capability upon successful registration, or that rejects on registration error. + */ + public async registerIntention(application: Application, intention: Intention): Promise { + await this.view.tab.click(); + await this._tabbar.selectTab('e2e-register-intention'); + + const registerIntention = new RegisterIntentionPagePO(this.locator.locator('app-register-intention-page')); + return registerIntention.registerIntention(application, intention); + } + + /** + * Unregisters the given capability. + * + * Returns a Promise that resolves upon successful unregistration, or that rejects on error. + */ + public async unregisterCapability(application: Application, id: string): Promise { + await this.view.tab.click(); + await this._tabbar.selectTab('e2e-unregister-capability'); + + const unregisterCapabilityPage = new UnregisterCapabilityPagePO(this.locator.locator('app-unregister-capability-page')); + return unregisterCapabilityPage.unregisterCapability(application, id); + } + + /** + * Publishes a message to the given topic. + */ + public async publishMessage(topic: string): Promise { + await this.view.tab.click(); + await this._tabbar.selectTab('e2e-publish-message'); + + const publishMessagePage = new PublishMessagePagePO(this.locator.locator('app-publish-message-page')); + return publishMessagePage.publishMessage(topic); + } +} diff --git a/projects/scion/e2e-testing/src/workbench/page-object/microfrontend-platform-page/publish-message-page.po.ts b/projects/scion/e2e-testing/src/workbench/page-object/microfrontend-platform-page/publish-message-page.po.ts new file mode 100644 index 000000000..469f7ea6c --- /dev/null +++ b/projects/scion/e2e-testing/src/workbench/page-object/microfrontend-platform-page/publish-message-page.po.ts @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2018-2024 Swiss Federal Railways + * + * This program and the accompanying materials are made + * available under the terms from the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import {Locator} from '@playwright/test'; +import {rejectWhenAttached, waitUntilAttached} from '../../../helper/testing.util'; + +/** + * Page object to interact with {@link PublishMessagePageComponent}. + */ +export class PublishMessagePagePO { + + constructor(public locator: Locator) { + } + + /** + * Publishes a message to the given topic. + */ + public async publishMessage(topic: string): Promise { + await this.locator.locator('input.e2e-topic').fill(topic); + await this.locator.locator('button.e2e-publish').click(); + + // Evaluate the response: resolve the promise on success, or reject it on error. + const successLocator = this.locator.locator('output.e2e-publish-success'); + const errorLocator = this.locator.locator('output.e2e-publish-error'); + await Promise.race([ + waitUntilAttached(successLocator), + rejectWhenAttached(errorLocator), + ]); + } +} diff --git a/projects/scion/e2e-testing/src/workbench-client/page-object/register-workbench-capability-page.po.ts b/projects/scion/e2e-testing/src/workbench/page-object/microfrontend-platform-page/register-capability-page.po.ts similarity index 76% rename from projects/scion/e2e-testing/src/workbench-client/page-object/register-workbench-capability-page.po.ts rename to projects/scion/e2e-testing/src/workbench/page-object/microfrontend-platform-page/register-capability-page.po.ts index 346c2939b..5203da340 100644 --- a/projects/scion/e2e-testing/src/workbench-client/page-object/register-workbench-capability-page.po.ts +++ b/projects/scion/e2e-testing/src/workbench/page-object/microfrontend-platform-page/register-capability-page.po.ts @@ -8,16 +8,13 @@ * SPDX-License-Identifier: EPL-2.0 */ -import {coerceArray, rejectWhenAttached, waitUntilAttached} from '../../helper/testing.util'; -import {AppPO} from '../../app.po'; -import {SciKeyValueFieldPO} from '../../@scion/components.internal/key-value-field.po'; -import {SciCheckboxPO} from '../../@scion/components.internal/checkbox.po'; +import {coerceArray, rejectWhenAttached, waitUntilAttached} from '../../../helper/testing.util'; +import {SciKeyValueFieldPO} from '../../../@scion/components.internal/key-value-field.po'; +import {SciCheckboxPO} from '../../../@scion/components.internal/checkbox.po'; import {Locator} from '@playwright/test'; -import {ViewId, WorkbenchDialogCapability as _WorkbenchDialogCapability, WorkbenchMessageBoxCapability as _WorkbenchMessageBoxCapability, WorkbenchPopupCapability as _WorkbenchPopupCapability, WorkbenchViewCapability as _WorkbenchViewCapability} from '@scion/workbench-client'; +import {WorkbenchDialogCapability as _WorkbenchDialogCapability, WorkbenchMessageBoxCapability as _WorkbenchMessageBoxCapability, WorkbenchPopupCapability as _WorkbenchPopupCapability, WorkbenchViewCapability as _WorkbenchViewCapability} from '@scion/workbench-client'; import {Capability} from '@scion/microfrontend-platform'; -import {SciRouterOutletPO} from './sci-router-outlet.po'; -import {MicrofrontendViewPagePO} from '../../workbench/page-object/workbench-view-page.po'; -import {ViewPO} from '../../view.po'; +import {Application} from './application'; /** * Playwright's test runner fails to compile when importing runtime types from `@scion/workbench` or `@scion/microfrontend-platform`, because @@ -26,37 +23,27 @@ import {ViewPO} from '../../view.po'; * Unlike classes or enums, interfaces can be referenced because they do not exist at runtime. * For that reason, we re-declare workbench capability interfaces and replace their `type` property (enum) with a string literal. */ -export type WorkbenchViewCapability = Omit<_WorkbenchViewCapability, 'type'> & {type: 'view'; properties: {pinToStartPage?: boolean; path: string | '' | ''}}; -export type WorkbenchPopupCapability = Omit<_WorkbenchPopupCapability, 'type'> & {type: 'popup'; properties: {pinToStartPage?: boolean; path: string | '' | ''}}; +export type WorkbenchViewCapability = Omit<_WorkbenchViewCapability, 'type'> & {type: 'view'; properties: {path: string | '' | ''}}; +export type WorkbenchPopupCapability = Omit<_WorkbenchPopupCapability, 'type'> & {type: 'popup'; properties: {path: string | '' | ''}}; export type WorkbenchDialogCapability = Omit<_WorkbenchDialogCapability, 'type'> & {type: 'dialog'; properties: {path: string | '' | ''}}; export type WorkbenchMessageBoxCapability = Omit<_WorkbenchMessageBoxCapability, 'type'> & {type: 'messagebox'; properties: {path: string | '' | ''}}; /** - * Page object to interact with {@link RegisterWorkbenchCapabilityPageComponent}. + * Page object to interact with {@link RegisterCapabilityPageComponent}. */ -export class RegisterWorkbenchCapabilityPagePO implements MicrofrontendViewPagePO { +export class RegisterCapabilityPagePO { - public readonly locator: Locator; - public readonly outlet: SciRouterOutletPO; - public readonly view: ViewPO; - - constructor(appPO: AppPO, locateBy: {viewId?: ViewId; cssClass?: string}) { - this.view = appPO.view({viewId: locateBy.viewId, cssClass: locateBy.cssClass}); - this.outlet = new SciRouterOutletPO(appPO, {name: locateBy.viewId, cssClass: locateBy.cssClass}); - this.locator = this.outlet.frameLocator.locator('app-register-workbench-capability-page'); + constructor(public locator: Locator) { } /** - * Registers the given workbench capability. - * - * This method exists as a convenience method to not have to enter all fields separately. + * Registers the given capability. * * Returns a Promise that resolves to the registered capability upon successful registration, or that rejects on registration error. */ - public async registerCapability(capability: T): Promise { - if (capability.type !== undefined) { - await this.locator.locator('select.e2e-type').selectOption(capability.type); - } + public async registerCapability(application: Application, capability: T): Promise { + await this.locator.locator('select.e2e-application').selectOption(application); + await this.locator.locator('input.e2e-type').fill(capability.type); if (capability.qualifier !== undefined) { const keyValueField = new SciKeyValueFieldPO(this.locator.locator('sci-key-value-field.e2e-qualifier')); await keyValueField.clear(); @@ -77,10 +64,10 @@ export class RegisterWorkbenchCapabilityPagePO implements MicrofrontendViewPageP if (capability.private !== undefined) { await new SciCheckboxPO(this.locator.locator('sci-checkbox.e2e-private')).toggle(capability.private); } - if (capability.properties.path !== undefined) { + if (capability.properties?.['path'] !== undefined) { await this.locator.locator('input.e2e-path').fill(capability.properties.path); } - if (capability.properties.cssClass !== undefined) { + if (capability.properties?.['cssClass'] !== undefined) { await this.locator.locator('input.e2e-class').fill(coerceArray(capability.properties.cssClass).join(' ')); } if (capability.type === 'view') { @@ -96,7 +83,7 @@ export class RegisterWorkbenchCapabilityPagePO implements MicrofrontendViewPageP await this.enterMessageBoxCapabilityProperties(capability as WorkbenchMessageBoxCapability); } - await this.clickRegister(); + await this.locator.locator('button.e2e-register').click(); // Evaluate the response: resolve the promise on success, or reject it on error. const responseLocator = this.locator.locator('output.e2e-register-response'); @@ -122,9 +109,6 @@ export class RegisterWorkbenchCapabilityPagePO implements MicrofrontendViewPageP if (capability.properties.showSplash !== undefined) { await new SciCheckboxPO(this.locator.locator('sci-checkbox.e2e-show-splash')).toggle(capability.properties.showSplash); } - if (capability.properties.pinToStartPage !== undefined) { - await new SciCheckboxPO(this.locator.locator('sci-checkbox.e2e-pin-to-startpage')).toggle(capability.properties.pinToStartPage); - } } private async enterPopupCapabilityProperties(capability: WorkbenchPopupCapability): Promise { @@ -151,9 +135,6 @@ export class RegisterWorkbenchCapabilityPagePO implements MicrofrontendViewPageP if (capability.properties.showSplash !== undefined) { await new SciCheckboxPO(this.locator.locator('sci-checkbox.e2e-show-splash')).toggle(capability.properties.showSplash); } - if (capability.properties.pinToStartPage !== undefined) { - await new SciCheckboxPO(this.locator.locator('sci-checkbox.e2e-pin-to-startpage')).toggle(capability.properties.pinToStartPage); - } } private async enterDialogCapabilityProperties(capability: WorkbenchDialogCapability): Promise { @@ -219,8 +200,4 @@ export class RegisterWorkbenchCapabilityPagePO implements MicrofrontendViewPageP await new SciCheckboxPO(this.locator.locator('sci-checkbox.e2e-show-splash')).toggle(capability.properties.showSplash); } } - - private async clickRegister(): Promise { - await this.locator.locator('button.e2e-register').click(); - } } diff --git a/projects/scion/e2e-testing/src/workbench/page-object/microfrontend-platform-page/register-intention-page.po.ts b/projects/scion/e2e-testing/src/workbench/page-object/microfrontend-platform-page/register-intention-page.po.ts new file mode 100644 index 000000000..0848b568d --- /dev/null +++ b/projects/scion/e2e-testing/src/workbench/page-object/microfrontend-platform-page/register-intention-page.po.ts @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2018-2024 Swiss Federal Railways + * + * This program and the accompanying materials are made + * available under the terms from the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import {Intention} from '@scion/microfrontend-platform'; +import {SciKeyValueFieldPO} from '../../../@scion/components.internal/key-value-field.po'; +import {Locator} from '@playwright/test'; +import {rejectWhenAttached, waitUntilAttached} from '../../../helper/testing.util'; +import {Application} from './application'; + +/** + * Page object to interact with {@link RegisterIntentionPageComponent}. + */ +export class RegisterIntentionPagePO { + + constructor(public locator: Locator) { + } + + /** + * Registers the given intention. + * + * Returns a Promise that resolves to the intention ID upon successful registration, or that rejects on registration error. + */ + public async registerIntention(application: Application, intention: Intention): Promise { + await this.locator.locator('select.e2e-application').selectOption(application); + await this.locator.locator('input.e2e-type').fill(intention.type); + if (intention.qualifier !== undefined) { + const keyValueField = new SciKeyValueFieldPO(this.locator.locator('sci-key-value-field.e2e-qualifier')); + await keyValueField.clear(); + await keyValueField.addEntries(intention.qualifier); + } + + // Register intention. + await this.locator.locator('button.e2e-register').click(); + + // Evaluate the response: resolve the promise on success, or reject it on error. + const responseLocator = this.locator.locator('output.e2e-register-response'); + const errorLocator = this.locator.locator('output.e2e-register-error'); + await Promise.race([ + waitUntilAttached(responseLocator), + rejectWhenAttached(errorLocator), + ]); + return responseLocator.locator('span.e2e-intention-id').innerText(); + } +} diff --git a/projects/scion/e2e-testing/src/workbench/page-object/microfrontend-platform-page/unregister-capability-page.po.ts b/projects/scion/e2e-testing/src/workbench/page-object/microfrontend-platform-page/unregister-capability-page.po.ts new file mode 100644 index 000000000..9f3b3beff --- /dev/null +++ b/projects/scion/e2e-testing/src/workbench/page-object/microfrontend-platform-page/unregister-capability-page.po.ts @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2018-2024 Swiss Federal Railways + * + * This program and the accompanying materials are made + * available under the terms from the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import {Locator} from '@playwright/test'; +import {rejectWhenAttached, waitUntilAttached} from '../../../helper/testing.util'; +import {Application} from './application'; + +/** + * Page object to interact with {@link UnregisterCapabilityPageComponent}. + */ +export class UnregisterCapabilityPagePO { + + constructor(public locator: Locator) { + } + + /** + * Unregisters the given capability. + * + * Returns a Promise that resolves upon successful unregistration, or that rejects on error. + */ + public async unregisterCapability(application: Application, id: string): Promise { + await this.locator.locator('select.e2e-application').selectOption(application); + await this.locator.locator('input.e2e-id').fill(id); + await this.locator.locator('button.e2e-unregister').click(); + + // Evaluate the response: resolve the promise on success, or reject it on error. + const responseLocator = this.locator.locator('output.e2e-unregistered'); + const errorLocator = this.locator.locator('output.e2e-unregister-error'); + return Promise.race([ + waitUntilAttached(responseLocator), + rejectWhenAttached(errorLocator), + ]); + } +} diff --git a/projects/scion/workbench/src/lib/microfrontend-platform/common/microfrontend.util.ts b/projects/scion/workbench/src/lib/microfrontend-platform/common/microfrontend.util.ts index cd3352a8c..3c1eb735e 100644 --- a/projects/scion/workbench/src/lib/microfrontend-platform/common/microfrontend.util.ts +++ b/projects/scion/workbench/src/lib/microfrontend-platform/common/microfrontend.util.ts @@ -8,12 +8,14 @@ * SPDX-License-Identifier: EPL-2.0 */ -import {SciRouterOutletElement} from '@scion/microfrontend-platform'; +import {APP_IDENTITY, Capability, SciRouterOutletElement} from '@scion/microfrontend-platform'; import {inject} from '@angular/core'; import {ɵTHEME_CONTEXT_KEY} from '@scion/workbench-client'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; import {WorkbenchService} from '../../workbench.service'; import {WorkbenchTheme} from '../../workbench.model'; +import {Maps} from '@scion/toolkit/util'; +import {Beans} from '@scion/toolkit/bean-manager'; /** * Provides functions related to workbench themes. @@ -44,12 +46,20 @@ export const Microfrontends = { * Named parameters begin with a colon (`:`). */ substituteNamedParameters, + /** + * Tests if given capability is provided by the host application. + */ + isHostProvider: (capability: Capability): boolean => { + return capability.metadata!.appSymbolicName === Beans.get(APP_IDENTITY); + }, } as const; -function substituteNamedParameters(value: string, params?: Map): string; -function substituteNamedParameters(value: string | null, params?: Map): string | null; -function substituteNamedParameters(value: string | undefined, params?: Map): string | undefined; -function substituteNamedParameters(value: string | null | undefined, params?: Map): string | null | undefined { +function substituteNamedParameters(value: string, params?: Map | {[key: string]: unknown}): string; +function substituteNamedParameters(value: string | null, params?: Map | {[key: string]: unknown}): string | null; +function substituteNamedParameters(value: string | undefined, params?: Map | {[key: string]: unknown}): string | undefined; +function substituteNamedParameters(value: string | null | undefined, params?: Map | {[key: string]: unknown}): string | null | undefined { + params = Maps.coerce(params); + if (!value || !params?.size) { return value; } diff --git a/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-dialog/microfrontend-dialog-capability-validator.interceptor.ts b/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-dialog/microfrontend-dialog-capability-validator.interceptor.ts index 663aa86cf..316764913 100644 --- a/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-dialog/microfrontend-dialog-capability-validator.interceptor.ts +++ b/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-dialog/microfrontend-dialog-capability-validator.interceptor.ts @@ -8,10 +8,10 @@ * SPDX-License-Identifier: EPL-2.0 */ -import {APP_IDENTITY, Capability, CapabilityInterceptor} from '@scion/microfrontend-platform'; +import {Capability, CapabilityInterceptor} from '@scion/microfrontend-platform'; import {Injectable} from '@angular/core'; import {WorkbenchCapabilities, WorkbenchDialogCapability} from '@scion/workbench-client'; -import {Beans} from '@scion/toolkit/bean-manager'; +import {Microfrontends} from '../common/microfrontend.util'; /** * Asserts dialog capabilities to have required properties. @@ -43,8 +43,7 @@ export class MicrofrontendDialogCapabilityValidator implements CapabilityInterce } private assertSize(capability: WorkbenchDialogCapability): void { - const isHostProvider = capability.metadata!.appSymbolicName === Beans.get(APP_IDENTITY); - if (isHostProvider) { + if (Microfrontends.isHostProvider(capability)) { return; } diff --git a/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-dialog/microfrontend-dialog-intent-handler.interceptor.ts b/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-dialog/microfrontend-dialog-intent-handler.interceptor.ts index 5632ff3d3..021c42362 100644 --- a/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-dialog/microfrontend-dialog-intent-handler.interceptor.ts +++ b/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-dialog/microfrontend-dialog-intent-handler.interceptor.ts @@ -9,7 +9,7 @@ */ import {Injectable} from '@angular/core'; -import {APP_IDENTITY, Handler, IntentInterceptor, IntentMessage, MessageClient, MessageHeaders, ResponseStatusCodes} from '@scion/microfrontend-platform'; +import {Handler, IntentInterceptor, IntentMessage, MessageClient, MessageHeaders, ResponseStatusCodes} from '@scion/microfrontend-platform'; import {WorkbenchCapabilities, WorkbenchDialogCapability, WorkbenchDialogOptions} from '@scion/workbench-client'; import {Logger, LoggerNames} from '../../logging'; import {Beans} from '@scion/toolkit/bean-manager'; @@ -18,6 +18,7 @@ import {Arrays} from '@scion/toolkit/util'; import {WorkbenchDialogService} from '../../dialog/workbench-dialog.service'; import {MicrofrontendDialogComponent} from './microfrontend-dialog.component'; import {MicrofrontendHostDialogComponent} from '../microfrontend-host-dialog/microfrontend-host-dialog.component'; +import {Microfrontends} from '../common/microfrontend.util'; /** * Handles dialog intents, instructing the workbench to open a dialog with the microfrontend declared on the resolved capability. @@ -70,10 +71,9 @@ export class MicrofrontendDialogIntentHandler implements IntentInterceptor { const options = message.body ?? {}; const capability = message.capability as WorkbenchDialogCapability; const params = message.intent.params ?? new Map(); - const isHostProvider = capability.metadata!.appSymbolicName === Beans.get(APP_IDENTITY); - this._logger.debug(() => 'Handling microfrontend dialog intent', LoggerNames.MICROFRONTEND, options); - return this._dialogService.open(isHostProvider ? MicrofrontendHostDialogComponent : MicrofrontendDialogComponent, { + this._logger.debug(() => 'Handling microfrontend dialog intent', LoggerNames.MICROFRONTEND, options); + return this._dialogService.open(Microfrontends.isHostProvider(capability) ? MicrofrontendHostDialogComponent : MicrofrontendDialogComponent, { inputs: {capability, params}, modality: options.modality, context: options.context, diff --git a/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-message-box/microfrontend-message-box-intent-handler.interceptor.ts b/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-message-box/microfrontend-message-box-intent-handler.interceptor.ts index 640a6da3f..75c52b70c 100644 --- a/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-message-box/microfrontend-message-box-intent-handler.interceptor.ts +++ b/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-message-box/microfrontend-message-box-intent-handler.interceptor.ts @@ -9,7 +9,7 @@ */ import {Injectable} from '@angular/core'; -import {APP_IDENTITY, Handler, IntentInterceptor, IntentMessage, MessageClient, MessageHeaders, ResponseStatusCodes} from '@scion/microfrontend-platform'; +import {Handler, IntentInterceptor, IntentMessage, MessageClient, MessageHeaders, ResponseStatusCodes} from '@scion/microfrontend-platform'; import {WorkbenchCapabilities, WorkbenchMessageBoxCapability, WorkbenchMessageBoxOptions} from '@scion/workbench-client'; import {Logger, LoggerNames} from '../../logging'; import {Beans} from '@scion/toolkit/bean-manager'; @@ -18,6 +18,7 @@ import {WorkbenchMessageBoxService} from '../../message-box/workbench-message-bo import {Arrays} from '@scion/toolkit/util'; import {MicrofrontendHostMessageBoxComponent} from '../microfrontend-host-message-box/microfrontend-host-message-box.component'; import {MicrofrontendMessageBoxComponent} from './microfrontend-message-box.component'; +import {Microfrontends} from '../common/microfrontend.util'; /** * Handles messagebox intents, instructing the workbench to open a message box with the microfrontend declared on the resolved capability. @@ -71,10 +72,9 @@ export class MicrofrontendMessageBoxIntentHandler implements IntentInterceptor { const options = message.body ?? {}; const capability = message.capability as WorkbenchMessageBoxCapability; const params = message.intent.params ?? new Map(); - const isHostProvider = capability.metadata!.appSymbolicName === Beans.get(APP_IDENTITY); this._logger.debug(() => 'Handling microfrontend messagebox intent', LoggerNames.MICROFRONTEND, options); - return this._messageBoxService.open(isHostProvider ? MicrofrontendHostMessageBoxComponent : MicrofrontendMessageBoxComponent, { + return this._messageBoxService.open(Microfrontends.isHostProvider(capability) ? MicrofrontendHostMessageBoxComponent : MicrofrontendMessageBoxComponent, { inputs: {capability, params}, title: options.title, actions: options.actions, diff --git a/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-popup/microfrontend-popup-intent-handler.interceptor.ts b/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-popup/microfrontend-popup-intent-handler.interceptor.ts index 0ad3194eb..499be4784 100644 --- a/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-popup/microfrontend-popup-intent-handler.interceptor.ts +++ b/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-popup/microfrontend-popup-intent-handler.interceptor.ts @@ -9,7 +9,7 @@ */ import {Injectable} from '@angular/core'; -import {APP_IDENTITY, Handler, IntentInterceptor, IntentMessage, mapToBody, MessageClient, MessageHeaders, ResponseStatusCodes} from '@scion/microfrontend-platform'; +import {Handler, IntentInterceptor, IntentMessage, mapToBody, MessageClient, MessageHeaders, ResponseStatusCodes} from '@scion/microfrontend-platform'; import {WorkbenchCapabilities, WorkbenchPopupCapability, WorkbenchPopupReferrer, ɵPopupContext, ɵWorkbenchCommands, ɵWorkbenchPopupCommand} from '@scion/workbench-client'; import {MicrofrontendPopupComponent} from './microfrontend-popup.component'; import {Observable} from 'rxjs'; @@ -22,6 +22,7 @@ import {PopupOrigin} from '../../popup/popup.origin'; import {WorkbenchViewRegistry} from '../../view/workbench-view.registry'; import {MicrofrontendHostPopupComponent} from '../microfrontend-host-popup/microfrontend-host-popup.component'; import {MicrofrontendWorkbenchView} from '../microfrontend-view/microfrontend-workbench-view.model'; +import {Microfrontends} from '../common/microfrontend.util'; /** * Handles popup intents, instructing the workbench to open a popup with the microfrontend declared on the resolved capability. @@ -87,7 +88,7 @@ export class MicrofrontendPopupIntentHandler implements IntentInterceptor { private async openPopup(message: IntentMessage<ɵWorkbenchPopupCommand>): Promise { const command = message.body!; const capability = message.capability as WorkbenchPopupCapability; - const isHostProvider = capability.metadata!.appSymbolicName === Beans.get(APP_IDENTITY); + const isHostProvider = Microfrontends.isHostProvider(capability); this._logger.debug(() => 'Handling microfrontend popup command', LoggerNames.MICROFRONTEND, command); const popupContext: ɵPopupContext = { diff --git a/projects/scion/workbench/src/lib/microfrontend-platform/routing/microfrontend-view-capability-validator.interceptor.ts b/projects/scion/workbench/src/lib/microfrontend-platform/routing/microfrontend-view-capability-validator.interceptor.ts index 610e04161..b53422c5f 100644 --- a/projects/scion/workbench/src/lib/microfrontend-platform/routing/microfrontend-view-capability-validator.interceptor.ts +++ b/projects/scion/workbench/src/lib/microfrontend-platform/routing/microfrontend-view-capability-validator.interceptor.ts @@ -11,6 +11,7 @@ import {Capability, CapabilityInterceptor} from '@scion/microfrontend-platform'; import {Injectable} from '@angular/core'; import {WorkbenchCapabilities, WorkbenchViewCapability} from '@scion/workbench-client'; +import {Microfrontends} from '../common/microfrontend.util'; /** * Asserts view capabilities to have required properties and assigns each view capability a stable identifer required for persistent navigation. @@ -35,6 +36,28 @@ export class MicrofrontendViewCapabilityValidator implements CapabilityIntercept throw Error(`[NullPathError] View capability requires a path to the microfrontend in its properties [capability=${JSON.stringify(viewCapability)}]`); } + if (Microfrontends.isHostProvider(viewCapability)) { + this.validateHostCapability(viewCapability); + } + return capability; } + + private validateHostCapability(hostCapability: WorkbenchViewCapability): void { + if (hostCapability.properties.title) { + throw Error(`[UnsupportedCapabilityProperty] Host view capability must not define the "title" property. Set the title via route data or the view handle instead. [capability=${JSON.stringify(hostCapability)}]`); + } + if (hostCapability.properties.heading) { + throw Error(`[UnsupportedCapabilityProperty] Host view capability must not define the "heading" property. Set the heading via route data or the view handle instead. [capability=${JSON.stringify(hostCapability)}]`); + } + if (hostCapability.properties.closable) { + throw Error(`[UnsupportedCapabilityProperty] Host view capability must not define "closable" property. Set the heading via route data or the view handle instead. [capability=${JSON.stringify(hostCapability)}]`); + } + if (hostCapability.properties.cssClass) { + throw Error(`[UnsupportedCapabilityProperty] Host view capability must not define the "cssClass" property. Set the CSS class(es) via route data or the view handle instead. [capability=${JSON.stringify(hostCapability)}]`); + } + if (hostCapability.properties.showSplash) { + throw Error(`[UnsupportedCapabilityProperty] Host view capability must not define the "showSplash" property. [capability=${JSON.stringify(hostCapability)}]`); + } + } } diff --git a/projects/scion/workbench/src/lib/microfrontend-platform/routing/microfrontend-view-intent-handler.interceptor.ts b/projects/scion/workbench/src/lib/microfrontend-platform/routing/microfrontend-view-intent-handler.interceptor.ts index affb059bf..0b399ac41 100644 --- a/projects/scion/workbench/src/lib/microfrontend-platform/routing/microfrontend-view-intent-handler.interceptor.ts +++ b/projects/scion/workbench/src/lib/microfrontend-platform/routing/microfrontend-view-intent-handler.interceptor.ts @@ -9,9 +9,9 @@ */ import {Handler, IntentInterceptor, IntentMessage, MessageClient, MessageHeaders, ResponseStatusCodes} from '@scion/microfrontend-platform'; -import {Injectable} from '@angular/core'; +import {Injectable, Injector, runInInjectionContext} from '@angular/core'; import {WorkbenchCapabilities, WorkbenchNavigationExtras, WorkbenchViewCapability} from '@scion/workbench-client'; -import {WorkbenchRouter} from '../../routing/workbench-router.service'; +import {ɵWorkbenchRouter} from '../../routing/ɵworkbench-router.service'; import {MicrofrontendViewRoutes} from './microfrontend-view-routes'; import {Logger, LoggerNames} from '../../logging'; import {Beans} from '@scion/toolkit/bean-manager'; @@ -19,18 +19,24 @@ import {Arrays, Dictionaries} from '@scion/toolkit/util'; import {WorkbenchViewRegistry} from '../../view/workbench-view.registry'; import {MicrofrontendWorkbenchView} from '../microfrontend-view/microfrontend-workbench-view.model'; import {Objects} from '../../common/objects.util'; +import {stringifyError} from '../../common/stringify-error.util'; +import {Microfrontends} from '../common/microfrontend.util'; +import {RouterUtils} from '../../routing/router.util'; /** * Handles microfrontend view intents, instructing the workbench to navigate to the microfrontend of the resolved capability. * + * Microfrontends of the host are displayed in {@link ViewComponent}, microfrontends of other applications in {@link MicrofrontendViewComponent}. + * * View intents are handled in this interceptor and are not transported to the providing application, enabling support for applications * that are not connected to the SCION Workbench. */ @Injectable(/* DO NOT PROVIDE via 'providedIn' metadata as only registered if microfrontend support is enabled. */) export class MicrofrontendViewIntentHandler implements IntentInterceptor { - constructor(private _workbenchRouter: WorkbenchRouter, + constructor(private _workbenchRouter: ɵWorkbenchRouter, private _viewRegistry: WorkbenchViewRegistry, + private _injector: Injector, private _logger: Logger) { } @@ -48,25 +54,29 @@ export class MicrofrontendViewIntentHandler implements IntentInterceptor { private async consumeViewIntent(message: IntentMessage): Promise { const replyTo = message.headers.get(MessageHeaders.ReplyTo); - const success = await this.navigate(message); - await Beans.get(MessageClient).publish(replyTo, success, {headers: new Map().set(MessageHeaders.Status, ResponseStatusCodes.TERMINAL)}); + try { + const success = await this.navigate(message); + await Beans.get(MessageClient).publish(replyTo, success, {headers: new Map().set(MessageHeaders.Status, ResponseStatusCodes.TERMINAL)}); + } + catch (error) { + await Beans.get(MessageClient).publish(replyTo, stringifyError(error), {headers: new Map().set(MessageHeaders.Status, ResponseStatusCodes.ERROR)}); + } } private async navigate(message: IntentMessage): Promise { - const viewCapability = message.capability as WorkbenchViewCapability; - const intent = message.intent; + const capability = message.capability as WorkbenchViewCapability; // TODO [Angular 20] remove backward compatibility for property 'blankInsertionIndex' const extras: WorkbenchNavigationExtras & {blankInsertionIndex?: number | 'start' | 'end' | 'before-active-view' | 'after-active-view'} = message.body ?? {}; + const intentParams = Objects.withoutUndefinedEntries(Dictionaries.coerce(message.intent.params)); + const {urlParams, transientParams} = MicrofrontendViewRoutes.splitParams(intentParams, capability); - const intentParams = Objects.withoutUndefinedEntries(Dictionaries.coerce(intent.params)); - const {urlParams, transientParams} = MicrofrontendViewRoutes.splitParams(intentParams, viewCapability); - const targets = this.resolveTargets(message, extras); - const commands = extras.close ? [] : MicrofrontendViewRoutes.createMicrofrontendNavigateCommands(viewCapability.metadata!.id, urlParams); + if (Microfrontends.isHostProvider(capability)) { + const path = Microfrontends.substituteNamedParameters(capability.properties.path, {...urlParams, capabilityId: capability.metadata!.id}); + const commands = runInInjectionContext(this._injector, () => RouterUtils.pathToCommands(path)); + this._logger.debug(() => `Navigating to: ${capability.properties.path}`, LoggerNames.MICROFRONTEND_ROUTING, commands, capability, transientParams); - this._logger.debug(() => `Navigating to: ${viewCapability.properties.path}`, LoggerNames.MICROFRONTEND_ROUTING, commands, viewCapability, transientParams); - const navigations = await Promise.all(Arrays.coerce(targets).map(target => { return this._workbenchRouter.navigate(commands, { - target, + target: extras.target, activate: extras.activate, close: extras.close, position: extras.position ?? extras.blankInsertionIndex, @@ -75,8 +85,26 @@ export class MicrofrontendViewIntentHandler implements IntentInterceptor { [MicrofrontendViewRoutes.STATE_TRANSIENT_PARAMS]: transientParams, }), }); - })); - return navigations.every(Boolean); + } + else { + const commands = extras.close ? [] : MicrofrontendViewRoutes.createMicrofrontendNavigateCommands(capability.metadata!.id, urlParams); + const targets = Arrays.coerce(this.resolveTargets(message, extras)); + this._logger.debug(() => `Navigating to: ${capability.properties.path}`, LoggerNames.MICROFRONTEND_ROUTING, commands, capability, transientParams); + + const navigations = await Promise.all(targets.map(target => { + return this._workbenchRouter.navigate(commands, { + target, + activate: extras.activate, + close: extras.close, + position: extras.position ?? extras.blankInsertionIndex, + cssClass: extras.cssClass, + state: Objects.withoutUndefinedEntries({ + [MicrofrontendViewRoutes.STATE_TRANSIENT_PARAMS]: transientParams, + }), + }); + })); + return navigations.every(Boolean); + } } /**