Skip to content

Commit 2338445

Browse files
authored
Automate testing an ability to disable "Install from VSIX..." functionality (#23454)
* Fix: add Chrome flags to suppress clipboard permission dialogs Prevents "devspaces.com wants to see text and images copied to the clipboard" popup when opening context menus on project files in VS Code editor during e2e test execution. Signed-off-by: Martin Szuc <[email protected]> * [Test] Add yaml resources for enabling and disabling VSIX extension installation for vscode Signed-off-by: Martin Szuc <[email protected]> * [Test] Add VsixInstallationDisable test Assisted-by: Gemini Signed-off-by: Martin Szuc <[email protected]>
1 parent 5d2b584 commit 2338445

14 files changed

+795
-2
lines changed

tests/e2e/configs/inversify.config.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ import { WebTerminalPage } from '../pageobjects/webterminal/WebTerminalPage';
5656
import { TrustAuthorPopup } from '../pageobjects/dashboard/TrustAuthorPopup';
5757
import { ViewsMoreActionsButton } from '../pageobjects/ide/ViewsMoreActionsButton';
5858
import { RestrictedModeButton } from '../pageobjects/ide/RestrictedModeButton';
59+
import { CommandPalette } from '../pageobjects/ide/CommandPalette';
60+
import { ExtensionsView } from '../pageobjects/ide/ExtensionsView';
61+
import { ExplorerView } from '../pageobjects/ide/ExplorerView';
62+
import { NotificationHandler } from '../pageobjects/ide/NotificationHandler';
5963

6064
const e2eContainer: Container = new Container({ defaultScope: 'Transient', skipBaseClassChecks: true });
6165

@@ -97,6 +101,10 @@ e2eContainer.bind<LocatorLoader>(EXTERNAL_CLASSES.LocatorLoader).to(LocatorLoade
97101
e2eContainer.bind<TrustAuthorPopup>(CLASSES.TrustAuthorPopup).to(TrustAuthorPopup);
98102
e2eContainer.bind<ViewsMoreActionsButton>(CLASSES.ViewsMoreActionsButton).to(ViewsMoreActionsButton);
99103
e2eContainer.bind<RestrictedModeButton>(CLASSES.RestrictedModeButton).to(RestrictedModeButton);
104+
e2eContainer.bind<CommandPalette>(CLASSES.CommandPalette).to(CommandPalette);
105+
e2eContainer.bind<ExtensionsView>(CLASSES.ExtensionsView).to(ExtensionsView);
106+
e2eContainer.bind<ExplorerView>(CLASSES.ExplorerView).to(ExplorerView);
107+
e2eContainer.bind<NotificationHandler>(CLASSES.NotificationHandler).to(NotificationHandler);
100108

101109
if (BASE_TEST_CONSTANTS.TS_PLATFORM === Platform.OPENSHIFT) {
102110
if (OAUTH_CONSTANTS.TS_SELENIUM_VALUE_OPENSHIFT_OAUTH) {

tests/e2e/configs/inversify.types.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,11 @@ const CLASSES: any = {
5353
RevokeOauthPage: 'RevokeOauthPage',
5454
TrustAuthorPopup: 'TrustAuthorPopup',
5555
ViewsMoreActionsButton: 'ViewsMoreActionsButton',
56-
RestrictedModeButton: 'RestrictedModeButton'
56+
RestrictedModeButton: 'RestrictedModeButton',
57+
CommandPalette: 'CommandPalette',
58+
ExtensionsView: 'ExtensionsView',
59+
ExplorerView: 'ExplorerView',
60+
NotificationHandler: 'NotificationHandler'
5761
};
5862

5963
const EXTERNAL_CLASSES: any = {

tests/e2e/constants/TIMEOUT_CONSTANTS.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export const TIMEOUT_CONSTANTS: {
1717
TS_EXPAND_PROJECT_TREE_ITEM_TIMEOUT: number;
1818
TS_FIND_EXTENSION_TEST_TIMEOUT: number;
1919
TS_IDE_LOAD_TIMEOUT: number;
20+
TS_NOTIFICATION_WAIT_TIMEOUT: number;
2021
TS_SELENIUM_CLICK_ON_VISIBLE_ITEM: number;
2122
TS_SELENIUM_DEFAULT_ATTEMPTS: number;
2223
TS_SELENIUM_DEFAULT_POLLING: number;
@@ -111,6 +112,11 @@ export const TIMEOUT_CONSTANTS: {
111112
*/
112113
TS_SELENIUM_CLICK_ON_VISIBLE_ITEM: Number(process.env.TS_SELENIUM_CLICK_ON_VISIBLE_ITEM) || 5_000,
113114

115+
/**
116+
* timeout for waiting for notifications to appear, "20 000" by default.
117+
*/
118+
TS_NOTIFICATION_WAIT_TIMEOUT: Number(process.env.TS_NOTIFICATION_WAIT_TIMEOUT) || 20_000,
119+
114120
// ----------------------------------------- PLUGINS -----------------------------------------
115121

116122
/**

tests/e2e/driver/ChromeDriver.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,11 @@ export class ChromeDriver implements IDriver {
3636
.addArguments('--no-sandbox')
3737
.addArguments('--disable-web-security')
3838
.addArguments('--allow-running-insecure-content')
39-
.addArguments('--ignore-certificate-errors');
39+
.addArguments('--ignore-certificate-errors')
40+
.addArguments('--enable-clipboard-read')
41+
.addArguments('--enable-clipboard-write')
42+
.addArguments('--deny-permission-prompts')
43+
.addArguments('--disable-popup-blocking');
4044

4145
// if 'true' run in 'headless' mode
4246
if (CHROME_DRIVER_CONSTANTS.TS_SELENIUM_HEADLESS) {

tests/e2e/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ export * from './pageobjects/dashboard/workspace-details/WorkspaceDetails';
3030
export * from './pageobjects/dashboard/Workspaces';
3131
export * from './pageobjects/git-providers/OauthPage';
3232
export * from './pageobjects/ide/CheCodeLocatorLoader';
33+
export * from './pageobjects/ide/CommandPalette';
34+
export * from './pageobjects/ide/ExplorerView';
35+
export * from './pageobjects/ide/ExtensionsView';
36+
export * from './pageobjects/ide/NotificationHandler';
3337
export * from './pageobjects/ide/RestrictedModeButton';
3438
export * from './pageobjects/ide/ViewsMoreActionsButton';
3539
export * from './pageobjects/login/interfaces/ICheLoginPage';
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/** *******************************************************************
2+
* copyright (c) 2025 Red Hat, Inc.
3+
*
4+
* This program and the accompanying materials are made
5+
* available under the terms of the Eclipse Public License 2.0
6+
* which is available at https://www.eclipse.org/legal/epl-2.0/
7+
*
8+
* SPDX-License-Identifier: EPL-2.0
9+
**********************************************************************/
10+
import { inject, injectable } from 'inversify';
11+
import { CLASSES } from '../../configs/inversify.types';
12+
import { By, Key, WebElement } from 'selenium-webdriver';
13+
import { DriverHelper } from '../../utils/DriverHelper';
14+
import { Logger } from '../../utils/Logger';
15+
import { TIMEOUT_CONSTANTS } from '../../constants/TIMEOUT_CONSTANTS';
16+
17+
@injectable()
18+
export class CommandPalette {
19+
private static readonly COMMAND_PALETTE_CONTAINER: By = By.css('.quick-input-widget');
20+
private static readonly COMMAND_PALETTE_LIST: By = By.css('#quickInput_list');
21+
private static readonly COMMAND_PALETTE_ITEMS: By = By.css('#quickInput_list [role="option"]');
22+
23+
constructor(
24+
@inject(CLASSES.DriverHelper)
25+
private readonly driverHelper: DriverHelper
26+
) {}
27+
28+
/**
29+
* open the Command Palette using keyboard shortcut
30+
* tries F1 first, then Ctrl+Shift+P if needed
31+
*/
32+
async openCommandPalette(): Promise<void> {
33+
Logger.debug();
34+
35+
await this.driverHelper.getDriver().actions().keyDown(Key.F1).keyUp(Key.F1).perform();
36+
37+
const paletteVisible: boolean = await this.driverHelper.waitVisibilityBoolean(CommandPalette.COMMAND_PALETTE_CONTAINER);
38+
39+
if (!paletteVisible) {
40+
await this.driverHelper
41+
.getDriver()
42+
.actions()
43+
.keyDown(Key.CONTROL)
44+
.keyDown(Key.SHIFT)
45+
.sendKeys('p')
46+
.keyUp(Key.SHIFT)
47+
.keyUp(Key.CONTROL)
48+
.perform();
49+
}
50+
51+
await this.driverHelper.waitVisibility(CommandPalette.COMMAND_PALETTE_LIST);
52+
}
53+
54+
/**
55+
* search for a command in the Command Palette
56+
*
57+
* @param commandText Text to search for
58+
*/
59+
async searchCommand(commandText: string): Promise<void> {
60+
Logger.debug(`"${commandText}"`);
61+
62+
await this.driverHelper.wait(TIMEOUT_CONSTANTS.TS_SELENIUM_DEFAULT_POLLING);
63+
await this.driverHelper.getDriver().actions().sendKeys(commandText).perform();
64+
await this.driverHelper.wait(TIMEOUT_CONSTANTS.TS_SELENIUM_DEFAULT_POLLING);
65+
}
66+
67+
/**
68+
* get all visible commands in the Command Palette
69+
*
70+
* @returns Array of command texts
71+
*/
72+
async getVisibleCommands(): Promise<string[]> {
73+
Logger.debug();
74+
75+
const listVisible: boolean = await this.driverHelper.waitVisibilityBoolean(CommandPalette.COMMAND_PALETTE_LIST);
76+
77+
if (!listVisible) {
78+
return [];
79+
}
80+
81+
await this.driverHelper.wait(TIMEOUT_CONSTANTS.TS_SELENIUM_DEFAULT_POLLING);
82+
const items: WebElement[] = await this.driverHelper.getDriver().findElements(CommandPalette.COMMAND_PALETTE_ITEMS);
83+
const itemTexts: string[] = [];
84+
85+
for (const item of items) {
86+
try {
87+
const ariaLabel: string = await item.getAttribute('aria-label');
88+
if (ariaLabel) {
89+
itemTexts.push(ariaLabel);
90+
}
91+
} catch (err) {
92+
// skip items that cannot be read
93+
}
94+
}
95+
96+
return itemTexts;
97+
}
98+
99+
async isCommandVisible(commandText: string): Promise<boolean> {
100+
Logger.debug(`"${commandText}"`);
101+
await this.driverHelper.wait(TIMEOUT_CONSTANTS.TS_SELENIUM_DEFAULT_POLLING);
102+
103+
const availableCommands: string[] = await this.getVisibleCommands();
104+
Logger.debug(`Available commands: ${availableCommands.join(', ')}`);
105+
return availableCommands.some((command: string): boolean => command.toLowerCase().includes(commandText.toLowerCase()));
106+
}
107+
108+
async closeCommandPalette(): Promise<void> {
109+
Logger.debug();
110+
111+
await this.driverHelper.getDriver().actions().sendKeys(Key.ESCAPE).perform();
112+
await this.driverHelper.waitDisappearance(CommandPalette.COMMAND_PALETTE_CONTAINER);
113+
}
114+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/** *******************************************************************
2+
* copyright (c) 2025 Red Hat, Inc.
3+
*
4+
* This program and the accompanying materials are made
5+
* available under the terms of the Eclipse Public License 2.0
6+
* which is available at https://www.eclipse.org/legal/epl-2.0/
7+
*
8+
* SPDX-License-Identifier: EPL-2.0
9+
**********************************************************************/
10+
import { inject, injectable } from 'inversify';
11+
import { CLASSES } from '../../configs/inversify.types';
12+
import { By, Key } from 'selenium-webdriver';
13+
import { ActivityBar, ViewControl, ViewItem, ViewSection } from 'monaco-page-objects';
14+
import { DriverHelper } from '../../utils/DriverHelper';
15+
import { Logger } from '../../utils/Logger';
16+
import { ProjectAndFileTests } from '../../tests-library/ProjectAndFileTests';
17+
import { TIMEOUT_CONSTANTS } from '../../constants/TIMEOUT_CONSTANTS';
18+
19+
@injectable()
20+
export class ExplorerView {
21+
private static readonly CONTEXT_MENU_CONTAINER: By = By.css('.monaco-menu-container');
22+
private static readonly CONTEXT_MENU_ITEMS: By = By.css('.monaco-menu-container .action-item');
23+
24+
constructor(
25+
@inject(CLASSES.DriverHelper)
26+
private readonly driverHelper: DriverHelper,
27+
@inject(CLASSES.ProjectAndFileTests)
28+
private readonly projectAndFileTests: ProjectAndFileTests
29+
) {}
30+
31+
async openExplorerView(): Promise<void> {
32+
Logger.debug();
33+
34+
const explorerCtrl: ViewControl | undefined = await new ActivityBar().getViewControl('Explorer');
35+
await explorerCtrl?.openView();
36+
}
37+
38+
/**
39+
* open the context menu for a file in the Explorer view
40+
*
41+
* @param fileName Name of the file to open context menu for
42+
*/
43+
async openFileContextMenu(fileName: string): Promise<void> {
44+
Logger.debug(`"${fileName}"`);
45+
46+
await this.openExplorerView();
47+
48+
const projectSection: ViewSection = await this.projectAndFileTests.getProjectViewSession();
49+
const fileItem: ViewItem | undefined = await this.projectAndFileTests.getProjectTreeItem(projectSection, fileName);
50+
51+
if (!fileItem) {
52+
throw new Error(`Could not find ${fileName} file in explorer`);
53+
}
54+
55+
await this.driverHelper.wait(TIMEOUT_CONSTANTS.TS_SELENIUM_DEFAULT_POLLING);
56+
57+
try {
58+
await fileItem.openContextMenu();
59+
await this.waitContextMenuVisible();
60+
} catch (error) {
61+
Logger.error(`Context menu failed for "${fileName}": ${error instanceof Error ? error.message : 'Unknown error'}`);
62+
throw new Error(`Context menu failed to open for "${fileName}"`);
63+
}
64+
}
65+
66+
/**
67+
* check if a specific item is visible in the context menu
68+
*
69+
* @param menuItemAriaLabel Aria label of the menu item to check for
70+
* @returns True if the item is visible
71+
*/
72+
async isContextMenuItemVisible(menuItemAriaLabel: string): Promise<boolean> {
73+
Logger.debug(`"${menuItemAriaLabel}"`);
74+
75+
await this.driverHelper.wait(TIMEOUT_CONSTANTS.TS_SELENIUM_DEFAULT_POLLING);
76+
77+
const contextMenuItemLocator: By = By.css(`.monaco-menu-container [aria-label="${menuItemAriaLabel}"]`);
78+
return await this.driverHelper.waitVisibilityBoolean(contextMenuItemLocator);
79+
}
80+
81+
async closeContextMenu(): Promise<void> {
82+
Logger.debug();
83+
84+
await this.driverHelper.getDriver().actions().sendKeys(Key.ESCAPE).perform();
85+
await this.driverHelper.wait(TIMEOUT_CONSTANTS.TS_SELENIUM_DEFAULT_POLLING);
86+
await this.driverHelper.waitDisappearance(ExplorerView.CONTEXT_MENU_CONTAINER);
87+
}
88+
89+
private async waitContextMenuVisible(): Promise<void> {
90+
Logger.debug();
91+
92+
const containerVisible: boolean = await this.driverHelper.waitVisibilityBoolean(ExplorerView.CONTEXT_MENU_CONTAINER);
93+
94+
if (!containerVisible) {
95+
throw new Error('Context menu container did not appear');
96+
}
97+
98+
await this.driverHelper.wait(TIMEOUT_CONSTANTS.TS_SELENIUM_DEFAULT_POLLING);
99+
100+
const menuItemsVisible: boolean = await this.driverHelper.waitVisibilityBoolean(ExplorerView.CONTEXT_MENU_ITEMS);
101+
102+
if (!menuItemsVisible) {
103+
throw new Error('Context menu items did not load properly');
104+
}
105+
}
106+
}

0 commit comments

Comments
 (0)