Skip to content

Commit

Permalink
feat(workbench-client/router): control workbench part to navigate views
Browse files Browse the repository at this point in the history
The navigator can now specify the part in which to navigate views.

The following rules apply:
- If target is `blank`, opens the view in the specified part;
- If target is `auto`, navigates matching views in the specified part, or opens a new view in that part otherwise;
- If the specified part is not in the layout, opens the view in the active part, with the active part of the main area taking precedence;
  • Loading branch information
Marcarrian committed Jun 20, 2024
1 parent 5b9f1e8 commit af702d0
Show file tree
Hide file tree
Showing 6 changed files with 288 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
</datalist>
</sci-form-field>

<sci-form-field label="Part ID">
<input [formControl]="form.controls.partId" class="e2e-part-id">
</sci-form-field>

<sci-form-field label="Position">
<input [formControl]="form.controls.position" [attr.list]="positionList" class="e2e-position">
<datalist [attr.id]="positionList">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export default class RouterPageComponent {
qualifier: this._formBuilder.array<FormGroup<KeyValueEntry>>([], Validators.required),
params: this._formBuilder.array<FormGroup<KeyValueEntry>>([]),
target: this._formBuilder.control(''),
partId: this._formBuilder.control(''),
position: this._formBuilder.control(''),
activate: this._formBuilder.control<boolean | undefined>(undefined),
close: this._formBuilder.control<boolean | undefined>(undefined),
Expand All @@ -66,6 +67,7 @@ export default class RouterPageComponent {
activate: this.form.controls.activate.value,
close: this.form.controls.close.value,
target: this.form.controls.target.value || undefined,
partId: this.form.controls.partId.value || undefined,
position: coercePosition(this.form.controls.position.value),
params: params || undefined,
cssClass: this.form.controls.cssClass.value,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export class RouterPagePO implements MicrofrontendViewPagePO {

private async enterExtras(extras: WorkbenchNavigationExtras | undefined): Promise<void> {
await this.enterTarget(extras?.target);
await this.enterPartId(extras?.partId);
await this.enterParams(extras?.params);
await this.checkActivate(extras?.activate);
await this.checkClose(extras?.close);
Expand All @@ -71,6 +72,10 @@ export class RouterPagePO implements MicrofrontendViewPagePO {
await this.locator.locator('input.e2e-target').fill(target ?? '');
}

private async enterPartId(partId?: string): Promise<void> {
await this.locator.locator('input.e2e-part-id').fill(partId ?? '');
}

private async enterParams(params?: Map<string, any> | Dictionary): Promise<void> {
const keyValueField = new SciKeyValueFieldPO(this.locator.locator('sci-key-value-field.e2e-params'));
await keyValueField.clear();
Expand Down
262 changes: 262 additions & 0 deletions projects/scion/e2e-testing/src/workbench-client/router.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {RouterPagePO} from './page-object/router-page.po';
import {expectView} from '../matcher/view-matcher';
import {MicrofrontendViewTestPagePO} from './page-object/test-pages/microfrontend-view-test-page.po';
import {PageNotFoundPagePO} from '../workbench/page-object/page-not-found-page.po';
import {ViewInfo} from '../workbench/page-object/view-info-dialog.po';

test.describe('Workbench Router', () => {

Expand Down Expand Up @@ -2034,4 +2035,265 @@ test.describe('Workbench Router', () => {
await expect(testeeView.tab.title).toHaveText('value1/value2/:param3');
await expect(testeeView.tab.heading).toHaveText('value1 value2 :param3');
});

test('should open view in the specified part', async ({appPO, workbenchNavigator, microfrontendNavigator}) => {
await appPO.navigateTo({microfrontendSupport: true});

// Add parts on the left and right.
await workbenchNavigator.modifyLayout(layout => layout
.addPart('left', {align: 'left', ratio: .25})
.addPart('right', {align: 'right', ratio: .25}),
);

await microfrontendNavigator.registerCapability('app1', {
type: 'view',
qualifier: {component: 'testee'},
properties: {
path: 'test-view',
title: 'testee',
},
});

// Open view in the left part.
const routerPage = await microfrontendNavigator.openInNewTab(RouterPagePO, 'app1');
await routerPage.navigate({component: 'testee'}, {
partId: 'left',
cssClass: 'testee',
});

// Expect view to be opened in the left part.
const view = appPO.view({cssClass: 'testee'});
await expect.poll(() => view.getInfo()).toMatchObject(
{
partId: 'left',
} satisfies Partial<ViewInfo>,
);
await expect(appPO.views()).toHaveCount(2);
});

test('should navigate existing view(s) in the specified part, or open a new view in the specified part otherwise', async ({appPO, workbenchNavigator, microfrontendNavigator}) => {
await appPO.navigateTo({microfrontendSupport: true});

// Add parts on the left and right.
await workbenchNavigator.modifyLayout(layout => layout
.addPart('left', {align: 'left', ratio: .25})
.addPart('right', {align: 'right', ratio: .25}),
);

await microfrontendNavigator.registerCapability('app1', {
type: 'view',
qualifier: {component: 'testee'},
params: [
{name: 'state', required: false},
],
properties: {
path: 'test-view;state=:state',
title: 'testee',
},
});

const view1 = appPO.view({cssClass: 'testee-1'});
const view2 = appPO.view({cssClass: 'testee-2'});
const view3 = appPO.view({cssClass: 'testee-3'});

// Open view in the left part.
const routerPage = await microfrontendNavigator.openInNewTab(RouterPagePO, 'app1');
await routerPage.navigate({component: 'testee'}, {
partId: 'left',
params: {state: '1'},
cssClass: 'testee-1',
});

// Expect view to be opened in the left part.
await expect.poll(() => view1.getInfo()).toMatchObject(
{
partId: 'left',
routeParams: {state: '1'},
} satisfies Partial<ViewInfo>,
);
await expect(appPO.views()).toHaveCount(2);

// Open view in the right part.
await routerPage.navigate({component: 'testee'}, {
partId: 'right',
params: {state: '2'},
cssClass: 'testee-2',
});

// Expect view in the left part not to be navigated.
await expect.poll(() => view1.getInfo()).toMatchObject(
{
partId: 'left',
routeParams: {state: '1'},
} satisfies Partial<ViewInfo>,
);
// Expect view to be opened in the right part.
await expect.poll(() => view2.getInfo()).toMatchObject(
{
partId: 'right',
routeParams: {state: '2'},
} satisfies Partial<ViewInfo>,
);
await expect(appPO.views()).toHaveCount(3);

// Navigate view in the left part.
await routerPage.navigate({component: 'testee'}, {
partId: 'left',
params: {state: '3'},
cssClass: 'testee-1',
});

// Expect view in the left part to be navigated.
await expect.poll(() => view1.getInfo()).toMatchObject(
{
partId: 'left',
routeParams: {state: '3'},
} satisfies Partial<ViewInfo>,
);
// Expect view in the right part not to be navigated.
await expect.poll(() => view2.getInfo()).toMatchObject(
{
partId: 'right',
routeParams: {state: '2'},
} satisfies Partial<ViewInfo>,
);
await expect(appPO.views()).toHaveCount(3);

// Open new view in the right part.
await routerPage.navigate({component: 'testee'}, {
partId: 'right',
target: 'blank',
params: {state: '4'},
cssClass: 'testee-3',
});

// Expect view in the left part not to be navigated.
await expect.poll(() => view1.getInfo()).toMatchObject(
{
partId: 'left',
routeParams: {state: '3'},
} satisfies Partial<ViewInfo>,
);
// Expect view in the right part not to be navigated.
await expect.poll(() => view2.getInfo()).toMatchObject(
{
partId: 'right',
routeParams: {state: '2'},
} satisfies Partial<ViewInfo>,
);
// Expect view to be opened in the right part.
await expect.poll(() => view3.getInfo()).toMatchObject(
{
partId: 'right',
routeParams: {state: '4'},
} satisfies Partial<ViewInfo>,
);
await expect(appPO.views()).toHaveCount(4);
});

test('should open view in the active part of the main area if specified part is not in the layout', async ({appPO, workbenchNavigator, microfrontendNavigator}) => {
await appPO.navigateTo({microfrontendSupport: true});

// Add parts on the left and right.
await workbenchNavigator.modifyLayout(layout => layout
.addPart('left', {align: 'left', ratio: .25})
.addPart('right', {align: 'right', ratio: .25}),
);

await microfrontendNavigator.registerCapability('app1', {
type: 'view',
qualifier: {component: 'testee'},
properties: {
path: 'test-view',
title: 'testee',
},
});

// Open view in a part not contained in the layout.
const routerPage = await microfrontendNavigator.openInNewTab(RouterPagePO, 'app1');
await routerPage.navigate({component: 'testee'}, {
partId: 'does-not-exist',
cssClass: 'testee',
});

// Expect view to be opened in the main area.
const view = appPO.view({cssClass: 'testee'});
await expect.poll(() => view.getInfo()).toMatchObject(
{
partId: await appPO.activePart({inMainArea: true}).getPartId(),
} satisfies Partial<ViewInfo>,
);
await expect(appPO.views()).toHaveCount(2);
});

test('should close view in the specified part', async ({appPO, workbenchNavigator, microfrontendNavigator}) => {
await appPO.navigateTo({microfrontendSupport: true});

// Add parts on the left and right.
await workbenchNavigator.modifyLayout(layout => layout
.addPart('left', {align: 'left', ratio: .25})
.addPart('right', {align: 'right', ratio: .25}),
);

await microfrontendNavigator.registerCapability('app1', {
type: 'view',
qualifier: {component: 'testee'},
properties: {
path: 'test-view',
title: 'testee',
},
});

const viewPage1 = new ViewPagePO(appPO, {cssClass: 'testee-1'});
const viewPage2 = new ViewPagePO(appPO, {cssClass: 'testee-2'});

// Open view in the left part.
const routerPage = await microfrontendNavigator.openInNewTab(RouterPagePO, 'app1');
await routerPage.navigate({component: 'testee'}, {
partId: 'left',
cssClass: 'testee-1',
});

// Open view in the right part.
await routerPage.navigate({component: 'testee'}, {
partId: 'right',
cssClass: 'testee-2',
});

// Expect view to be opened in the left part.
await expect.poll(() => viewPage1.view.getInfo()).toMatchObject(
{
partId: 'left',
} satisfies Partial<ViewInfo>,
);
// Expect view to be opened in the right part.
await expect.poll(() => viewPage2.view.getInfo()).toMatchObject(
{
partId: 'right',
} satisfies Partial<ViewInfo>,
);
await expect(appPO.views()).toHaveCount(3);

// Close view in the right part.
await routerPage.navigate({component: 'testee'}, {
partId: 'right',
close: true,
});

// Expect view in the right part to be closed.
await expectView(viewPage1).toBeActive();
await expectView(viewPage2).not.toBeAttached();
await expect(appPO.views()).toHaveCount(2);

// Close view in the left part.
await routerPage.navigate({component: 'testee'}, {
partId: 'left',
close: true,
});

// Expect views in the left and right part to be closed.
await expectView(viewPage1).not.toBeAttached();
await expectView(viewPage2).not.toBeAttached();
await expect(appPO.views()).toHaveCount(1);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,15 @@ export interface WorkbenchNavigationExtras {
* - <viewId>: Navigates the specified view. If already opened, replaces it, or opens a new view otherwise.
*/
target?: string | 'blank' | 'auto';
/**
* Controls which part to navigate views in.
*
* If target is `blank`, opens the view in the specified part.
* If target is `auto`, navigates matching views in the specified part, or opens a new view in that part otherwise.
*
* If the specified part is not in the layout, opens the view in the active part, with the active part of the main area taking precedence.
*/
partId?: string;
/**
* Instructs the router to activate the view. Default is `true`.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,13 @@ export class MicrofrontendViewIntentHandler implements IntentInterceptor {
const {urlParams, transientParams} = MicrofrontendViewRoutes.splitParams(intentParams, viewCapability);
const targets = this.resolveTargets(message, extras);
const commands = extras.close ? [] : MicrofrontendViewRoutes.createMicrofrontendNavigateCommands(viewCapability.metadata!.id, urlParams);
const partId = extras.close ? undefined : extras.partId;

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,
partId,
activate: extras.activate,
close: extras.close,
position: extras.position ?? extras.blankInsertionIndex,
Expand All @@ -88,10 +90,10 @@ export class MicrofrontendViewIntentHandler implements IntentInterceptor {
throw Error(`[NavigateError] The target must be empty if closing a view [target=${(extras.target)}]`);
}
if (extras.close) {
return this.resolvePresentViewIds(intentMessage, {matchWildcardParams: true}) ?? [];
return this.resolvePresentViewIds(intentMessage, extras, {matchWildcardParams: true}) ?? [];
}
if (!extras.target || extras.target === 'auto') {
return this.resolvePresentViewIds(intentMessage) ?? 'blank';
return this.resolvePresentViewIds(intentMessage, extras) ?? 'blank';
}
return extras.target;
}
Expand All @@ -102,11 +104,12 @@ export class MicrofrontendViewIntentHandler implements IntentInterceptor {
*
* Allows matching wildcard parameters by setting the option `matchWildcardParameters` to `true`.
*/
private resolvePresentViewIds(intentMessage: IntentMessage, options?: {matchWildcardParams?: boolean}): string[] | null {
private resolvePresentViewIds(intentMessage: IntentMessage, extras: WorkbenchNavigationExtras, options?: {matchWildcardParams?: boolean}): string[] | null {
const requiredParams = intentMessage.capability.params?.filter(param => param.required).map(param => param.name) ?? [];
const matchWildcardParams = options?.matchWildcardParams ?? false;

const viewIds = this._viewRegistry.views
.filter(view => !extras.partId || extras.partId === view.part.id)
.filter(view => {
const microfrontendWorkbenchView = view.adapt(MicrofrontendWorkbenchView);
if (!microfrontendWorkbenchView) {
Expand Down

0 comments on commit af702d0

Please sign in to comment.