Skip to content

Commit

Permalink
fix(workbench): support application URL to contain view outlets of pe…
Browse files Browse the repository at this point in the history
…rspectival views

Previously, opening the application with a URL containing view outlets of perspectival views caused the Angular error `NG04002` because the perspective layout was unavailable during initial navigation. Now we register routes based on the view outlets in the URL, not the views in the layout.

fixes #474
  • Loading branch information
danielwiehl authored and k-genov committed Aug 21, 2023
1 parent 754747a commit 1eead4b
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 44 deletions.
14 changes: 13 additions & 1 deletion projects/scion/e2e-testing/src/app.po.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,19 @@ export class AppPO {
featureQueryParams.append('showNewTabAction', `${options.showNewTabAction}`);
}

await this.page.goto(`${options?.url ?? ''}/?${this._workbenchStartupQueryParams.toString()}#/${featureQueryParams.toString() ? `?${featureQueryParams.toString()}` : ''}`);
// Perform navigation.
await this.page.goto((() => {
const [baseUrl = '/', hashedUrl = ''] = (options?.url?.split('#') ?? []);

// Add startup query params to the base URL part.
const url = `${baseUrl}?${this._workbenchStartupQueryParams.toString()}#${hashedUrl}`;
if (!featureQueryParams.size) {
return url;
}
// Add feature query params to the hashed URL part.
return hashedUrl.includes('?') ? `${url}&${featureQueryParams}` : `${url}?${featureQueryParams}`;
})());

// Wait until the workbench completed startup.
await this.waitUntilWorkbenchStarted();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,71 +23,70 @@ import {SciKeyValuePO} from '../../@scion/components.internal/key-value.po';
*/
export class ViewPagePO {

private readonly _locator: Locator;

public readonly locator: Locator;
public readonly viewPO: ViewPO;
public readonly viewTabPO: ViewTabPO;

constructor(appPO: AppPO, public viewId: string) {
this.viewPO = appPO.view({viewId});
this.viewTabPO = appPO.view({viewId}).viewTab;
this._locator = this.viewPO.locator('app-view-page');
this.locator = this.viewPO.locator('app-view-page');
}

public async isPresent(): Promise<boolean> {
return await this.viewTabPO.isPresent() && await isPresent(this._locator);
return await this.viewTabPO.isPresent() && await isPresent(this.locator);
}

public async isVisible(): Promise<boolean> {
return await this.viewPO.isVisible() && await this._locator.isVisible();
return await this.viewPO.isVisible() && await this.locator.isVisible();
}

public async getViewId(): Promise<string> {
return this._locator.locator('span.e2e-view-id').innerText();
return this.locator.locator('span.e2e-view-id').innerText();
}

public async getComponentInstanceId(): Promise<string> {
return this._locator.locator('span.e2e-component-instance-id').innerText();
return this.locator.locator('span.e2e-component-instance-id').innerText();
}

public async getRouteParams(): Promise<Params> {
const accordionPO = new SciAccordionPO(this._locator.locator('sci-accordion.e2e-route-params'));
const accordionPO = new SciAccordionPO(this.locator.locator('sci-accordion.e2e-route-params'));
await accordionPO.expand();
try {
return await new SciKeyValuePO(this._locator.locator('sci-key-value.e2e-route-params')).readEntries();
return await new SciKeyValuePO(this.locator.locator('sci-key-value.e2e-route-params')).readEntries();
}
finally {
await accordionPO.collapse();
}
}

public async enterTitle(title: string): Promise<void> {
await this._locator.locator('input.e2e-title').fill(title);
await this.locator.locator('input.e2e-title').fill(title);
}

public async enterHeading(heading: string): Promise<void> {
await this._locator.locator('input.e2e-heading').fill(heading);
await this.locator.locator('input.e2e-heading').fill(heading);
}

public async checkDirty(check: boolean): Promise<void> {
await new SciCheckboxPO(this._locator.locator('sci-checkbox.e2e-dirty')).toggle(check);
await new SciCheckboxPO(this.locator.locator('sci-checkbox.e2e-dirty')).toggle(check);
}

public async checkClosable(check: boolean): Promise<void> {
await new SciCheckboxPO(this._locator.locator('sci-checkbox.e2e-closable')).toggle(check);
await new SciCheckboxPO(this.locator.locator('sci-checkbox.e2e-closable')).toggle(check);
}

public async clickClose(): Promise<void> {
const accordionPO = new SciAccordionPO(this._locator.locator('sci-accordion.e2e-view-actions'));
const accordionPO = new SciAccordionPO(this.locator.locator('sci-accordion.e2e-view-actions'));
await accordionPO.expand();
await this._locator.locator('button.e2e-close').click();
await this.locator.locator('button.e2e-close').click();
}

public async addViewAction(partAction: WorkbenchPartActionDescriptor, options?: {append?: boolean}): Promise<void> {
const accordionPO = new SciAccordionPO(this._locator.locator('sci-accordion.e2e-part-actions'));
const accordionPO = new SciAccordionPO(this.locator.locator('sci-accordion.e2e-part-actions'));
await accordionPO.expand();
try {
const inputLocator = this._locator.locator('input.e2e-part-actions');
const inputLocator = this.locator.locator('input.e2e-part-actions');
if (options?.append ?? true) {
const input = await inputLocator.inputValue() || null;
const presentActions: WorkbenchPartActionDescriptor[] = coerceArray(input ? JSON.parse(input) : null);
Expand All @@ -103,11 +102,11 @@ export class ViewPagePO {
}

public async enterFreeText(text: string): Promise<void> {
await this._locator.locator('input.e2e-free-text').fill(text);
await this.locator.locator('input.e2e-free-text').fill(text);
}

public getFreeText(): Promise<string> {
return this._locator.locator('input.e2e-free-text').inputValue();
return this.locator.locator('input.e2e-free-text').inputValue();
}
}

Expand Down
80 changes: 80 additions & 0 deletions projects/scion/e2e-testing/src/workbench/router.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import {expect} from '@playwright/test';
import {test} from '../fixtures';
import {RouterPagePO} from './page-object/router-page.po';
import {ViewPagePO} from './page-object/view-page.po';
import {LayoutPagePO} from './page-object/layout-page.po';
import {MPart, MTreeNode} from '../matcher/to-equal-workbench-layout.matcher';
import {MAIN_AREA_PART_ID} from '../workbench.model';

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

Expand Down Expand Up @@ -1165,4 +1168,81 @@ test.describe('Workbench Router', () => {
// expect the test view to be the active view
await expect(await testee1ViewPO.viewTab.isActive()).toBe(true);
});

test('should support app URL to contain view outlets of views in the perspectival grid', async ({appPO, workbenchNavigator, page}) => {
await appPO.navigateTo({microfrontendSupport: false, perspectives: ['perspective']});

// Define perspective with a part on the left.
const perspectiveToggleButtonPO = await appPO.header.perspectiveToggleButton({perspectiveId: 'perspective'});
await perspectiveToggleButtonPO.click();
const layoutPagePO = await workbenchNavigator.openInNewTab(LayoutPagePO);
await layoutPagePO.addPart('left', {align: 'left', ratio: .25});

// Add view to the left perspectival part.
const routerPagePO = await workbenchNavigator.openInNewTab(RouterPagePO);
await routerPagePO.enterPath('test-view');
await routerPagePO.enterTarget('blank');
await routerPagePO.enterBlankPartId('left');
await routerPagePO.clickNavigate();

// Expect the view to be opened in the left part.
await expect(appPO.workbenchLocator).toEqualWorkbenchLayout({
peripheralGrid: {
root: new MTreeNode({
direction: 'row',
ratio: .25,
child1: new MPart({
id: 'left',
views: [{id: 'view.3'}], // test view page
activeViewId: 'view.3',
}),
child2: new MPart({id: MAIN_AREA_PART_ID}),
}),
},
mainGrid: {
root: new MPart({
id: await layoutPagePO.viewPO.part.getPartId(),
views: [{id: 'view.1'}, {id: 'view.2'}], // layout page, router page
activeViewId: 'view.2',
}),
activePartId: await layoutPagePO.viewPO.part.getPartId(),
},
});

// Capture current URL.
const url = page.url();

// Clear the browser URL.
await page.goto('about:blank');

// WHEN: Opening the app with a URL that contains view outlets of views from the perspective grid
await appPO.navigateTo({url, microfrontendSupport: false, perspectives: ['perspective']});

// THEN: Expect the workbench layout to be restored.
await expect(appPO.workbenchLocator).toEqualWorkbenchLayout({
peripheralGrid: {
root: new MTreeNode({
direction: 'row',
ratio: .25,
child1: new MPart({
id: 'left',
views: [{id: 'view.3'}], // test view page
activeViewId: 'view.3',
}),
child2: new MPart({id: MAIN_AREA_PART_ID}),
}),
},
mainGrid: {
root: new MPart({
id: await layoutPagePO.viewPO.part.getPartId(),
views: [{id: 'view.1'}, {id: 'view.2'}], // layout page, router page
activeViewId: 'view.2',
}),
activePartId: await layoutPagePO.viewPO.part.getPartId(),
},
});

// Expect the test view to display.
await expect(new ViewPagePO(appPO, 'view.3').locator).toBeVisible();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

import {Injectable, IterableChanges, IterableDiffer, IterableDiffers} from '@angular/core';
import {ɵWorkbenchLayout} from '../layout/ɵworkbench-layout';
import {UrlTree} from '@angular/router';
import {RouterUtils} from './router.util';

/**
* Stateful differ for finding added/removed workbench layout elements.
Expand All @@ -19,22 +21,26 @@ export class WorkbenchLayoutDiffer {

private _partsDiffer: IterableDiffer<string>;
private _viewsDiffer: IterableDiffer<string>;
private _viewOutletsDiffer: IterableDiffer<string>;

constructor(differs: IterableDiffers) {
this._partsDiffer = differs.find([]).create<string>();
this._viewsDiffer = differs.find([]).create<string>();
this._viewOutletsDiffer = differs.find([]).create<string>();
}

/**
* Computes differences in the layout since last time {@link WorkbenchLayoutDiffer#diff} was invoked.
*/
public diff(workbenchLayout?: ɵWorkbenchLayout): WorkbenchLayoutDiff {
public diff(workbenchLayout: ɵWorkbenchLayout | null, urlTree: UrlTree): WorkbenchLayoutDiff {
const partIds = workbenchLayout?.parts().map(part => part.id) || [];
const viewIds = workbenchLayout?.views().map(view => view.id) || [];
const viewOutlets = Object.keys(urlTree.root.children).filter(RouterUtils.isPrimaryRouteTarget);

return new WorkbenchLayoutDiff({
parts: this._partsDiffer.diff(partIds),
views: this._viewsDiffer.diff(viewIds),
viewOutlets: this._viewOutletsDiffer.diff(viewOutlets),
});
}
}
Expand All @@ -50,12 +56,18 @@ export class WorkbenchLayoutDiff {
public readonly addedViews = new Array<string>();
public readonly removedViews = new Array<string>();

constructor(changes: {parts: IterableChanges<string> | null; views: IterableChanges<string> | null}) {
public readonly addedViewOutlets = new Array<string>();
public readonly removedViewOutlets = new Array<string>();

constructor(changes: {parts: IterableChanges<string> | null; views: IterableChanges<string> | null; viewOutlets: IterableChanges<string> | null}) {
changes.parts?.forEachAddedItem(({item}) => this.addedParts.push(item));
changes.parts?.forEachRemovedItem(({item}) => this.removedParts.push(item));

changes.views?.forEachAddedItem(({item}) => this.addedViews.push(item));
changes.views?.forEachRemovedItem(({item}) => this.removedViews.push(item));

changes.viewOutlets?.forEachAddedItem(({item}) => this.addedViewOutlets.push(item));
changes.viewOutlets?.forEachRemovedItem(({item}) => this.removedViewOutlets.push(item));
}

public toString(): string {
Expand All @@ -64,6 +76,8 @@ export class WorkbenchLayoutDiff {
.concat(this.removedParts.length ? `removedParts=[${this.removedParts}]` : [])
.concat(this.addedViews.length ? `addedViews=[${this.addedViews}]` : [])
.concat(this.removedViews.length ? `removedViews=[${this.removedViews}]` : [])
.concat(this.addedViewOutlets.length ? `addedViewOutlets=[${this.addedViewOutlets}]` : [])
.concat(this.removedViewOutlets.length ? `removedViewOutlets=[${this.removedViewOutlets}]` : [])
.join(', ')}`;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,18 @@ export class WorkbenchPopupDiffer {
*/
export class WorkbenchPopupDiff {

public readonly addedPopups = new Array<string>();
public readonly removedPopups = new Array<string>();
public readonly addedPopupOutlets = new Array<string>();
public readonly removedPopupOutlets = new Array<string>();

constructor(changes: IterableChanges<string> | null) {
changes?.forEachAddedItem(({item}) => this.addedPopups.push(item));
changes?.forEachRemovedItem(({item}) => this.removedPopups.push(item));
changes?.forEachAddedItem(({item}) => this.addedPopupOutlets.push(item));
changes?.forEachRemovedItem(({item}) => this.removedPopupOutlets.push(item));
}

public toString(): string {
return `${new Array<string>()
.concat(this.addedPopups.length ? `addedPopups=[${this.addedPopups}]` : [])
.concat(this.removedPopups.length ? `removedPopups=[${this.removedPopups}]` : [])
.concat(this.addedPopupOutlets.length ? `addedPopupOutlets=[${this.addedPopupOutlets}]` : [])
.concat(this.removedPopupOutlets.length ? `removedPopupOutlets=[${this.removedPopupOutlets}]` : [])
.join(', ')}`;
}
}
Loading

0 comments on commit 1eead4b

Please sign in to comment.