Skip to content

Commit

Permalink
feat: Support ViewSection dropdown menu actions
Browse files Browse the repository at this point in the history
  • Loading branch information
djelinek committed Feb 26, 2025
1 parent be8b2d6 commit 88c008e
Show file tree
Hide file tree
Showing 7 changed files with 137 additions and 68 deletions.
17 changes: 16 additions & 1 deletion docs/ViewSection.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,28 @@ Section header may also contain some action buttons.

```typescript
// get an action button by label
const action = await section.getAction("New File");
const action = await section.getAction("New File") as ViewPanelAction;
// get all action buttons for the section
const actions = await section.getActions();
// click an action button
await action.click();
```

##### Action Buttons - Dropdown

![actionButtonDropdown](images/viewActions-dropdown.png)

**Note:** Be aware that it is not supported on macOS. For more information see [Known Issues](https://github.com/redhat-developer/vscode-extension-tester/blob/main/KNOWN_ISSUES.md).

```typescript
// find an view action button by title
const action = (await view.getAction("Hello Who...")) as ViewPanelActionDropdown;
// open the dropdown for that button
const menu = await action.open();
// select an item from an opened context menu
await menu.select("Hello a World");
```

#### (Tree) Items Manipulation

```typescript
Expand Down
Binary file added docs/images/viewActions-dropdown.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License", destination); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { ElementWithContextMenu } from './ElementWithContextMenu';
import { ContextMenu } from './menu/ContextMenu';
import { By, Key } from 'selenium-webdriver';
import { AbstractElement } from './AbstractElement';

export abstract class ActionButtonElementDropdown extends AbstractElement {
async open(): Promise<ContextMenu> {
await this.click();
const shadowRootHost = await this.enclosingItem.findElements(By.className('shadow-root-host'));
const actions = this.getDriver().actions();
await actions.clear();
await actions.sendKeys(Key.ESCAPE).perform();

if (shadowRootHost.length > 0) {
if ((await this.getAttribute('aria-expanded')) !== 'true') {
await this.click();
}
const shadowRoot = await shadowRootHost[0].getShadowRoot();
return new ContextMenu(await shadowRoot.findElement(By.className('monaco-menu-container'))).wait();
} else {
await this.click();
const workbench = await this.getDriver().findElement(ElementWithContextMenu.locators.Workbench.constructor);
return new ContextMenu(workbench).wait();
}
}
}
50 changes: 9 additions & 41 deletions packages/page-objects/src/components/editor/EditorAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,52 +15,20 @@
* limitations under the License.
*/

import { EditorGroup } from './EditorView';
import { ContextMenu, Key, WebElement } from '../..';
import { ElementWithContextMenu } from '../ElementWithContextMenu';
import { ChromiumWebDriver } from 'selenium-webdriver/chromium';

export class EditorAction extends ElementWithContextMenu {
constructor(element: WebElement, parent: EditorGroup) {
super(element, parent);
}
import { ActionButtonElementDropdown } from '../ActionButtonElementDropdown';

/**
* Base class for editor actions that provides a common method to get the title.
*/
abstract class BaseEditorAction extends ActionButtonElementDropdown {
/**
* Get text description of the action.
*/
async getTitle(): Promise<string> {
return await this.getAttribute(EditorAction.locators.EditorView.attribute);
return await this.getAttribute(BaseEditorAction.locators.EditorView.attribute);
}
}

export class EditorActionDropdown extends EditorAction {
async open(): Promise<ContextMenu> {
await this.click();
const shadowRootHost = await this.enclosingItem.findElements(EditorAction.locators.EditorAction.shadowRootHost);
const actions = this.getDriver().actions();
await actions.clear();
await actions.sendKeys(Key.ESCAPE).perform();
const webdriverCapabilities = await (this.getDriver() as ChromiumWebDriver).getCapabilities();
const chromiumVersion = webdriverCapabilities.getBrowserVersion();
if (shadowRootHost.length > 0) {
if ((await this.getAttribute('aria-expanded')) !== 'true') {
await this.click();
}
let shadowRoot;
const webdriverCapabilities = await (this.getDriver() as ChromiumWebDriver).getCapabilities();
const chromiumVersion = webdriverCapabilities.getBrowserVersion();
if (chromiumVersion && parseInt(chromiumVersion.split('.')[0]) >= 96) {
shadowRoot = await shadowRootHost[0].getShadowRoot();
return new ContextMenu(await shadowRoot.findElement(EditorAction.locators.EditorAction.monacoMenuContainer)).wait();
} else {
shadowRoot = (await this.getDriver().executeScript('return arguments[0].shadowRoot', shadowRootHost[0])) as WebElement;
return new ContextMenu(shadowRoot).wait();
}
} else if (chromiumVersion && parseInt(chromiumVersion.split('.')[0]) >= 100) {
await this.click();
const workbench = await this.getDriver().findElement(ElementWithContextMenu.locators.Workbench.constructor);
return new ContextMenu(workbench).wait();
}
return await super.openContextMenu();
}
}
export class EditorAction extends BaseEditorAction {}

export class EditorActionDropdown extends BaseEditorAction {}
45 changes: 28 additions & 17 deletions packages/page-objects/src/components/sidebar/ViewSection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { ContextMenu, ViewContent, ViewItem, waitForAttributeValue, WelcomeConte
import { AbstractElement } from '../AbstractElement';
import { ElementWithContextMenu } from '../ElementWithContextMenu';
import { ChromiumWebDriver } from 'selenium-webdriver/chromium';
import { ActionButtonElementDropdown } from '../ActionButtonElementDropdown';

export type ViewSectionConstructor<T extends ViewSection> = {
new (rootElement: WebElement, tree: ViewContent): T;
Expand Down Expand Up @@ -55,6 +56,7 @@ export abstract class ViewSection extends AbstractElement {
const collapseExpandButton = await header.findElement(ViewSection.locators.ViewSection.headerCollapseExpandButton);
await collapseExpandButton.click();
await this.getDriver().wait(waitForAttributeValue(header, ViewSection.locators.ViewSection.headerExpanded, 'true'), timeout);
await this.getDriver().sleep(500);
}
}

Expand All @@ -71,6 +73,7 @@ export abstract class ViewSection extends AbstractElement {
const collapseExpandButton = await header.findElement(ViewSection.locators.ViewSection.headerCollapseExpandButton);
await collapseExpandButton.click();
await this.getDriver().wait(waitForAttributeValue(header, ViewSection.locators.ViewSection.headerExpanded, 'false'), timeout);
await this.getDriver().sleep(500);
}
}

Expand Down Expand Up @@ -140,18 +143,17 @@ export abstract class ViewSection extends AbstractElement {
* @returns Promise resolving to array of ViewPanelAction objects
*/
async getActions(): Promise<ViewPanelAction[]> {
const actions: ViewPanelAction[] = [];

if (!(await this.isHeaderHidden())) {
const header = await this.findElement(ViewSection.locators.ViewSection.header);
const act = await header.findElement(ViewSection.locators.ViewSection.actions);
const elements = await act.findElements(ViewSection.locators.ViewSection.button);

for (const element of elements) {
actions.push(await new ViewPanelAction(element, this).wait());
}
}
return actions;
const actions = await this.findElement(ViewSection.locators.ViewSection.actions).findElements(ViewSection.locators.ViewSection.button);
return Promise.all(
actions.map(async (action) => {
const dropdown = await action.getAttribute('aria-haspopup');
if (dropdown) {
return new ViewPanelActionDropdown(action, this);
} else {
return new ViewPanelAction(action, this);
}
}),
);
}

/**
Expand Down Expand Up @@ -208,9 +210,10 @@ export abstract class ViewSection extends AbstractElement {
}

/**
* Action button on the header of a view section
* Base class for action buttons on view sections.
* Provides shared functionality for both standard and dropdown actions.
*/
export class ViewPanelAction extends AbstractElement {
abstract class BaseViewPanelAction extends ActionButtonElementDropdown {
constructor(element: WebElement, viewPart: ViewSection) {
super(element, viewPart);
}
Expand All @@ -219,11 +222,19 @@ export class ViewPanelAction extends AbstractElement {
* Get label of the action button
*/
async getLabel(): Promise<string> {
return await this.getAttribute(ViewSection.locators.ViewSection.buttonLabel);
return await this.getAttribute(BaseViewPanelAction.locators.ViewSection.buttonLabel);
}

async wait(timeout: number = 1000): Promise<this> {
await this.getDriver().wait(until.elementLocated(ViewSection.locators.ViewSection.actions), timeout);
/**
* Wait for the action button to be located within a given timeout.
* @param timeout Time in milliseconds (default: 1000ms)
*/
async wait(timeout: number = 1_000): Promise<this> {
await this.getDriver().wait(until.elementLocated(BaseViewPanelAction.locators.ViewSection.actions), timeout);
return this;
}
}

export class ViewPanelAction extends BaseViewPanelAction {}

export class ViewPanelActionDropdown extends BaseViewPanelAction {}
30 changes: 24 additions & 6 deletions tests/test-project/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,7 @@
{
"command": "testView.refresh",
"title": "Refresh",
"icon": {
"light": "resources/icons/light/refresh.svg",
"dark": "resources/icons/dark/refresh.svg"
}
"icon": "$(refresh)"
}
],
"viewsContainers": {
Expand Down Expand Up @@ -202,10 +199,31 @@
"view/title": [
{
"command": "testView.refresh",
"group": "navigation"
"group": "navigation@2",
"when": "view == testView || view == testView2"
},
{
"submenu": "extester.menu.test",
"group": "navigation@1",
"when": "view == testView"
}
],
"extester.menu.test": [
{
"command": "extension.helloWorld"
},
{
"command": "extension.helloWorld2"
}
]
}
},
"submenus": [
{
"id": "extester.menu.test",
"label": "Hello Who...",
"icon": "$(rocket)"
}
]
},
"scripts": {
"vscode:prepublish": "npm run build",
Expand Down
20 changes: 17 additions & 3 deletions tests/test-project/src/test/xsideBar/customView.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import {
ViewContent,
ViewControl,
ViewItem,
ViewPanelAction,
ViewPanelActionDropdown,
WelcomeContentButton,
WelcomeContentSection,
Workbench,
Expand All @@ -48,7 +50,6 @@ describe('CustomTreeSection', () => {
emptySection = await content.getSection('Test View 2');
await emptySection.expand();
emptyViewSection = await content.getSection('Empty View');
await emptyViewSection.expand();
});

after(async () => {
Expand Down Expand Up @@ -113,18 +114,30 @@ describe('CustomTreeSection', () => {
});

it('getAction works', async () => {
const action = await section.getAction('Collapse All');
const action = (await section.getAction('Collapse All')) as ViewPanelAction;
expect(await action?.getLabel()).equals('Collapse All');
});

it('getAction of EMPTY works', async () => {
const action = await emptySection.getAction('Refresh');
const action = (await emptySection.getAction('Refresh')) as ViewPanelAction;
expect(await action?.getLabel()).equals('Refresh');
await emptySection.collapse();
});

(process.platform === 'darwin' ? it.skip : it)('getAction dropdown works', async () => {
const action = (await section.getAction('Hello Who...')) as ViewPanelActionDropdown;
const menu = await action.open();
expect(menu).not.undefined;

await menu.select('Hello a World');
const infoMessage = await (await new Workbench().openNotificationsCenter()).getNotifications(NotificationType.Info);
expect(await infoMessage[0].getMessage()).to.equal('Hello World, Test Project!');
await (await new Workbench().openNotificationsCenter()).clearAllNotifications();
});

it('findWelcomeContent returns undefined if no WelcomeContent is present', async () => {
expect(await section.findWelcomeContent()).to.equal(undefined);
await emptyViewSection.expand();
expect(await emptyViewSection.findWelcomeContent()).to.not.equal(undefined);
});

Expand Down Expand Up @@ -280,6 +293,7 @@ describe('CustomTreeSection', () => {

it('clicking on the tree item with a command assigned, triggers the command', async () => {
await dItem?.click();
await dItem?.getDriver().sleep(1_000);
const errorNotification = await (await bench.openNotificationsCenter()).getNotifications(NotificationType.Error);
expect(errorNotification).to.have.length(1);
expect(await errorNotification[0].getMessage()).to.equal('This is an error!');
Expand Down

0 comments on commit 88c008e

Please sign in to comment.