diff --git a/tests/e2e/configs/inversify.config.ts b/tests/e2e/configs/inversify.config.ts index a3d7802ce10..82e42aa28fa 100644 --- a/tests/e2e/configs/inversify.config.ts +++ b/tests/e2e/configs/inversify.config.ts @@ -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 }); @@ -97,6 +101,10 @@ e2eContainer.bind(EXTERNAL_CLASSES.LocatorLoader).to(LocatorLoade e2eContainer.bind(CLASSES.TrustAuthorPopup).to(TrustAuthorPopup); e2eContainer.bind(CLASSES.ViewsMoreActionsButton).to(ViewsMoreActionsButton); e2eContainer.bind(CLASSES.RestrictedModeButton).to(RestrictedModeButton); +e2eContainer.bind(CLASSES.CommandPalette).to(CommandPalette); +e2eContainer.bind(CLASSES.ExtensionsView).to(ExtensionsView); +e2eContainer.bind(CLASSES.ExplorerView).to(ExplorerView); +e2eContainer.bind(CLASSES.NotificationHandler).to(NotificationHandler); if (BASE_TEST_CONSTANTS.TS_PLATFORM === Platform.OPENSHIFT) { if (OAUTH_CONSTANTS.TS_SELENIUM_VALUE_OPENSHIFT_OAUTH) { diff --git a/tests/e2e/configs/inversify.types.ts b/tests/e2e/configs/inversify.types.ts index c9134fb6386..5c8cfee429a 100644 --- a/tests/e2e/configs/inversify.types.ts +++ b/tests/e2e/configs/inversify.types.ts @@ -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 = { diff --git a/tests/e2e/driver/ChromeDriver.ts b/tests/e2e/driver/ChromeDriver.ts index bdbd8725307..e3b8f6a50b6 100644 --- a/tests/e2e/driver/ChromeDriver.ts +++ b/tests/e2e/driver/ChromeDriver.ts @@ -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') + .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) { diff --git a/tests/e2e/index.ts b/tests/e2e/index.ts index bdbef7c233e..939498f25ee 100644 --- a/tests/e2e/index.ts +++ b/tests/e2e/index.ts @@ -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'; diff --git a/tests/e2e/pageobjects/ide/CommandPalette.ts b/tests/e2e/pageobjects/ide/CommandPalette.ts new file mode 100644 index 00000000000..acb62a5336f --- /dev/null +++ b/tests/e2e/pageobjects/ide/CommandPalette.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + Logger.debug(); + + await this.driverHelper.getDriver().actions().sendKeys(Key.ESCAPE).perform(); + await this.driverHelper.waitDisappearance(CommandPalette.COMMAND_PALETTE_CONTAINER); + } +} diff --git a/tests/e2e/pageobjects/ide/ExplorerView.ts b/tests/e2e/pageobjects/ide/ExplorerView.ts new file mode 100644 index 00000000000..930dc54a908 --- /dev/null +++ b/tests/e2e/pageobjects/ide/ExplorerView.ts @@ -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 { + Logger.debug(); + + const explorerCtrl: any = await new ActivityBar().getViewControl('Explorer'); + await explorerCtrl?.openView(); + } + + async openFileContextMenu(fileName: string): Promise { + 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 { + 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 { + Logger.debug(); + + await this.driverHelper.getDriver().actions().sendKeys(Key.ESCAPE).perform(); + await this.driverHelper.wait(500); + } + + private async waitContextMenuVisible(): Promise { + 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'); + } + } +} diff --git a/tests/e2e/pageobjects/ide/ExtensionsView.ts b/tests/e2e/pageobjects/ide/ExtensionsView.ts new file mode 100644 index 00000000000..770d0b72946 --- /dev/null +++ b/tests/e2e/pageobjects/ide/ExtensionsView.ts @@ -0,0 +1,155 @@ +/** ******************************************************************* + * 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 { ActivityBar, ExtensionsViewItem, ExtensionsViewSection, SideBarView } from 'monaco-page-objects'; +import { DriverHelper } from '../../utils/DriverHelper'; +import { Logger } from '../../utils/Logger'; +import { ViewsMoreActionsButton } from './ViewsMoreActionsButton'; + +@injectable() +export class ExtensionsView { + private static readonly EXTENSIONS_VIEW_SELECTOR: By = By.css('.extensions-viewlet'); + private static readonly MENU_ITEM: By = By.css('.context-view .monaco-menu .action-item'); + + constructor( + @inject(CLASSES.DriverHelper) + private readonly driverHelper: DriverHelper, + @inject(CLASSES.ViewsMoreActionsButton) + private readonly viewsMoreActionsButton: ViewsMoreActionsButton + ) {} + + async openExtensionsView(): Promise { + Logger.debug(); + + const viewCtrl: any = await new ActivityBar().getViewControl('Extensions'); + await viewCtrl?.openView(); + + const extensionsViewVisible: boolean = await this.driverHelper.waitVisibilityBoolean( + ExtensionsView.EXTENSIONS_VIEW_SELECTOR, + 5, + 1000 + ); + + if (!extensionsViewVisible) { + throw new Error('Extensions view could not be opened'); + } + } + + async openMoreActionsMenu(): Promise { + Logger.debug(); + + await this.viewsMoreActionsButton.clickViewsMoreActionsButton(); + await this.viewsMoreActionsButton.waitForContextMenu(); + } + + async getMoreActionsMenuItems(): Promise { + Logger.debug(); + + const menuItems: WebElement[] = await this.driverHelper.getDriver().findElements(ExtensionsView.MENU_ITEM); + const menuTexts: string[] = []; + + for (const item of menuItems) { + try { + const text: string = await item.getText(); + menuTexts.push(text); + } catch (err) { + // skip items that cannot be read + } + } + + return menuTexts; + } + + async isMoreActionsMenuItemVisible(menuItemText: string): Promise { + Logger.debug(`"${menuItemText}"`); + + const menuItems: string[] = await this.getMoreActionsMenuItems(); + return menuItems.some((item: string): boolean => item.includes(menuItemText)); + } + + async closeMoreActionsMenu(): Promise { + Logger.debug(); + + await this.driverHelper.getDriver().actions().sendKeys(Key.ESCAPE).perform(); + } + + /** + * get installed extension names using @installed filter + * Reuses pattern from RecommendedExtensions.spec.ts + */ + async getInstalledExtensionNames(): Promise { + Logger.debug(); + + await this.openExtensionsView(); + + const extensionsView: SideBarView | undefined = await (await new ActivityBar().getViewControl('Extensions'))?.openView(); + if (!extensionsView) { + throw new Error('Could not open Extensions view'); + } + + const [extensionSection]: ExtensionsViewSection[] = (await extensionsView.getContent().getSections()) as ExtensionsViewSection[]; + if (!extensionSection) { + throw new Error('Could not find Extensions section'); + } + + // search for installed extensions + await this.searchInExtensions(extensionSection, '@installed'); + + // wait for results to load + await this.driverHelper.wait(2000); + + const installedItems: ExtensionsViewItem[] = await extensionSection.getVisibleItems(); + const extensionNames: string[] = []; + + for (const item of installedItems) { + try { + const title: string = await item.getTitle(); + extensionNames.push(title); + } catch (err) { + // skip items that cannot be read + } + } + + Logger.debug(`Found ${extensionNames.length} installed extensions: ${extensionNames.join(', ')}`); + return extensionNames; + } + + /** + * search in extensions - reused from RecommendedExtensions.spec.ts pattern + */ + private async searchInExtensions(extensionSection: ExtensionsViewSection, searchText: string): Promise { + Logger.debug(`Searching for: "${searchText}"`); + + const enclosingItem: WebElement = extensionSection.getEnclosingElement(); + + try { + const searchField: WebElement = await enclosingItem.findElement(By.css('input.monaco-inputbox-input')); + + // clear existing search + await this.driverHelper.getDriver().actions().click(searchField).perform(); + await this.driverHelper + .getDriver() + .actions() + .keyDown(Key.CONTROL) + .sendKeys('a') + .keyUp(Key.CONTROL) + .sendKeys(Key.DELETE) + .perform(); + + // enter new search text + await this.driverHelper.getDriver().actions().sendKeys(searchText).perform(); + await this.driverHelper.wait(1000); + } catch (err) { + Logger.debug(`Could not interact with search field: ${err}`); + } + } +} diff --git a/tests/e2e/pageobjects/ide/NotificationHandler.ts b/tests/e2e/pageobjects/ide/NotificationHandler.ts new file mode 100644 index 00000000000..8891e3f2f3f --- /dev/null +++ b/tests/e2e/pageobjects/ide/NotificationHandler.ts @@ -0,0 +1,90 @@ +/** ******************************************************************* + * 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, WebElement } from 'selenium-webdriver'; +import { DriverHelper } from '../../utils/DriverHelper'; +import { Logger } from '../../utils/Logger'; + +@injectable() +export class NotificationHandler { + private static readonly NOTIFICATION_SELECTORS: string[] = [ + '.notification-list-item-message', + '[class*="notification"]', + '.monaco-list-row' + ]; + + constructor( + @inject(CLASSES.DriverHelper) + private readonly driverHelper: DriverHelper + ) {} + + /** + * check for notifications containing specific text + */ + async checkForNotification(expectedText: string, timeoutSeconds: number = 20): Promise { + Logger.info(`Checking for notification containing: "${expectedText}"`); + + // check existing notifications first + if (await this.findInExistingNotifications(expectedText)) { + return true; + } + + // wait for new notifications to appear + const maxAttempts: number = timeoutSeconds / 2; + for (let attempt: number = 0; attempt < maxAttempts; attempt++) { + if (await this.findInExistingNotifications(expectedText)) { + return true; + } + await this.driverHelper.wait(2000); + } + + Logger.debug(`Notification containing "${expectedText}" not found`); + return false; + } + + /** + * check for any of multiple notification texts + */ + async checkForAnyNotification(texts: string[], timeoutSeconds: number = 20): Promise { + for (const text of texts) { + if (await this.checkForNotification(text, timeoutSeconds)) { + return true; + } + } + return false; + } + + /** + * find notification text in existing notifications + */ + private async findInExistingNotifications(expectedText: string): Promise { + for (const selector of NotificationHandler.NOTIFICATION_SELECTORS) { + try { + const elements: WebElement[] = await this.driverHelper.getDriver().findElements(By.css(selector)); + + for (const element of elements) { + try { + const text: string = await element.getText(); + if (text.trim() && text.toLowerCase().includes(expectedText.toLowerCase())) { + Logger.debug(`Found notification: "${text}"`); + return true; + } + } catch (err) { + // continue to next element + } + } + } catch (err) { + // continue to next selector + } + } + return false; + } +} diff --git a/tests/e2e/pageobjects/ide/ViewsMoreActionsButton.ts b/tests/e2e/pageobjects/ide/ViewsMoreActionsButton.ts index 45bdbd18bf7..4e79ca62e77 100644 --- a/tests/e2e/pageobjects/ide/ViewsMoreActionsButton.ts +++ b/tests/e2e/pageobjects/ide/ViewsMoreActionsButton.ts @@ -53,4 +53,15 @@ export class ViewsMoreActionsButton { return viewsActionsButton; } + + async clickViewsMoreActionsButton(): Promise { + Logger.debug(); + await this.driverHelper.waitAndClick(ViewsMoreActionsButton.VIEWS_AND_MORE_ACTIONS_BUTTON); + } + + async waitForContextMenu(): Promise { + const cheCodeLocatorLoader: CheCodeLocatorLoader = e2eContainer.get(CLASSES.CheCodeLocatorLoader); + const webCheCodeLocators: Locators = cheCodeLocatorLoader.webCheCodeLocators; + await this.driverHelper.waitVisibility(webCheCodeLocators.ContextMenu.contextView); + } } diff --git a/tests/e2e/resources/configmap-disable-vsix-installation.yaml b/tests/e2e/resources/configmap-disable-vsix-installation.yaml new file mode 100644 index 00000000000..68d6ca1b831 --- /dev/null +++ b/tests/e2e/resources/configmap-disable-vsix-installation.yaml @@ -0,0 +1,22 @@ +kind: ConfigMap +apiVersion: v1 +metadata: + name: vscode-editor-configurations + namespace: openshift-devspaces + labels: + app.kubernetes.io/part-of: che.eclipse.org + app.kubernetes.io/component: workspaces-config +data: + configurations.json: | + { + "extensions.install-from-vsix-enabled": false + } + settings.json: | + { + "window.header": "VSIX INSTALL = DISABLED", + "window.commandCenter": false, + "workbench.colorCustomizations": { + "titleBar.activeBackground": "#CCA700", + "titleBar.activeForeground": "#ffffff" + } + } diff --git a/tests/e2e/resources/configmap-enable-vsix-installation.yaml b/tests/e2e/resources/configmap-enable-vsix-installation.yaml new file mode 100644 index 00000000000..865388af4b2 --- /dev/null +++ b/tests/e2e/resources/configmap-enable-vsix-installation.yaml @@ -0,0 +1,22 @@ +kind: ConfigMap +apiVersion: v1 +metadata: + name: vscode-editor-configurations + namespace: openshift-devspaces + labels: + app.kubernetes.io/part-of: che.eclipse.org + app.kubernetes.io/component: workspaces-config +data: + configurations.json: | + { + "extensions.install-from-vsix-enabled": true + } + settings.json: | + { + "window.header": "VSIX INSTALL = ENABLED", + "window.commandCenter": false, + "workbench.colorCustomizations": { + "titleBar.activeBackground": "#CCA700", + "titleBar.activeForeground": "#ffffff" + } + } \ No newline at end of file diff --git a/tests/e2e/resources/default-extensions-configmap.yaml b/tests/e2e/resources/default-extensions-configmap.yaml new file mode 100644 index 00000000000..61006bc6a43 --- /dev/null +++ b/tests/e2e/resources/default-extensions-configmap.yaml @@ -0,0 +1,12 @@ +kind: ConfigMap +apiVersion: v1 +metadata: + name: default-extensions + namespace: admin-devspaces + labels: + controller.devfile.io/mount-to-devworkspace: 'true' + controller.devfile.io/watch-configmap: 'true' + annotations: + controller.devfile.io/mount-as: env +data: + DEFAULT_EXTENSIONS: '/projects/web-nodejs-sample/redhat.vscode-yaml-1.17.0.vsix' \ No newline at end of file diff --git a/tests/e2e/specs/miscellaneous/VsixInstallationDisable.spec.ts b/tests/e2e/specs/miscellaneous/VsixInstallationDisable.spec.ts new file mode 100644 index 00000000000..2c7db4f68f7 --- /dev/null +++ b/tests/e2e/specs/miscellaneous/VsixInstallationDisable.spec.ts @@ -0,0 +1,244 @@ +/** ******************************************************************* + * 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 'reflect-metadata'; +import { e2eContainer } from '../../configs/inversify.config'; +import { CLASSES, TYPES } from '../../configs/inversify.types'; +import { expect } from 'chai'; +import * as fs from 'fs'; +import * as path from 'path'; + +import { WorkspaceHandlingTests } from '../../tests-library/WorkspaceHandlingTests'; +import { ProjectAndFileTests } from '../../tests-library/ProjectAndFileTests'; +import { LoginTests } from '../../tests-library/LoginTests'; +import { registerRunningWorkspace } from '../MochaHooks'; + +import { KubernetesCommandLineToolsExecutor } from '../../utils/KubernetesCommandLineToolsExecutor'; +import { ShellExecutor } from '../../utils/ShellExecutor'; +import { ITestWorkspaceUtil } from '../../utils/workspace/ITestWorkspaceUtil'; + +import { Dashboard } from '../../pageobjects/dashboard/Dashboard'; +import { NotificationHandler } from '../../pageobjects/ide/NotificationHandler'; +import { CommandPalette } from '../../pageobjects/ide/CommandPalette'; +import { ExtensionsView } from '../../pageobjects/ide/ExtensionsView'; +import { ExplorerView } from '../../pageobjects/ide/ExplorerView'; + +import { DriverHelper } from '../../utils/DriverHelper'; +import { BrowserTabsUtil } from '../../utils/BrowserTabsUtil'; +import { BASE_TEST_CONSTANTS } from '../../constants/BASE_TEST_CONSTANTS'; +import { Logger } from '../../utils/Logger'; + +suite(`Verify VSIX installation can be disabled via configuration ${BASE_TEST_CONSTANTS.TEST_ENVIRONMENT}`, function (): void { + const workspaceHandlingTests: WorkspaceHandlingTests = e2eContainer.get(CLASSES.WorkspaceHandlingTests); + const projectAndFileTests: ProjectAndFileTests = e2eContainer.get(CLASSES.ProjectAndFileTests); + const loginTests: LoginTests = e2eContainer.get(CLASSES.LoginTests); + const kubernetesCommandLineToolsExecutor: KubernetesCommandLineToolsExecutor = e2eContainer.get( + CLASSES.KubernetesCommandLineToolsExecutor + ); + const shellExecutor: ShellExecutor = e2eContainer.get(CLASSES.ShellExecutor); + const testWorkspaceUtil: ITestWorkspaceUtil = e2eContainer.get(TYPES.WorkspaceUtil); + const dashboard: Dashboard = e2eContainer.get(CLASSES.Dashboard); + const driverHelper: DriverHelper = e2eContainer.get(CLASSES.DriverHelper); + const browserTabsUtil: BrowserTabsUtil = e2eContainer.get(CLASSES.BrowserTabsUtil); + const notificationHandler: NotificationHandler = e2eContainer.get(CLASSES.NotificationHandler); + const commandPalette: CommandPalette = e2eContainer.get(CLASSES.CommandPalette); + const extensionsView: ExtensionsView = e2eContainer.get(CLASSES.ExtensionsView); + const explorerView: ExplorerView = e2eContainer.get(CLASSES.ExplorerView); + + // test configuration + const testRepoUrl: string = 'https://github.com/RomanNikitenko/web-nodejs-sample/tree/install-from-vsix-disabled-7-100'; + const resourcesPath: string = path.join(__dirname, '../../../resources'); + const defaultExtensions: string[] = ['YAML']; + + /** + * verify VSIX installation capability in UI locations + */ + async function verifyVsixInstallationCapability(expectedEnabled: boolean): Promise { + Logger.info(`Verifying VSIX installation capability - expected enabled: ${expectedEnabled}`); + + // check Command Palette + await commandPalette.openCommandPalette(); + await commandPalette.searchCommand('Install from VSIX'); + const commandAvailable: boolean = await commandPalette.isCommandVisible('Extensions: Install from VSIX...'); + await commandPalette.closeCommandPalette(); + + expect(commandAvailable).to.equal( + expectedEnabled, + `Command Palette should ${expectedEnabled ? 'contain' : 'not contain'} Install from VSIX command` + ); + + // check Extensions View More Actions Menu + await extensionsView.openExtensionsView(); + await extensionsView.openMoreActionsMenu(); + const extensionMenuAvailable: boolean = await extensionsView.isMoreActionsMenuItemVisible('Install from VSIX'); + await extensionsView.closeMoreActionsMenu(); + + expect(extensionMenuAvailable).to.equal( + expectedEnabled, + `Extensions view should ${expectedEnabled ? 'contain' : 'not contain'} Install from VSIX action` + ); + + // check Explorer Context Menu + const vsixFileName: string = 'redhat.vscode-yaml-1.17.0.vsix'; + await explorerView.openFileContextMenu(vsixFileName); + const contextMenuAvailable: boolean = await explorerView.isContextMenuItemVisible('Install Extension VSIX'); + await explorerView.closeContextMenu(); + + expect(contextMenuAvailable).to.equal( + expectedEnabled, + `Explorer context menu should ${expectedEnabled ? 'contain' : 'not contain'} Install Extension VSIX action` + ); + } + + let workspaceName: string = ''; + + /** + * verify default extensions installation status + */ + async function verifyDefaultExtensionsInstallation(shouldBeInstalled: boolean): Promise { + Logger.info(`Verifying default VSIX extensions auto-installation - expected installed: ${shouldBeInstalled}`); + + await extensionsView.openExtensionsView(); + const installedExtensions: string[] = await extensionsView.getInstalledExtensionNames(); + + Logger.debug(`Found installed extensions: ${installedExtensions.join(', ')}`); + + for (const extensionName of defaultExtensions) { + const isInstalled: boolean = installedExtensions.some((installed: string): boolean => + installed.toLowerCase().includes(extensionName.toLowerCase()) + ); + + expect(isInstalled).to.equal( + shouldBeInstalled, + `Default VSIX extension "${extensionName}" should ${shouldBeInstalled ? 'be auto-installed' : 'not be auto-installed'}` + ); + } + } + + /** + * apply a ConfigMap from resources folder + */ + function applyConfigMap(configFileName: string): void { + const configPath: string = path.join(resourcesPath, configFileName); + const configContent: string = fs.readFileSync(configPath, 'utf8'); + shellExecutor.executeCommand(`oc apply -f - < { + await loginTests.loginIntoChe(); + }); + + test('Apply ConfigMaps that disable VSIX and set default extensions', function (): void { + applyConfigMap('configmap-disable-vsix-installation.yaml'); + applyConfigMap('default-extensions-configmap.yaml'); + }); + + test('Create and open workspace from Git repository', async function (): Promise { + await workspaceHandlingTests.createAndOpenWorkspaceFromGitRepository(testRepoUrl); + await workspaceHandlingTests.obtainWorkspaceNameFromStartingPage(); + workspaceName = WorkspaceHandlingTests.getWorkspaceName(); + registerRunningWorkspace(workspaceName); + }); + + test('Wait workspace readiness', async function (): Promise { + await projectAndFileTests.waitWorkspaceReadinessForCheCodeEditor(); + }); + + test('Verify for VSIX disabled notifications', async function (): Promise { + const hasDisableNotification: boolean = await notificationHandler.checkForNotification('install from vsix command is disabled'); + expect(hasDisableNotification).to.be.true; + }); + + test('Perform trust author dialog', async function (): Promise { + await projectAndFileTests.performTrustAuthorDialog(); + }); + + test('Verify VSIX installation is disabled', async function (): Promise { + await verifyVsixInstallationCapability(false); + }); + + test('Verify default extensions are not auto-installed when VSIX disabled', async function (): Promise { + await verifyDefaultExtensionsInstallation(false); + }); + + test('Enable VSIX installation and create new workspace', async function (): Promise { + // clean up current workspace + await dashboard.openDashboard(); + await testWorkspaceUtil.stopAndDeleteWorkspaceByName(workspaceName); + registerRunningWorkspace(''); + + // apply new ConfigMaps + applyConfigMap('configmap-enable-vsix-installation.yaml'); + applyConfigMap('default-extensions-configmap.yaml'); + + Logger.info('Waiting for new ConfigMap settings to take effect...'); + await driverHelper.wait(30000); + + // create new workspace + await workspaceHandlingTests.createAndOpenWorkspaceFromGitRepository(testRepoUrl); + await workspaceHandlingTests.obtainWorkspaceNameFromStartingPage(); + workspaceName = WorkspaceHandlingTests.getWorkspaceName(); + registerRunningWorkspace(workspaceName); + }); + + test('Wait workspace readiness', async function (): Promise { + await projectAndFileTests.waitWorkspaceReadinessForCheCodeEditor(); + }); + + test('Verify default extension installation success notifications before trust dialog', async function (): Promise { + const successTexts: string[] = ['Completed installing extension', 'installed', 'extension']; + const hasSuccessNotification: boolean = await notificationHandler.checkForAnyNotification(successTexts); + expect(hasSuccessNotification).to.be.true; + }); + + test('Perform trust author dialog', async function (): Promise { + await projectAndFileTests.performTrustAuthorDialog(); + }); + + test('Verify VSIX installation is enabled', async function (): Promise { + await verifyVsixInstallationCapability(true); + }); + + test('Verify default extensions are auto-installed when VSIX enabled', async function (): Promise { + await verifyDefaultExtensionsInstallation(true); + }); + + suiteTeardown('Clean up ConfigMaps and workspace', async function (): Promise { + cleanupConfigMaps(); + await dashboard.openDashboard(); + await browserTabsUtil.closeAllTabsExceptCurrent(); + await testWorkspaceUtil.stopAndDeleteWorkspaceByName(workspaceName); + registerRunningWorkspace(''); + Logger.info('Cleanup completed'); + }); +});