Skip to content

Automate testing an ability to disable "Install from VSIX..." functionality #23454

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions tests/e2e/configs/inversify.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ import { WebTerminalPage } from '../pageobjects/webterminal/WebTerminalPage';
import { TrustAuthorPopup } from '../pageobjects/dashboard/TrustAuthorPopup';
import { ViewsMoreActionsButton } from '../pageobjects/ide/ViewsMoreActionsButton';
import { RestrictedModeButton } from '../pageobjects/ide/RestrictedModeButton';
import { CommandPalette } from '../pageobjects/ide/CommandPalette';
import { ExtensionsView } from '../pageobjects/ide/ExtensionsView';
import { ExplorerView } from '../pageobjects/ide/ExplorerView';
import { NotificationHandler } from '../pageobjects/ide/NotificationHandler';

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

Expand Down Expand Up @@ -97,6 +101,10 @@ e2eContainer.bind<LocatorLoader>(EXTERNAL_CLASSES.LocatorLoader).to(LocatorLoade
e2eContainer.bind<TrustAuthorPopup>(CLASSES.TrustAuthorPopup).to(TrustAuthorPopup);
e2eContainer.bind<ViewsMoreActionsButton>(CLASSES.ViewsMoreActionsButton).to(ViewsMoreActionsButton);
e2eContainer.bind<RestrictedModeButton>(CLASSES.RestrictedModeButton).to(RestrictedModeButton);
e2eContainer.bind<CommandPalette>(CLASSES.CommandPalette).to(CommandPalette);
e2eContainer.bind<ExtensionsView>(CLASSES.ExtensionsView).to(ExtensionsView);
e2eContainer.bind<ExplorerView>(CLASSES.ExplorerView).to(ExplorerView);
e2eContainer.bind<NotificationHandler>(CLASSES.NotificationHandler).to(NotificationHandler);

if (BASE_TEST_CONSTANTS.TS_PLATFORM === Platform.OPENSHIFT) {
if (OAUTH_CONSTANTS.TS_SELENIUM_VALUE_OPENSHIFT_OAUTH) {
Expand Down
6 changes: 5 additions & 1 deletion tests/e2e/configs/inversify.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,11 @@ const CLASSES: any = {
RevokeOauthPage: 'RevokeOauthPage',
TrustAuthorPopup: 'TrustAuthorPopup',
ViewsMoreActionsButton: 'ViewsMoreActionsButton',
RestrictedModeButton: 'RestrictedModeButton'
RestrictedModeButton: 'RestrictedModeButton',
CommandPalette: 'CommandPalette',
ExtensionsView: 'ExtensionsView',
ExplorerView: 'ExplorerView',
NotificationHandler: 'NotificationHandler'
};

const EXTERNAL_CLASSES: any = {
Expand Down
6 changes: 5 additions & 1 deletion tests/e2e/driver/ChromeDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@ export class ChromeDriver implements IDriver {
.addArguments('--no-sandbox')
.addArguments('--disable-web-security')
.addArguments('--allow-running-insecure-content')
.addArguments('--ignore-certificate-errors');
.addArguments('--ignore-certificate-errors')
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to add these arguments because of popup:
image

.addArguments('--enable-clipboard-read')
.addArguments('--enable-clipboard-write')
.addArguments('--deny-permission-prompts')
.addArguments('--disable-popup-blocking');

// if 'true' run in 'headless' mode
if (CHROME_DRIVER_CONSTANTS.TS_SELENIUM_HEADLESS) {
Expand Down
4 changes: 4 additions & 0 deletions tests/e2e/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ export * from './pageobjects/dashboard/workspace-details/WorkspaceDetails';
export * from './pageobjects/dashboard/Workspaces';
export * from './pageobjects/git-providers/OauthPage';
export * from './pageobjects/ide/CheCodeLocatorLoader';
export * from './pageobjects/ide/CommandPalette';
export * from './pageobjects/ide/ExplorerView';
export * from './pageobjects/ide/ExtensionsView';
export * from './pageobjects/ide/NotificationHandler';
export * from './pageobjects/ide/RestrictedModeButton';
export * from './pageobjects/ide/ViewsMoreActionsButton';
export * from './pageobjects/login/interfaces/ICheLoginPage';
Expand Down
109 changes: 109 additions & 0 deletions tests/e2e/pageobjects/ide/CommandPalette.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/** *******************************************************************
* copyright (c) 2025 Red Hat, Inc.
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
**********************************************************************/
import { inject, injectable } from 'inversify';
import { CLASSES } from '../../configs/inversify.types';
import { By, Key, WebElement } from 'selenium-webdriver';
import { DriverHelper } from '../../utils/DriverHelper';
import { Logger } from '../../utils/Logger';
import { TIMEOUT_CONSTANTS } from '../../constants/TIMEOUT_CONSTANTS';

@injectable()
export class CommandPalette {
private static readonly COMMAND_PALETTE_CONTAINER: By = By.css('.quick-input-widget');
private static readonly COMMAND_PALETTE_LIST: By = By.css('#quickInput_list');
private static readonly COMMAND_PALETTE_ITEMS: By = By.css('#quickInput_list [role="option"]');

constructor(
@inject(CLASSES.DriverHelper)
private readonly driverHelper: DriverHelper
) {}

async openCommandPalette(): Promise<void> {
Logger.debug();

await this.driverHelper.getDriver().actions().keyDown(Key.F1).keyUp(Key.F1).perform();

const paletteVisible: boolean = await this.driverHelper.waitVisibilityBoolean(
CommandPalette.COMMAND_PALETTE_CONTAINER,
5,
TIMEOUT_CONSTANTS.TS_SELENIUM_DEFAULT_POLLING
);

if (!paletteVisible) {
await this.driverHelper
.getDriver()
.actions()
.keyDown(Key.CONTROL)
.keyDown(Key.SHIFT)
.sendKeys('p')
.keyUp(Key.SHIFT)
.keyUp(Key.CONTROL)
.perform();
}

await this.driverHelper.waitVisibility(CommandPalette.COMMAND_PALETTE_LIST);
}

async searchCommand(commandText: string): Promise<void> {
Logger.debug(`"${commandText}"`);

// add pause before typing to prevent issues with fast typing
await this.driverHelper.wait(2000);
await this.driverHelper.getDriver().actions().sendKeys(commandText).perform();
await this.driverHelper.wait(1000);
}

async getVisibleCommands(): Promise<string[]> {
Logger.debug();

const listVisible: boolean = await this.driverHelper.waitVisibilityBoolean(
CommandPalette.COMMAND_PALETTE_LIST,
10,
TIMEOUT_CONSTANTS.TS_SELENIUM_DEFAULT_POLLING
);

if (!listVisible) {
return [];
}

await this.driverHelper.wait(1000);
const items: WebElement[] = await this.driverHelper.getDriver().findElements(CommandPalette.COMMAND_PALETTE_ITEMS);
const itemTexts: string[] = [];

for (const item of items) {
try {
const ariaLabel: string = await item.getAttribute('aria-label');
if (ariaLabel) {
itemTexts.push(ariaLabel);
}
} catch (err) {
// skip items that cannot be read
}
}

return itemTexts;
}

async isCommandVisible(commandText: string): Promise<boolean> {
Logger.debug(`"${commandText}"`);
await this.driverHelper.wait(3000);

const availableCommands: string[] = await this.getVisibleCommands();
Logger.debug(`Available commands: ${availableCommands.join(', ')}`);
return availableCommands.some((command: string): boolean => command.toLowerCase().includes(commandText.toLowerCase()));
}

async closeCommandPalette(): Promise<void> {
Logger.debug();

await this.driverHelper.getDriver().actions().sendKeys(Key.ESCAPE).perform();
await this.driverHelper.waitDisappearance(CommandPalette.COMMAND_PALETTE_CONTAINER);
}
}
95 changes: 95 additions & 0 deletions tests/e2e/pageobjects/ide/ExplorerView.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/** *******************************************************************
* copyright (c) 2025 Red Hat, Inc.
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
**********************************************************************/
import { inject, injectable } from 'inversify';
import { CLASSES } from '../../configs/inversify.types';
import { By, Key } from 'selenium-webdriver';
import { ActivityBar, ViewItem, ViewSection } from 'monaco-page-objects';
import { DriverHelper } from '../../utils/DriverHelper';
import { Logger } from '../../utils/Logger';
import { CheCodeLocatorLoader } from './CheCodeLocatorLoader';
import { ProjectAndFileTests } from '../../tests-library/ProjectAndFileTests';

@injectable()
export class ExplorerView {
constructor(
@inject(CLASSES.DriverHelper)
private readonly driverHelper: DriverHelper,
@inject(CLASSES.CheCodeLocatorLoader)
private readonly cheCodeLocatorLoader: CheCodeLocatorLoader,
@inject(CLASSES.ProjectAndFileTests)
private readonly projectAndFileTests: ProjectAndFileTests
) {}

async openExplorerView(): Promise<void> {
Logger.debug();

const explorerCtrl: any = await new ActivityBar().getViewControl('Explorer');
await explorerCtrl?.openView();
}

async openFileContextMenu(fileName: string): Promise<void> {
Logger.debug(`"${fileName}"`);

await this.openExplorerView();

const projectSection: ViewSection = await this.projectAndFileTests.getProjectViewSession();
const fileItem: ViewItem | undefined = await this.projectAndFileTests.getProjectTreeItem(projectSection, fileName);

if (!fileItem) {
throw new Error(`Could not find ${fileName} file in explorer`);
}

await fileItem.openContextMenu();
await this.waitContextMenuVisible();
}

async isContextMenuItemVisible(menuItemAriaLabel: string): Promise<boolean> {
Logger.debug(`"${menuItemAriaLabel}"`);

// add a small delay to ensure menu is stable
await this.driverHelper.wait(500);

const contextMenuItemLocator: By = By.css(`.monaco-menu-container [aria-label="${menuItemAriaLabel}"]`);
return await this.driverHelper.waitVisibilityBoolean(contextMenuItemLocator, 5, 1000);
}

async closeContextMenu(): Promise<void> {
Logger.debug();

await this.driverHelper.getDriver().actions().sendKeys(Key.ESCAPE).perform();
await this.driverHelper.wait(500);
}

private async waitContextMenuVisible(): Promise<void> {
Logger.debug();

// wait for container with increased timeout
const containerVisible: boolean = await this.driverHelper.waitVisibilityBoolean(
this.cheCodeLocatorLoader.webCheCodeLocators.ContextMenu.contextView,
10,
1000
);

if (!containerVisible) {
throw new Error('Context menu container did not appear');
}

// brief pause to allow menu items to render
await this.driverHelper.wait(800);

// verify menu items are present
const menuItemsLocator: By = By.css('.monaco-menu-container .action-item');
const menuItemsVisible: boolean = await this.driverHelper.waitVisibilityBoolean(menuItemsLocator, 5, 1000);

if (!menuItemsVisible) {
throw new Error('Context menu items did not load properly');
}
}
}
Loading