Skip to content

Commit 457bc33

Browse files
authored
feat: Support ViewSection dropdown menu actions (#1782)
1 parent be8b2d6 commit 457bc33

File tree

7 files changed

+137
-68
lines changed

7 files changed

+137
-68
lines changed

docs/ViewSection.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,28 @@ Section header may also contain some action buttons.
3131

3232
```typescript
3333
// get an action button by label
34-
const action = await section.getAction("New File");
34+
const action = (await section.getAction("New File")) as ViewPanelAction;
3535
// get all action buttons for the section
3636
const actions = await section.getActions();
3737
// click an action button
3838
await action.click();
3939
```
4040

41+
##### Action Buttons - Dropdown
42+
43+
![actionButtonDropdown](images/viewActions-dropdown.png)
44+
45+
**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).
46+
47+
```typescript
48+
// find an view action button by title
49+
const action = (await view.getAction("Hello Who...")) as ViewPanelActionDropdown;
50+
// open the dropdown for that button
51+
const menu = await action.open();
52+
// select an item from an opened context menu
53+
await menu.select("Hello a World");
54+
```
55+
4156
#### (Tree) Items Manipulation
4257

4358
```typescript
16.9 KB
Loading
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License", destination); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* https://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import { ElementWithContextMenu } from './ElementWithContextMenu';
19+
import { ContextMenu } from './menu/ContextMenu';
20+
import { By, Key } from 'selenium-webdriver';
21+
import { AbstractElement } from './AbstractElement';
22+
23+
export abstract class ActionButtonElementDropdown extends AbstractElement {
24+
async open(): Promise<ContextMenu> {
25+
await this.click();
26+
const shadowRootHost = await this.enclosingItem.findElements(By.className('shadow-root-host'));
27+
const actions = this.getDriver().actions();
28+
await actions.clear();
29+
await actions.sendKeys(Key.ESCAPE).perform();
30+
31+
if (shadowRootHost.length > 0) {
32+
if ((await this.getAttribute('aria-expanded')) !== 'true') {
33+
await this.click();
34+
}
35+
const shadowRoot = await shadowRootHost[0].getShadowRoot();
36+
return new ContextMenu(await shadowRoot.findElement(By.className('monaco-menu-container'))).wait();
37+
} else {
38+
await this.click();
39+
const workbench = await this.getDriver().findElement(ElementWithContextMenu.locators.Workbench.constructor);
40+
return new ContextMenu(workbench).wait();
41+
}
42+
}
43+
}

packages/page-objects/src/components/editor/EditorAction.ts

Lines changed: 9 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -15,52 +15,20 @@
1515
* limitations under the License.
1616
*/
1717

18-
import { EditorGroup } from './EditorView';
19-
import { ContextMenu, Key, WebElement } from '../..';
20-
import { ElementWithContextMenu } from '../ElementWithContextMenu';
21-
import { ChromiumWebDriver } from 'selenium-webdriver/chromium';
22-
23-
export class EditorAction extends ElementWithContextMenu {
24-
constructor(element: WebElement, parent: EditorGroup) {
25-
super(element, parent);
26-
}
18+
import { ActionButtonElementDropdown } from '../ActionButtonElementDropdown';
2719

20+
/**
21+
* Base class for editor actions that provides a common method to get the title.
22+
*/
23+
abstract class BaseEditorAction extends ActionButtonElementDropdown {
2824
/**
2925
* Get text description of the action.
3026
*/
3127
async getTitle(): Promise<string> {
32-
return await this.getAttribute(EditorAction.locators.EditorView.attribute);
28+
return await this.getAttribute(BaseEditorAction.locators.EditorView.attribute);
3329
}
3430
}
3531

36-
export class EditorActionDropdown extends EditorAction {
37-
async open(): Promise<ContextMenu> {
38-
await this.click();
39-
const shadowRootHost = await this.enclosingItem.findElements(EditorAction.locators.EditorAction.shadowRootHost);
40-
const actions = this.getDriver().actions();
41-
await actions.clear();
42-
await actions.sendKeys(Key.ESCAPE).perform();
43-
const webdriverCapabilities = await (this.getDriver() as ChromiumWebDriver).getCapabilities();
44-
const chromiumVersion = webdriverCapabilities.getBrowserVersion();
45-
if (shadowRootHost.length > 0) {
46-
if ((await this.getAttribute('aria-expanded')) !== 'true') {
47-
await this.click();
48-
}
49-
let shadowRoot;
50-
const webdriverCapabilities = await (this.getDriver() as ChromiumWebDriver).getCapabilities();
51-
const chromiumVersion = webdriverCapabilities.getBrowserVersion();
52-
if (chromiumVersion && parseInt(chromiumVersion.split('.')[0]) >= 96) {
53-
shadowRoot = await shadowRootHost[0].getShadowRoot();
54-
return new ContextMenu(await shadowRoot.findElement(EditorAction.locators.EditorAction.monacoMenuContainer)).wait();
55-
} else {
56-
shadowRoot = (await this.getDriver().executeScript('return arguments[0].shadowRoot', shadowRootHost[0])) as WebElement;
57-
return new ContextMenu(shadowRoot).wait();
58-
}
59-
} else if (chromiumVersion && parseInt(chromiumVersion.split('.')[0]) >= 100) {
60-
await this.click();
61-
const workbench = await this.getDriver().findElement(ElementWithContextMenu.locators.Workbench.constructor);
62-
return new ContextMenu(workbench).wait();
63-
}
64-
return await super.openContextMenu();
65-
}
66-
}
32+
export class EditorAction extends BaseEditorAction {}
33+
34+
export class EditorActionDropdown extends BaseEditorAction {}

packages/page-objects/src/components/sidebar/ViewSection.ts

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { ContextMenu, ViewContent, ViewItem, waitForAttributeValue, WelcomeConte
2020
import { AbstractElement } from '../AbstractElement';
2121
import { ElementWithContextMenu } from '../ElementWithContextMenu';
2222
import { ChromiumWebDriver } from 'selenium-webdriver/chromium';
23+
import { ActionButtonElementDropdown } from '../ActionButtonElementDropdown';
2324

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

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

@@ -140,18 +143,17 @@ export abstract class ViewSection extends AbstractElement {
140143
* @returns Promise resolving to array of ViewPanelAction objects
141144
*/
142145
async getActions(): Promise<ViewPanelAction[]> {
143-
const actions: ViewPanelAction[] = [];
144-
145-
if (!(await this.isHeaderHidden())) {
146-
const header = await this.findElement(ViewSection.locators.ViewSection.header);
147-
const act = await header.findElement(ViewSection.locators.ViewSection.actions);
148-
const elements = await act.findElements(ViewSection.locators.ViewSection.button);
149-
150-
for (const element of elements) {
151-
actions.push(await new ViewPanelAction(element, this).wait());
152-
}
153-
}
154-
return actions;
146+
const actions = await this.findElement(ViewSection.locators.ViewSection.actions).findElements(ViewSection.locators.ViewSection.button);
147+
return Promise.all(
148+
actions.map(async (action) => {
149+
const dropdown = await action.getAttribute('aria-haspopup');
150+
if (dropdown) {
151+
return new ViewPanelActionDropdown(action, this);
152+
} else {
153+
return new ViewPanelAction(action, this);
154+
}
155+
}),
156+
);
155157
}
156158

157159
/**
@@ -208,9 +210,10 @@ export abstract class ViewSection extends AbstractElement {
208210
}
209211

210212
/**
211-
* Action button on the header of a view section
213+
* Base class for action buttons on view sections.
214+
* Provides shared functionality for both standard and dropdown actions.
212215
*/
213-
export class ViewPanelAction extends AbstractElement {
216+
abstract class BaseViewPanelAction extends ActionButtonElementDropdown {
214217
constructor(element: WebElement, viewPart: ViewSection) {
215218
super(element, viewPart);
216219
}
@@ -219,11 +222,19 @@ export class ViewPanelAction extends AbstractElement {
219222
* Get label of the action button
220223
*/
221224
async getLabel(): Promise<string> {
222-
return await this.getAttribute(ViewSection.locators.ViewSection.buttonLabel);
225+
return await this.getAttribute(BaseViewPanelAction.locators.ViewSection.buttonLabel);
223226
}
224227

225-
async wait(timeout: number = 1000): Promise<this> {
226-
await this.getDriver().wait(until.elementLocated(ViewSection.locators.ViewSection.actions), timeout);
228+
/**
229+
* Wait for the action button to be located within a given timeout.
230+
* @param timeout Time in milliseconds (default: 1000ms)
231+
*/
232+
async wait(timeout: number = 1_000): Promise<this> {
233+
await this.getDriver().wait(until.elementLocated(BaseViewPanelAction.locators.ViewSection.actions), timeout);
227234
return this;
228235
}
229236
}
237+
238+
export class ViewPanelAction extends BaseViewPanelAction {}
239+
240+
export class ViewPanelActionDropdown extends BaseViewPanelAction {}

tests/test-project/package.json

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,7 @@
103103
{
104104
"command": "testView.refresh",
105105
"title": "Refresh",
106-
"icon": {
107-
"light": "resources/icons/light/refresh.svg",
108-
"dark": "resources/icons/dark/refresh.svg"
109-
}
106+
"icon": "$(refresh)"
110107
}
111108
],
112109
"viewsContainers": {
@@ -202,10 +199,31 @@
202199
"view/title": [
203200
{
204201
"command": "testView.refresh",
205-
"group": "navigation"
202+
"group": "navigation@2",
203+
"when": "view == testView || view == testView2"
204+
},
205+
{
206+
"submenu": "extester.menu.test",
207+
"group": "navigation@1",
208+
"when": "view == testView"
209+
}
210+
],
211+
"extester.menu.test": [
212+
{
213+
"command": "extension.helloWorld"
214+
},
215+
{
216+
"command": "extension.helloWorld2"
206217
}
207218
]
208-
}
219+
},
220+
"submenus": [
221+
{
222+
"id": "extester.menu.test",
223+
"label": "Hello Who...",
224+
"icon": "$(rocket)"
225+
}
226+
]
209227
},
210228
"scripts": {
211229
"vscode:prepublish": "npm run build",

tests/test-project/src/test/xsideBar/customView.test.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ import {
2626
ViewContent,
2727
ViewControl,
2828
ViewItem,
29+
ViewPanelAction,
30+
ViewPanelActionDropdown,
2931
WelcomeContentButton,
3032
WelcomeContentSection,
3133
Workbench,
@@ -48,7 +50,6 @@ describe('CustomTreeSection', () => {
4850
emptySection = await content.getSection('Test View 2');
4951
await emptySection.expand();
5052
emptyViewSection = await content.getSection('Empty View');
51-
await emptyViewSection.expand();
5253
});
5354

5455
after(async () => {
@@ -113,18 +114,30 @@ describe('CustomTreeSection', () => {
113114
});
114115

115116
it('getAction works', async () => {
116-
const action = await section.getAction('Collapse All');
117+
const action = (await section.getAction('Collapse All')) as ViewPanelAction;
117118
expect(await action?.getLabel()).equals('Collapse All');
118119
});
119120

120121
it('getAction of EMPTY works', async () => {
121-
const action = await emptySection.getAction('Refresh');
122+
const action = (await emptySection.getAction('Refresh')) as ViewPanelAction;
122123
expect(await action?.getLabel()).equals('Refresh');
123124
await emptySection.collapse();
124125
});
125126

127+
(process.platform === 'darwin' ? it.skip : it)('getAction dropdown works', async () => {
128+
const action = (await section.getAction('Hello Who...')) as ViewPanelActionDropdown;
129+
const menu = await action.open();
130+
expect(menu).not.undefined;
131+
132+
await menu.select('Hello a World');
133+
const infoMessage = await (await new Workbench().openNotificationsCenter()).getNotifications(NotificationType.Info);
134+
expect(await infoMessage[0].getMessage()).to.equal('Hello World, Test Project!');
135+
await (await new Workbench().openNotificationsCenter()).clearAllNotifications();
136+
});
137+
126138
it('findWelcomeContent returns undefined if no WelcomeContent is present', async () => {
127139
expect(await section.findWelcomeContent()).to.equal(undefined);
140+
await emptyViewSection.expand();
128141
expect(await emptyViewSection.findWelcomeContent()).to.not.equal(undefined);
129142
});
130143

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

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

0 commit comments

Comments
 (0)