From cd1b0aa7cb2fc41d9cc06b37a6c641b3334e8a82 Mon Sep 17 00:00:00 2001 From: Oleksii Korniienko Date: Mon, 10 Aug 2026 13:05:38 +0200 Subject: [PATCH 1/4] Added RestartFromLocalDevfile test Assisted-by: Claude Code Signed-off-by: Oleksii Korniienko --- .../api/RestartFromLocalDevfileAPI.spec.ts | 413 ++++++++++++++++++ 1 file changed, 413 insertions(+) create mode 100644 tests/e2e/specs/api/RestartFromLocalDevfileAPI.spec.ts diff --git a/tests/e2e/specs/api/RestartFromLocalDevfileAPI.spec.ts b/tests/e2e/specs/api/RestartFromLocalDevfileAPI.spec.ts new file mode 100644 index 00000000000..576878147da --- /dev/null +++ b/tests/e2e/specs/api/RestartFromLocalDevfileAPI.spec.ts @@ -0,0 +1,413 @@ +/** ******************************************************************* + * copyright (c) 2026 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 { ContainerTerminal, KubernetesCommandLineToolsExecutor } from '../../utils/KubernetesCommandLineToolsExecutor'; +import { expect } from 'chai'; +import { ShellString } from 'shelljs'; +import { BASE_TEST_CONSTANTS } from '../../constants/BASE_TEST_CONSTANTS'; +import { e2eContainer } from '../../configs/inversify.config'; +import { CLASSES } from '../../configs/inversify.types'; +import { ShellExecutor } from '../../utils/ShellExecutor'; +import { Logger } from '../../utils/Logger'; +import { LoginTests } from '../../tests-library/LoginTests'; +import { WorkspaceHandlingTests } from '../../tests-library/WorkspaceHandlingTests'; +import { Dashboard } from '../../pageobjects/dashboard/Dashboard'; +import { Workspaces } from '../../pageobjects/dashboard/Workspaces'; +import { WorkspaceDetails } from '../../pageobjects/dashboard/workspace-details/WorkspaceDetails'; +import { BrowserTabsUtil } from '../../utils/BrowserTabsUtil'; +import { CheCodeLocatorLoader } from '../../pageobjects/ide/CheCodeLocatorLoader'; +import { Locators, Key, WebElement } from 'monaco-page-objects'; +import { DriverHelper } from '../../utils/DriverHelper'; +import { NotificationHandler } from '../../pageobjects/ide/NotificationHandler'; +import { ProjectAndFileTests } from '../../tests-library/ProjectAndFileTests'; +import { By } from 'selenium-webdriver'; + +async function restartFromLocalDevfile(driverHelper: DriverHelper, projectName: string): Promise { + await driverHelper.getDriver().actions().keyDown(Key.CONTROL).sendKeys('p').keyUp(Key.CONTROL).perform(); + await driverHelper.wait(1000); + + await driverHelper.getDriver().actions().sendKeys('>Dev Spaces: Restart Workspace from Local Devfile').perform(); + await driverHelper.wait(1000); + await driverHelper.getDriver().actions().sendKeys(Key.ENTER).perform(); + await driverHelper.wait(1000); + + await driverHelper.getDriver().actions().sendKeys(`/projects/${projectName}/devfile.yaml`).perform(); + await driverHelper.wait(1000); + await driverHelper.getDriver().actions().sendKeys(Key.ENTER).perform(); + await driverHelper.wait(1000); + + await driverHelper.waitAndClick(By.xpath('//div[@class="dialog-buttons"]//a[text()="Restart"]')); +} + + suite(`Test case with empty workspace (per-user storage) ${BASE_TEST_CONSTANTS.TEST_ENVIRONMENT}`, function (): void { + const loginTests: LoginTests = e2eContainer.get(CLASSES.LoginTests); + const workspaceHandlingTests: WorkspaceHandlingTests = e2eContainer.get(CLASSES.WorkspaceHandlingTests); + const dashboard: Dashboard = e2eContainer.get(CLASSES.Dashboard); + const browserTabsUtil: BrowserTabsUtil = e2eContainer.get(CLASSES.BrowserTabsUtil); + const cheCodeLocatorLoader: CheCodeLocatorLoader = e2eContainer.get(CLASSES.CheCodeLocatorLoader); + const webCheCodeLocators: Locators = cheCodeLocatorLoader.webCheCodeLocators; + const driverHelper: DriverHelper = e2eContainer.get(CLASSES.DriverHelper); + const notificationHandler: NotificationHandler = e2eContainer.get(CLASSES.NotificationHandler); + const projectAndFileTests: ProjectAndFileTests = e2eContainer.get(CLASSES.ProjectAndFileTests); + const containerTerminal: ContainerTerminal = e2eContainer.get(CLASSES.ContainerTerminal); + const kubernetesCommandLineToolsExecutor: KubernetesCommandLineToolsExecutor = e2eContainer.get( + CLASSES.KubernetesCommandLineToolsExecutor + ); + const shellExecutor: ShellExecutor = e2eContainer.get(CLASSES.ShellExecutor); + const testFileName: string = 'test-new-file.txt'; + const testFileContent: string = 'test file content for restart verification'; + const gitRepository: string = BASE_TEST_CONSTANTS.IS_CLUSTER_DISCONNECTED() + ? 'https://gh.crw-qe.com/test-automation-only/devfile-with-typo' + : 'https://github.com/dmytro-ndp/devfile-with-typo'; + const projectName: string = 'devfile-with-typo'; + let currentTabHandle: string = 'undefined'; + const editorXpath: string = '//*[@id="editor-selector-card-che-incubator/che-code/latest"]'; + const xPathToWaitFor: string = '//*[@id="workbench.parts.sidebar"]'; + const cheClusterName: string = 'devspaces'; + let originalStorageStrategy: string = ''; + + suiteSetup('Login into Che and set per-user storage strategy', async function (): Promise { + kubernetesCommandLineToolsExecutor.loginToOcp(); + shellExecutor.executeCommand('oc project openshift-devspaces'); + + originalStorageStrategy = shellExecutor + .executeCommand(`oc get checluster/${cheClusterName} -o "jsonpath={.spec.devEnvironments.storage.pvcStrategy}"`) + .stdout.trim(); + + if (originalStorageStrategy !== 'per-user') { + const patchResult: ShellString = shellExecutor.executeCommand( + `oc patch checluster ${cheClusterName} --type=merge ` + + '-p \'{"spec":{"devEnvironments":{"storage":{"pvcStrategy":"per-user"}}}}\'' + ); + expect(patchResult.code).to.equal(0, 'Failed to patch CheCluster to set per-user storage strategy'); + } + + await loginTests.loginIntoChe(); + }); + + function clearCurrentTabHandle(): void { + currentTabHandle = 'undefined'; + } + + test('Create Empty workspace', async function (): Promise { + await dashboard.openDashboard(); + currentTabHandle = await browserTabsUtil.getCurrentWindowHandle(); + await dashboard.clickCreateWorkspaceButton(); + await workspaceHandlingTests.createAndOpenWorkspaceWithSpecificEditorAndSample(editorXpath,'Empty Workspace', xPathToWaitFor); + }); + + test('Setup workspace context for API operations', function (): void { + kubernetesCommandLineToolsExecutor.workspaceName = WorkspaceHandlingTests.getWorkspaceName(); + // kubernetesCommandLineToolsExecutor.loginToOcp(); + kubernetesCommandLineToolsExecutor.getPodAndContainerNames(); + }); + + test('Clone devfile-with-typo repository', function (): void { + const cloneOutput: ShellString = containerTerminal.gitClone(gitRepository); + expect(cloneOutput.stdout + cloneOutput.stderr).includes('Cloning'); + }); + + test('Accept the project as a trusted one', async function (): Promise { + await projectAndFileTests.performTrustAuthorDialog(); + }); + + test('Verify cloned project exists', function (): void { + expect(containerTerminal.ls().stdout).includes(projectName); + }); + + test('Create new file in project root', function (): void { + const output: ShellString = containerTerminal.execInContainerCommand( + `echo "${testFileContent}" > /projects/${projectName}/${testFileName}` + ); + expect(output.code).to.equal(0); + }); + + + test('Restart workspace from local devfile with bad image', async function (): Promise { + await restartFromLocalDevfile(driverHelper, projectName); + }); + + test('Wait for workspace start failure and restart', async function (): Promise { + Logger.info('Waiting for workspace to enter Failed phase and restart with default devfile'); + await driverHelper.waitAndClick(By.xpath('//span[text()="Restart with default devfile"]'), 120000); + await driverHelper.waitVisibility(By.xpath(xPathToWaitFor), 180000); + }); + + test('Re-initialize workspace context after restart', function (): void { + kubernetesCommandLineToolsExecutor.getPodAndContainerNames(); + }); + + test('Verify project exists with cloned files after restart', function (): void { + expect(containerTerminal.ls().stdout).includes(projectName); + expect(containerTerminal.ls(`/projects/${projectName}`).stdout).includes('devfile.yaml'); + }); + + test('Verify new file is present in project root after restart', function (): void { + const output: ShellString = containerTerminal.execInContainerCommand(`cat /projects/${projectName}/${testFileName}`); + expect(output.stdout.trim()).to.equal(testFileContent); + }); + + test('Verify devfile schema validation error on invalid env declaration', async function (): Promise { + Logger.info('Opening devfile.yaml via Quick Open (Ctrl+P)'); + const devfileTab: By = By.xpath('//div[contains(@class, "tab") and contains(., "devfile.yaml")]'); + const maxAttempts: number = 3; + for (let attempt: number = 1; attempt <= maxAttempts; attempt++) { + Logger.info(`Attempt ${attempt}/${maxAttempts} to open devfile.yaml`); + await driverHelper.getDriver().actions().keyDown(Key.CONTROL).sendKeys('p').keyUp(Key.CONTROL).perform(); + await driverHelper.wait(1000); + await driverHelper.getDriver().actions().sendKeys(`/projects/${projectName}/devfile.yaml`).perform(); + await driverHelper.wait(1000); + await driverHelper.getDriver().actions().sendKeys(Key.ENTER).perform(); + + if (await driverHelper.waitVisibilityBoolean(devfileTab, 5, 2000)) { + Logger.info('devfile.yaml tab opened successfully'); + break; + } + + if (attempt === maxAttempts) { + throw new Error('Failed to open devfile.yaml in editor after multiple attempts'); + } + + Logger.warn('devfile.yaml tab did not appear, pressing Escape and retrying'); + await driverHelper.getDriver().actions().sendKeys(Key.ESCAPE).perform(); + await driverHelper.wait(1000); + } + + // go to line 9999 to jump to the end of the file + Logger.info('Navigating to end of file and appending invalid env content'); + await driverHelper.getDriver().actions().keyDown(Key.CONTROL).sendKeys('g').keyUp(Key.CONTROL).perform(); + await driverHelper.wait(1000); + await driverHelper.getDriver().actions().sendKeys('9999').perform(); + await driverHelper.getDriver().actions().sendKeys(Key.ENTER).perform(); + await driverHelper.wait(500); + await driverHelper.getDriver().actions().sendKeys(Key.END).perform(); + await driverHelper.getDriver().actions().sendKeys(' env:').perform(); + await driverHelper.getDriver().actions().sendKeys(Key.ESCAPE).perform(); + await driverHelper.wait(300); + await driverHelper.getDriver().actions().sendKeys(Key.ENTER).perform(); + await driverHelper.getDriver().actions().sendKeys(Key.HOME).sendKeys(Key.HOME).perform(); + await driverHelper.getDriver().actions().sendKeys(' - name: test-env').perform(); + await driverHelper.getDriver().actions().sendKeys(Key.ESCAPE).perform(); + await driverHelper.wait(300); + await driverHelper.getDriver().actions().sendKeys(Key.ENTER).perform(); + await driverHelper.getDriver().actions().sendKeys(Key.HOME).sendKeys(Key.HOME).perform(); + await driverHelper.getDriver().actions().sendKeys(' value: true').perform(); + + Logger.info('Saving file with Ctrl+S'); + await driverHelper.getDriver().actions().keyDown(Key.CONTROL).sendKeys('s').keyUp(Key.CONTROL).perform(); + await driverHelper.wait(1000); + + Logger.info('Restart from local devfile'); + await restartFromLocalDevfile(driverHelper, projectName); + + Logger.info('Check error popup'); + const errorDialog: By = By.xpath('//*[@class="dialog-message-text"]'); + const dialogElement: WebElement = await driverHelper.waitVisibility(errorDialog, 40000); + const dialogText: string = await dialogElement.getText(); + Logger.info(`Error dialog title text: ${dialogText}`); + + const errorDetailDialog: By = By.xpath('//*[@class="dialog-message-detail"]'); + const detailElement: WebElement = await driverHelper.waitVisibility(errorDetailDialog, 40000); + const detailText: string = await detailElement.getText(); + Logger.info(`Error dialog detail text: ${detailText}`); + + expect(detailText).to.equal('Devfile schema validation failed. Error: 0: instance.components[0] is not allowed to have the additional property "env"'); + }); + + suiteTeardown('Delete DevWorkspace', async function (): Promise { + Logger.debug('Delete DevWorkspace. After each test.'); + if (currentTabHandle !== 'undefined') { + await browserTabsUtil.switchToWindow(currentTabHandle); + } + + await dashboard.openDashboard(); + await browserTabsUtil.closeAllTabsExceptCurrent(); + + if (WorkspaceHandlingTests.getWorkspaceName() !== 'undefined') { + Logger.debug('Workspace name is defined. Deleting workspace...'); + await dashboard.deleteStoppedWorkspaceByUI(WorkspaceHandlingTests.getWorkspaceName()); + } + + WorkspaceHandlingTests.clearWorkspaceName(); + clearCurrentTabHandle(); + + if (originalStorageStrategy !== '' && originalStorageStrategy !== 'per-user') { + const restoreResult: ShellString = shellExecutor.executeCommand( + `oc patch checluster ${cheClusterName} --type=merge ` + + `-p '{"spec":{"devEnvironments":{"storage":{"pvcStrategy":"${originalStorageStrategy}"}}}}'` + ); + expect(restoreResult.code).to.equal(0, 'Failed to restore CheCluster storage strategy'); + } + }); + }); + + suite(`Test case with pvc-fail workspace (bad image restart) ${BASE_TEST_CONSTANTS.TEST_ENVIRONMENT}`, function (): void { + const loginTests: LoginTests = e2eContainer.get(CLASSES.LoginTests); + const workspaceHandlingTests: WorkspaceHandlingTests = e2eContainer.get(CLASSES.WorkspaceHandlingTests); + const dashboard: Dashboard = e2eContainer.get(CLASSES.Dashboard); + const workspaces: Workspaces = e2eContainer.get(CLASSES.Workspaces); + const workspaceDetails: WorkspaceDetails = e2eContainer.get(CLASSES.WorkspaceDetails); + const browserTabsUtil: BrowserTabsUtil = e2eContainer.get(CLASSES.BrowserTabsUtil); + const driverHelper: DriverHelper = e2eContainer.get(CLASSES.DriverHelper); + const containerTerminal: ContainerTerminal = e2eContainer.get(CLASSES.ContainerTerminal); + const kubernetesCommandLineToolsExecutor: KubernetesCommandLineToolsExecutor = e2eContainer.get( + CLASSES.KubernetesCommandLineToolsExecutor + ); + const projectAndFileTests: ProjectAndFileTests = e2eContainer.get(CLASSES.ProjectAndFileTests); + const shellExecutor: ShellExecutor = e2eContainer.get(CLASSES.ShellExecutor); + const testFileName: string = 'test-new-file.txt'; + const testFileContent: string = 'test file content for pvc-fail restart verification'; + const gitRepository: string = 'https://github.com/cgruver/test-workspace/tree/pvc-fail'; + const projectName: string = 'test-workspace'; + let currentTabHandle: string = 'undefined'; + const xPathToWaitFor: string = '//*[@id="workbench.parts.sidebar"]'; + const editorXpath: string = '//*[@id="editor-selector-card-che-incubator/che-code/latest"]'; + const cheClusterName: string = 'devspaces'; + let originalStorageStrategy: string = ''; + + suiteSetup('Skip suite on disconnected clusters', function (): void { + if (BASE_TEST_CONSTANTS.IS_CLUSTER_DISCONNECTED()) { + Logger.info('Test cluster is disconnected. Skipping pvc-fail workspace suite.'); + this.skip(); + } + }); + + suiteSetup('Login into Che and set per-workspace storage strategy', async function (): Promise { + kubernetesCommandLineToolsExecutor.loginToOcp(); + shellExecutor.executeCommand('oc project openshift-devspaces'); + + originalStorageStrategy = shellExecutor + .executeCommand(`oc get checluster/${cheClusterName} -o "jsonpath={.spec.devEnvironments.storage.pvcStrategy}"`) + .stdout.trim(); + + if (originalStorageStrategy !== 'per-workspace') { + const patchResult: ShellString = shellExecutor.executeCommand( + `oc patch checluster ${cheClusterName} --type=merge ` + + '-p \'{"spec":{"devEnvironments":{"storage":{"pvcStrategy":"per-workspace"}}}}\'' + ); + expect(patchResult.code).to.equal(0, 'Failed to patch CheCluster to set per-workspace storage strategy'); + } + + await loginTests.loginIntoChe(); + }); + + function clearCurrentTabHandle(): void { + currentTabHandle = 'undefined'; + } + + test('Create workspace from test-workspace pvc-fail branch', async function (): Promise { + await dashboard.openDashboard(); + currentTabHandle = await browserTabsUtil.getCurrentWindowHandle(); + await dashboard.clickCreateWorkspaceButton(); + await workspaceHandlingTests.createAndOpenWorkspaceWithSpecificEditorAndGitUrl(editorXpath ,gitRepository, xPathToWaitFor); + }); + + test('Accept the project as a trusted one', async function (): Promise { + await projectAndFileTests.performTrustAuthorDialog(); + }); + + test('Setup workspace context for API operations', function (): void { + kubernetesCommandLineToolsExecutor.workspaceName = WorkspaceHandlingTests.getWorkspaceName(); + // kubernetesCommandLineToolsExecutor.loginToOcp(); + kubernetesCommandLineToolsExecutor.getPodAndContainerNames(); + }); + + test('Create new file in project folder', function (): void { + const output: ShellString = containerTerminal.execInContainerCommand( + `echo "${testFileContent}" > /projects/${projectName}/${testFileName}` + ); + expect(output.code).to.equal(0); + }); + + test('Modify devfile to point to bad image location', function (): void { + Logger.info('Replacing ":latest" with ":badimage" in devfile.yaml using sed'); + const output: ShellString = containerTerminal.execInContainerCommand( + `sed -i 's/:latest/:badimage/g' /projects/${projectName}/devfile.yaml` + ); + expect(output.code).to.equal(0, 'Failed to replace :latest with :badimage in devfile.yaml'); + + const verifyOutput: ShellString = containerTerminal.execInContainerCommand( + `cat /projects/${projectName}/devfile.yaml` + ); + expect(verifyOutput.stdout).to.include(':badimage'); + expect(verifyOutput.stdout).to.not.include(':latest'); + }); + + test('Restart workspace from local devfile with bad image', async function (): Promise { + await restartFromLocalDevfile(driverHelper, projectName); + }); + + test('Wait for workspace start failure and restart with default devfile', async function (): Promise { + Logger.info('Waiting for workspace to enter Failed phase and restart with default devfile'); + await driverHelper.waitAndClick(By.xpath('//span[text()="Restart with default devfile"]'), 120000); + await driverHelper.waitVisibility(By.xpath(xPathToWaitFor), 120000); + }); + + test('Re-initialize workspace context after restart', function (): void { + kubernetesCommandLineToolsExecutor.getPodAndContainerNames(); + }); + + test('Verify project exists after restart with default devfile', function (): void { + expect(containerTerminal.ls().stdout).includes(projectName); + expect(containerTerminal.ls(`/projects/${projectName}`).stdout).includes('devfile.yaml'); + }); + + test('Verify new file is present after restart with default devfile', function (): void { + const output: ShellString = containerTerminal.execInContainerCommand(`cat /projects/${projectName}/${testFileName}`); + expect(output.stdout.trim()).to.equal(testFileContent); + }); + + test('Verify storage type is per-workspace in workspace details', async function (): Promise { + const workspaceName: string = WorkspaceHandlingTests.getWorkspaceName(); + + Logger.info('Opening Workspaces page'); + await dashboard.openDashboard(); + await dashboard.clickWorkspacesButton(); + await workspaces.waitPage(); + + Logger.info(`Opening workspace details for: ${workspaceName}`); + await workspaces.clickWorkspaceListItemLink(workspaceName); + await workspaceDetails.waitWorkspaceTitle(workspaceName); + + Logger.info('Verifying storage type is per-workspace on Overview tab'); + const storageTypeLocator: By = By.xpath( + '//span[@aria-label="More info for storage type"]/ancestor::div[contains(@class, "pf-v6-c-form__group")]' + + '//div[contains(@class, "pf-v6-c-form__group-control")]/span' + ); + const storageTypeValue: string = await driverHelper.waitAndGetText(storageTypeLocator); + expect(storageTypeValue).to.equal('per-workspace'); + }); + + suiteTeardown('Delete DevWorkspace', async function (): Promise { + Logger.debug('Delete DevWorkspace. After each test.'); + if (currentTabHandle !== 'undefined') { + await browserTabsUtil.switchToWindow(currentTabHandle); + } + + await dashboard.openDashboard(); + await browserTabsUtil.closeAllTabsExceptCurrent(); + + if (WorkspaceHandlingTests.getWorkspaceName() !== 'undefined') { + Logger.debug('Workspace name is defined. Deleting workspace...'); + await dashboard.deleteStoppedWorkspaceByUI(WorkspaceHandlingTests.getWorkspaceName()); + } + + WorkspaceHandlingTests.clearWorkspaceName(); + clearCurrentTabHandle(); + + if (originalStorageStrategy !== '' && originalStorageStrategy !== 'per-workspace') { + const restoreResult: ShellString = shellExecutor.executeCommand( + `oc patch checluster ${cheClusterName} --type=merge ` + + `-p '{"spec":{"devEnvironments":{"storage":{"pvcStrategy":"${originalStorageStrategy}"}}}}'` + ); + expect(restoreResult.code).to.equal(0, 'Failed to restore CheCluster storage strategy'); + } + }); + }); From 2337aa2907cc36db37f4703bbfd9dcfd877702eb Mon Sep 17 00:00:00 2001 From: Oleksii Korniienko Date: Mon, 10 Aug 2026 13:07:26 +0200 Subject: [PATCH 2/4] Renamed test case Signed-off-by: Oleksii Korniienko --- ...romLocalDevfileAPI.spec.ts => RestartFromLocalDevfile.spec.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/e2e/specs/api/{RestartFromLocalDevfileAPI.spec.ts => RestartFromLocalDevfile.spec.ts} (100%) diff --git a/tests/e2e/specs/api/RestartFromLocalDevfileAPI.spec.ts b/tests/e2e/specs/api/RestartFromLocalDevfile.spec.ts similarity index 100% rename from tests/e2e/specs/api/RestartFromLocalDevfileAPI.spec.ts rename to tests/e2e/specs/api/RestartFromLocalDevfile.spec.ts From 817ad2e71f84f418621f505472d484e7e072f862 Mon Sep 17 00:00:00 2001 From: Oleksii Korniienko Date: Tue, 25 Aug 2026 15:10:31 +0200 Subject: [PATCH 3/4] Reworked RestartFromLocalDevfile test Assisted-by: Claude Code Signed-off-by: Oleksii Korniienko --- .../specs/api/RestartFromLocalDevfile.spec.ts | 413 ------------------ .../RestartFromLocalDevfile.spec.ts | 409 +++++++++++++++++ 2 files changed, 409 insertions(+), 413 deletions(-) delete mode 100644 tests/e2e/specs/api/RestartFromLocalDevfile.spec.ts create mode 100644 tests/e2e/specs/restart-tests/RestartFromLocalDevfile.spec.ts diff --git a/tests/e2e/specs/api/RestartFromLocalDevfile.spec.ts b/tests/e2e/specs/api/RestartFromLocalDevfile.spec.ts deleted file mode 100644 index 576878147da..00000000000 --- a/tests/e2e/specs/api/RestartFromLocalDevfile.spec.ts +++ /dev/null @@ -1,413 +0,0 @@ -/** ******************************************************************* - * copyright (c) 2026 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 { ContainerTerminal, KubernetesCommandLineToolsExecutor } from '../../utils/KubernetesCommandLineToolsExecutor'; -import { expect } from 'chai'; -import { ShellString } from 'shelljs'; -import { BASE_TEST_CONSTANTS } from '../../constants/BASE_TEST_CONSTANTS'; -import { e2eContainer } from '../../configs/inversify.config'; -import { CLASSES } from '../../configs/inversify.types'; -import { ShellExecutor } from '../../utils/ShellExecutor'; -import { Logger } from '../../utils/Logger'; -import { LoginTests } from '../../tests-library/LoginTests'; -import { WorkspaceHandlingTests } from '../../tests-library/WorkspaceHandlingTests'; -import { Dashboard } from '../../pageobjects/dashboard/Dashboard'; -import { Workspaces } from '../../pageobjects/dashboard/Workspaces'; -import { WorkspaceDetails } from '../../pageobjects/dashboard/workspace-details/WorkspaceDetails'; -import { BrowserTabsUtil } from '../../utils/BrowserTabsUtil'; -import { CheCodeLocatorLoader } from '../../pageobjects/ide/CheCodeLocatorLoader'; -import { Locators, Key, WebElement } from 'monaco-page-objects'; -import { DriverHelper } from '../../utils/DriverHelper'; -import { NotificationHandler } from '../../pageobjects/ide/NotificationHandler'; -import { ProjectAndFileTests } from '../../tests-library/ProjectAndFileTests'; -import { By } from 'selenium-webdriver'; - -async function restartFromLocalDevfile(driverHelper: DriverHelper, projectName: string): Promise { - await driverHelper.getDriver().actions().keyDown(Key.CONTROL).sendKeys('p').keyUp(Key.CONTROL).perform(); - await driverHelper.wait(1000); - - await driverHelper.getDriver().actions().sendKeys('>Dev Spaces: Restart Workspace from Local Devfile').perform(); - await driverHelper.wait(1000); - await driverHelper.getDriver().actions().sendKeys(Key.ENTER).perform(); - await driverHelper.wait(1000); - - await driverHelper.getDriver().actions().sendKeys(`/projects/${projectName}/devfile.yaml`).perform(); - await driverHelper.wait(1000); - await driverHelper.getDriver().actions().sendKeys(Key.ENTER).perform(); - await driverHelper.wait(1000); - - await driverHelper.waitAndClick(By.xpath('//div[@class="dialog-buttons"]//a[text()="Restart"]')); -} - - suite(`Test case with empty workspace (per-user storage) ${BASE_TEST_CONSTANTS.TEST_ENVIRONMENT}`, function (): void { - const loginTests: LoginTests = e2eContainer.get(CLASSES.LoginTests); - const workspaceHandlingTests: WorkspaceHandlingTests = e2eContainer.get(CLASSES.WorkspaceHandlingTests); - const dashboard: Dashboard = e2eContainer.get(CLASSES.Dashboard); - const browserTabsUtil: BrowserTabsUtil = e2eContainer.get(CLASSES.BrowserTabsUtil); - const cheCodeLocatorLoader: CheCodeLocatorLoader = e2eContainer.get(CLASSES.CheCodeLocatorLoader); - const webCheCodeLocators: Locators = cheCodeLocatorLoader.webCheCodeLocators; - const driverHelper: DriverHelper = e2eContainer.get(CLASSES.DriverHelper); - const notificationHandler: NotificationHandler = e2eContainer.get(CLASSES.NotificationHandler); - const projectAndFileTests: ProjectAndFileTests = e2eContainer.get(CLASSES.ProjectAndFileTests); - const containerTerminal: ContainerTerminal = e2eContainer.get(CLASSES.ContainerTerminal); - const kubernetesCommandLineToolsExecutor: KubernetesCommandLineToolsExecutor = e2eContainer.get( - CLASSES.KubernetesCommandLineToolsExecutor - ); - const shellExecutor: ShellExecutor = e2eContainer.get(CLASSES.ShellExecutor); - const testFileName: string = 'test-new-file.txt'; - const testFileContent: string = 'test file content for restart verification'; - const gitRepository: string = BASE_TEST_CONSTANTS.IS_CLUSTER_DISCONNECTED() - ? 'https://gh.crw-qe.com/test-automation-only/devfile-with-typo' - : 'https://github.com/dmytro-ndp/devfile-with-typo'; - const projectName: string = 'devfile-with-typo'; - let currentTabHandle: string = 'undefined'; - const editorXpath: string = '//*[@id="editor-selector-card-che-incubator/che-code/latest"]'; - const xPathToWaitFor: string = '//*[@id="workbench.parts.sidebar"]'; - const cheClusterName: string = 'devspaces'; - let originalStorageStrategy: string = ''; - - suiteSetup('Login into Che and set per-user storage strategy', async function (): Promise { - kubernetesCommandLineToolsExecutor.loginToOcp(); - shellExecutor.executeCommand('oc project openshift-devspaces'); - - originalStorageStrategy = shellExecutor - .executeCommand(`oc get checluster/${cheClusterName} -o "jsonpath={.spec.devEnvironments.storage.pvcStrategy}"`) - .stdout.trim(); - - if (originalStorageStrategy !== 'per-user') { - const patchResult: ShellString = shellExecutor.executeCommand( - `oc patch checluster ${cheClusterName} --type=merge ` + - '-p \'{"spec":{"devEnvironments":{"storage":{"pvcStrategy":"per-user"}}}}\'' - ); - expect(patchResult.code).to.equal(0, 'Failed to patch CheCluster to set per-user storage strategy'); - } - - await loginTests.loginIntoChe(); - }); - - function clearCurrentTabHandle(): void { - currentTabHandle = 'undefined'; - } - - test('Create Empty workspace', async function (): Promise { - await dashboard.openDashboard(); - currentTabHandle = await browserTabsUtil.getCurrentWindowHandle(); - await dashboard.clickCreateWorkspaceButton(); - await workspaceHandlingTests.createAndOpenWorkspaceWithSpecificEditorAndSample(editorXpath,'Empty Workspace', xPathToWaitFor); - }); - - test('Setup workspace context for API operations', function (): void { - kubernetesCommandLineToolsExecutor.workspaceName = WorkspaceHandlingTests.getWorkspaceName(); - // kubernetesCommandLineToolsExecutor.loginToOcp(); - kubernetesCommandLineToolsExecutor.getPodAndContainerNames(); - }); - - test('Clone devfile-with-typo repository', function (): void { - const cloneOutput: ShellString = containerTerminal.gitClone(gitRepository); - expect(cloneOutput.stdout + cloneOutput.stderr).includes('Cloning'); - }); - - test('Accept the project as a trusted one', async function (): Promise { - await projectAndFileTests.performTrustAuthorDialog(); - }); - - test('Verify cloned project exists', function (): void { - expect(containerTerminal.ls().stdout).includes(projectName); - }); - - test('Create new file in project root', function (): void { - const output: ShellString = containerTerminal.execInContainerCommand( - `echo "${testFileContent}" > /projects/${projectName}/${testFileName}` - ); - expect(output.code).to.equal(0); - }); - - - test('Restart workspace from local devfile with bad image', async function (): Promise { - await restartFromLocalDevfile(driverHelper, projectName); - }); - - test('Wait for workspace start failure and restart', async function (): Promise { - Logger.info('Waiting for workspace to enter Failed phase and restart with default devfile'); - await driverHelper.waitAndClick(By.xpath('//span[text()="Restart with default devfile"]'), 120000); - await driverHelper.waitVisibility(By.xpath(xPathToWaitFor), 180000); - }); - - test('Re-initialize workspace context after restart', function (): void { - kubernetesCommandLineToolsExecutor.getPodAndContainerNames(); - }); - - test('Verify project exists with cloned files after restart', function (): void { - expect(containerTerminal.ls().stdout).includes(projectName); - expect(containerTerminal.ls(`/projects/${projectName}`).stdout).includes('devfile.yaml'); - }); - - test('Verify new file is present in project root after restart', function (): void { - const output: ShellString = containerTerminal.execInContainerCommand(`cat /projects/${projectName}/${testFileName}`); - expect(output.stdout.trim()).to.equal(testFileContent); - }); - - test('Verify devfile schema validation error on invalid env declaration', async function (): Promise { - Logger.info('Opening devfile.yaml via Quick Open (Ctrl+P)'); - const devfileTab: By = By.xpath('//div[contains(@class, "tab") and contains(., "devfile.yaml")]'); - const maxAttempts: number = 3; - for (let attempt: number = 1; attempt <= maxAttempts; attempt++) { - Logger.info(`Attempt ${attempt}/${maxAttempts} to open devfile.yaml`); - await driverHelper.getDriver().actions().keyDown(Key.CONTROL).sendKeys('p').keyUp(Key.CONTROL).perform(); - await driverHelper.wait(1000); - await driverHelper.getDriver().actions().sendKeys(`/projects/${projectName}/devfile.yaml`).perform(); - await driverHelper.wait(1000); - await driverHelper.getDriver().actions().sendKeys(Key.ENTER).perform(); - - if (await driverHelper.waitVisibilityBoolean(devfileTab, 5, 2000)) { - Logger.info('devfile.yaml tab opened successfully'); - break; - } - - if (attempt === maxAttempts) { - throw new Error('Failed to open devfile.yaml in editor after multiple attempts'); - } - - Logger.warn('devfile.yaml tab did not appear, pressing Escape and retrying'); - await driverHelper.getDriver().actions().sendKeys(Key.ESCAPE).perform(); - await driverHelper.wait(1000); - } - - // go to line 9999 to jump to the end of the file - Logger.info('Navigating to end of file and appending invalid env content'); - await driverHelper.getDriver().actions().keyDown(Key.CONTROL).sendKeys('g').keyUp(Key.CONTROL).perform(); - await driverHelper.wait(1000); - await driverHelper.getDriver().actions().sendKeys('9999').perform(); - await driverHelper.getDriver().actions().sendKeys(Key.ENTER).perform(); - await driverHelper.wait(500); - await driverHelper.getDriver().actions().sendKeys(Key.END).perform(); - await driverHelper.getDriver().actions().sendKeys(' env:').perform(); - await driverHelper.getDriver().actions().sendKeys(Key.ESCAPE).perform(); - await driverHelper.wait(300); - await driverHelper.getDriver().actions().sendKeys(Key.ENTER).perform(); - await driverHelper.getDriver().actions().sendKeys(Key.HOME).sendKeys(Key.HOME).perform(); - await driverHelper.getDriver().actions().sendKeys(' - name: test-env').perform(); - await driverHelper.getDriver().actions().sendKeys(Key.ESCAPE).perform(); - await driverHelper.wait(300); - await driverHelper.getDriver().actions().sendKeys(Key.ENTER).perform(); - await driverHelper.getDriver().actions().sendKeys(Key.HOME).sendKeys(Key.HOME).perform(); - await driverHelper.getDriver().actions().sendKeys(' value: true').perform(); - - Logger.info('Saving file with Ctrl+S'); - await driverHelper.getDriver().actions().keyDown(Key.CONTROL).sendKeys('s').keyUp(Key.CONTROL).perform(); - await driverHelper.wait(1000); - - Logger.info('Restart from local devfile'); - await restartFromLocalDevfile(driverHelper, projectName); - - Logger.info('Check error popup'); - const errorDialog: By = By.xpath('//*[@class="dialog-message-text"]'); - const dialogElement: WebElement = await driverHelper.waitVisibility(errorDialog, 40000); - const dialogText: string = await dialogElement.getText(); - Logger.info(`Error dialog title text: ${dialogText}`); - - const errorDetailDialog: By = By.xpath('//*[@class="dialog-message-detail"]'); - const detailElement: WebElement = await driverHelper.waitVisibility(errorDetailDialog, 40000); - const detailText: string = await detailElement.getText(); - Logger.info(`Error dialog detail text: ${detailText}`); - - expect(detailText).to.equal('Devfile schema validation failed. Error: 0: instance.components[0] is not allowed to have the additional property "env"'); - }); - - suiteTeardown('Delete DevWorkspace', async function (): Promise { - Logger.debug('Delete DevWorkspace. After each test.'); - if (currentTabHandle !== 'undefined') { - await browserTabsUtil.switchToWindow(currentTabHandle); - } - - await dashboard.openDashboard(); - await browserTabsUtil.closeAllTabsExceptCurrent(); - - if (WorkspaceHandlingTests.getWorkspaceName() !== 'undefined') { - Logger.debug('Workspace name is defined. Deleting workspace...'); - await dashboard.deleteStoppedWorkspaceByUI(WorkspaceHandlingTests.getWorkspaceName()); - } - - WorkspaceHandlingTests.clearWorkspaceName(); - clearCurrentTabHandle(); - - if (originalStorageStrategy !== '' && originalStorageStrategy !== 'per-user') { - const restoreResult: ShellString = shellExecutor.executeCommand( - `oc patch checluster ${cheClusterName} --type=merge ` + - `-p '{"spec":{"devEnvironments":{"storage":{"pvcStrategy":"${originalStorageStrategy}"}}}}'` - ); - expect(restoreResult.code).to.equal(0, 'Failed to restore CheCluster storage strategy'); - } - }); - }); - - suite(`Test case with pvc-fail workspace (bad image restart) ${BASE_TEST_CONSTANTS.TEST_ENVIRONMENT}`, function (): void { - const loginTests: LoginTests = e2eContainer.get(CLASSES.LoginTests); - const workspaceHandlingTests: WorkspaceHandlingTests = e2eContainer.get(CLASSES.WorkspaceHandlingTests); - const dashboard: Dashboard = e2eContainer.get(CLASSES.Dashboard); - const workspaces: Workspaces = e2eContainer.get(CLASSES.Workspaces); - const workspaceDetails: WorkspaceDetails = e2eContainer.get(CLASSES.WorkspaceDetails); - const browserTabsUtil: BrowserTabsUtil = e2eContainer.get(CLASSES.BrowserTabsUtil); - const driverHelper: DriverHelper = e2eContainer.get(CLASSES.DriverHelper); - const containerTerminal: ContainerTerminal = e2eContainer.get(CLASSES.ContainerTerminal); - const kubernetesCommandLineToolsExecutor: KubernetesCommandLineToolsExecutor = e2eContainer.get( - CLASSES.KubernetesCommandLineToolsExecutor - ); - const projectAndFileTests: ProjectAndFileTests = e2eContainer.get(CLASSES.ProjectAndFileTests); - const shellExecutor: ShellExecutor = e2eContainer.get(CLASSES.ShellExecutor); - const testFileName: string = 'test-new-file.txt'; - const testFileContent: string = 'test file content for pvc-fail restart verification'; - const gitRepository: string = 'https://github.com/cgruver/test-workspace/tree/pvc-fail'; - const projectName: string = 'test-workspace'; - let currentTabHandle: string = 'undefined'; - const xPathToWaitFor: string = '//*[@id="workbench.parts.sidebar"]'; - const editorXpath: string = '//*[@id="editor-selector-card-che-incubator/che-code/latest"]'; - const cheClusterName: string = 'devspaces'; - let originalStorageStrategy: string = ''; - - suiteSetup('Skip suite on disconnected clusters', function (): void { - if (BASE_TEST_CONSTANTS.IS_CLUSTER_DISCONNECTED()) { - Logger.info('Test cluster is disconnected. Skipping pvc-fail workspace suite.'); - this.skip(); - } - }); - - suiteSetup('Login into Che and set per-workspace storage strategy', async function (): Promise { - kubernetesCommandLineToolsExecutor.loginToOcp(); - shellExecutor.executeCommand('oc project openshift-devspaces'); - - originalStorageStrategy = shellExecutor - .executeCommand(`oc get checluster/${cheClusterName} -o "jsonpath={.spec.devEnvironments.storage.pvcStrategy}"`) - .stdout.trim(); - - if (originalStorageStrategy !== 'per-workspace') { - const patchResult: ShellString = shellExecutor.executeCommand( - `oc patch checluster ${cheClusterName} --type=merge ` + - '-p \'{"spec":{"devEnvironments":{"storage":{"pvcStrategy":"per-workspace"}}}}\'' - ); - expect(patchResult.code).to.equal(0, 'Failed to patch CheCluster to set per-workspace storage strategy'); - } - - await loginTests.loginIntoChe(); - }); - - function clearCurrentTabHandle(): void { - currentTabHandle = 'undefined'; - } - - test('Create workspace from test-workspace pvc-fail branch', async function (): Promise { - await dashboard.openDashboard(); - currentTabHandle = await browserTabsUtil.getCurrentWindowHandle(); - await dashboard.clickCreateWorkspaceButton(); - await workspaceHandlingTests.createAndOpenWorkspaceWithSpecificEditorAndGitUrl(editorXpath ,gitRepository, xPathToWaitFor); - }); - - test('Accept the project as a trusted one', async function (): Promise { - await projectAndFileTests.performTrustAuthorDialog(); - }); - - test('Setup workspace context for API operations', function (): void { - kubernetesCommandLineToolsExecutor.workspaceName = WorkspaceHandlingTests.getWorkspaceName(); - // kubernetesCommandLineToolsExecutor.loginToOcp(); - kubernetesCommandLineToolsExecutor.getPodAndContainerNames(); - }); - - test('Create new file in project folder', function (): void { - const output: ShellString = containerTerminal.execInContainerCommand( - `echo "${testFileContent}" > /projects/${projectName}/${testFileName}` - ); - expect(output.code).to.equal(0); - }); - - test('Modify devfile to point to bad image location', function (): void { - Logger.info('Replacing ":latest" with ":badimage" in devfile.yaml using sed'); - const output: ShellString = containerTerminal.execInContainerCommand( - `sed -i 's/:latest/:badimage/g' /projects/${projectName}/devfile.yaml` - ); - expect(output.code).to.equal(0, 'Failed to replace :latest with :badimage in devfile.yaml'); - - const verifyOutput: ShellString = containerTerminal.execInContainerCommand( - `cat /projects/${projectName}/devfile.yaml` - ); - expect(verifyOutput.stdout).to.include(':badimage'); - expect(verifyOutput.stdout).to.not.include(':latest'); - }); - - test('Restart workspace from local devfile with bad image', async function (): Promise { - await restartFromLocalDevfile(driverHelper, projectName); - }); - - test('Wait for workspace start failure and restart with default devfile', async function (): Promise { - Logger.info('Waiting for workspace to enter Failed phase and restart with default devfile'); - await driverHelper.waitAndClick(By.xpath('//span[text()="Restart with default devfile"]'), 120000); - await driverHelper.waitVisibility(By.xpath(xPathToWaitFor), 120000); - }); - - test('Re-initialize workspace context after restart', function (): void { - kubernetesCommandLineToolsExecutor.getPodAndContainerNames(); - }); - - test('Verify project exists after restart with default devfile', function (): void { - expect(containerTerminal.ls().stdout).includes(projectName); - expect(containerTerminal.ls(`/projects/${projectName}`).stdout).includes('devfile.yaml'); - }); - - test('Verify new file is present after restart with default devfile', function (): void { - const output: ShellString = containerTerminal.execInContainerCommand(`cat /projects/${projectName}/${testFileName}`); - expect(output.stdout.trim()).to.equal(testFileContent); - }); - - test('Verify storage type is per-workspace in workspace details', async function (): Promise { - const workspaceName: string = WorkspaceHandlingTests.getWorkspaceName(); - - Logger.info('Opening Workspaces page'); - await dashboard.openDashboard(); - await dashboard.clickWorkspacesButton(); - await workspaces.waitPage(); - - Logger.info(`Opening workspace details for: ${workspaceName}`); - await workspaces.clickWorkspaceListItemLink(workspaceName); - await workspaceDetails.waitWorkspaceTitle(workspaceName); - - Logger.info('Verifying storage type is per-workspace on Overview tab'); - const storageTypeLocator: By = By.xpath( - '//span[@aria-label="More info for storage type"]/ancestor::div[contains(@class, "pf-v6-c-form__group")]' + - '//div[contains(@class, "pf-v6-c-form__group-control")]/span' - ); - const storageTypeValue: string = await driverHelper.waitAndGetText(storageTypeLocator); - expect(storageTypeValue).to.equal('per-workspace'); - }); - - suiteTeardown('Delete DevWorkspace', async function (): Promise { - Logger.debug('Delete DevWorkspace. After each test.'); - if (currentTabHandle !== 'undefined') { - await browserTabsUtil.switchToWindow(currentTabHandle); - } - - await dashboard.openDashboard(); - await browserTabsUtil.closeAllTabsExceptCurrent(); - - if (WorkspaceHandlingTests.getWorkspaceName() !== 'undefined') { - Logger.debug('Workspace name is defined. Deleting workspace...'); - await dashboard.deleteStoppedWorkspaceByUI(WorkspaceHandlingTests.getWorkspaceName()); - } - - WorkspaceHandlingTests.clearWorkspaceName(); - clearCurrentTabHandle(); - - if (originalStorageStrategy !== '' && originalStorageStrategy !== 'per-workspace') { - const restoreResult: ShellString = shellExecutor.executeCommand( - `oc patch checluster ${cheClusterName} --type=merge ` + - `-p '{"spec":{"devEnvironments":{"storage":{"pvcStrategy":"${originalStorageStrategy}"}}}}'` - ); - expect(restoreResult.code).to.equal(0, 'Failed to restore CheCluster storage strategy'); - } - }); - }); diff --git a/tests/e2e/specs/restart-tests/RestartFromLocalDevfile.spec.ts b/tests/e2e/specs/restart-tests/RestartFromLocalDevfile.spec.ts new file mode 100644 index 00000000000..5c3cada3363 --- /dev/null +++ b/tests/e2e/specs/restart-tests/RestartFromLocalDevfile.spec.ts @@ -0,0 +1,409 @@ +/** ******************************************************************* + * copyright (c) 2026 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 { ContainerTerminal, KubernetesCommandLineToolsExecutor } from '../../utils/KubernetesCommandLineToolsExecutor'; +import { expect } from 'chai'; +import { ShellString } from 'shelljs'; +import { BASE_TEST_CONSTANTS } from '../../constants/BASE_TEST_CONSTANTS'; +import { e2eContainer } from '../../configs/inversify.config'; +import { CLASSES } from '../../configs/inversify.types'; +import { ShellExecutor } from '../../utils/ShellExecutor'; +import { Logger } from '../../utils/Logger'; +import { LoginTests } from '../../tests-library/LoginTests'; +import { WorkspaceHandlingTests } from '../../tests-library/WorkspaceHandlingTests'; +import { Dashboard } from '../../pageobjects/dashboard/Dashboard'; +import { Workspaces } from '../../pageobjects/dashboard/Workspaces'; +import { WorkspaceDetails } from '../../pageobjects/dashboard/workspace-details/WorkspaceDetails'; +import { BrowserTabsUtil } from '../../utils/BrowserTabsUtil'; +import { Key, WebElement } from 'monaco-page-objects'; +import { DriverHelper } from '../../utils/DriverHelper'; +import { ProjectAndFileTests } from '../../tests-library/ProjectAndFileTests'; +import { By } from 'selenium-webdriver'; + +async function restartFromLocalDevfile(driverHelper: DriverHelper, projectName: string): Promise { + await driverHelper.getDriver().actions().keyDown(Key.CONTROL).sendKeys('p').keyUp(Key.CONTROL).perform(); + await driverHelper.wait(1000); + + await driverHelper.getDriver().actions().sendKeys('>Dev Spaces: Restart Workspace from Local Devfile').perform(); + await driverHelper.wait(1000); + await driverHelper.getDriver().actions().sendKeys(Key.ENTER).perform(); + await driverHelper.wait(1000); + + await driverHelper.getDriver().actions().sendKeys(`/projects/${projectName}/devfile.yaml`).perform(); + await driverHelper.wait(1000); + await driverHelper.getDriver().actions().sendKeys(Key.ENTER).perform(); + await driverHelper.wait(1000); + + await driverHelper.waitAndClick(By.xpath('//div[@class="dialog-buttons"]//a[text()="Restart"]')); +} + +suite(`Test case with empty workspace (per-user storage) ${BASE_TEST_CONSTANTS.TEST_ENVIRONMENT}`, function (): void { + const loginTests: LoginTests = e2eContainer.get(CLASSES.LoginTests); + const workspaceHandlingTests: WorkspaceHandlingTests = e2eContainer.get(CLASSES.WorkspaceHandlingTests); + const dashboard: Dashboard = e2eContainer.get(CLASSES.Dashboard); + const browserTabsUtil: BrowserTabsUtil = e2eContainer.get(CLASSES.BrowserTabsUtil); + const driverHelper: DriverHelper = e2eContainer.get(CLASSES.DriverHelper); + const projectAndFileTests: ProjectAndFileTests = e2eContainer.get(CLASSES.ProjectAndFileTests); + const containerTerminal: ContainerTerminal = e2eContainer.get(CLASSES.ContainerTerminal); + const kubernetesCommandLineToolsExecutor: KubernetesCommandLineToolsExecutor = e2eContainer.get( + CLASSES.KubernetesCommandLineToolsExecutor + ); + const shellExecutor: ShellExecutor = e2eContainer.get(CLASSES.ShellExecutor); + const testFileName: string = 'test-new-file.txt'; + const testFileContent: string = 'test file content for restart verification'; + const gitRepository: string = BASE_TEST_CONSTANTS.IS_CLUSTER_DISCONNECTED() + ? 'https://gh.crw-qe.com/test-automation-only/devfile-with-typo' + : 'https://github.com/dmytro-ndp/devfile-with-typo'; + const projectName: string = 'devfile-with-typo'; + let currentTabHandle: string = 'undefined'; + const editorXpath: string = '//*[@id="editor-selector-card-che-incubator/che-code/latest"]'; + const xPathToWaitFor: string = '//*[@id="workbench.parts.sidebar"]'; + const cheClusterName: string = 'devspaces'; + let originalStorageStrategy: string = ''; + + suiteSetup('Login into Che and set per-user storage strategy', async function (): Promise { + kubernetesCommandLineToolsExecutor.loginToOcp(); + shellExecutor.executeCommand('oc project openshift-devspaces'); + + originalStorageStrategy = shellExecutor + .executeCommand(`oc get checluster/${cheClusterName} -o "jsonpath={.spec.devEnvironments.storage.pvcStrategy}"`) + .stdout.trim(); + + if (originalStorageStrategy !== 'per-user') { + const patchResult: ShellString = shellExecutor.executeCommand( + `oc patch checluster ${cheClusterName} --type=merge ` + + '-p \'{"spec":{"devEnvironments":{"storage":{"pvcStrategy":"per-user"}}}}\'' + ); + expect(patchResult.code).to.equal(0, 'Failed to patch CheCluster to set per-user storage strategy'); + } + + await loginTests.loginIntoChe(); + }); + + function clearCurrentTabHandle(): void { + currentTabHandle = 'undefined'; + } + + test('Create Empty workspace', async function (): Promise { + await dashboard.openDashboard(); + currentTabHandle = await browserTabsUtil.getCurrentWindowHandle(); + await dashboard.clickCreateWorkspaceButton(); + await workspaceHandlingTests.createAndOpenWorkspaceWithSpecificEditorAndSample(editorXpath, 'Empty Workspace', xPathToWaitFor); + }); + + test('Setup workspace context for API operations', function (): void { + kubernetesCommandLineToolsExecutor.workspaceName = WorkspaceHandlingTests.getWorkspaceName(); + kubernetesCommandLineToolsExecutor.getPodAndContainerNames(); + }); + + test('Clone devfile-with-typo repository', function (): void { + const cloneOutput: ShellString = containerTerminal.gitClone(gitRepository); + expect(cloneOutput.stdout + cloneOutput.stderr).includes('Cloning'); + }); + + test('Accept the project as a trusted one', async function (): Promise { + await projectAndFileTests.performTrustAuthorDialog(); + }); + + test('Verify cloned project exists', function (): void { + expect(containerTerminal.ls().stdout).includes(projectName); + }); + + test('Create new file in project root', function (): void { + const output: ShellString = containerTerminal.execInContainerCommand( + `echo "${testFileContent}" > /projects/${projectName}/${testFileName}` + ); + expect(output.code).to.equal(0); + }); + + test('Restart workspace from local devfile with bad image', async function (): Promise { + await restartFromLocalDevfile(driverHelper, projectName); + Logger.info('Waiting for "Restart Workspace" confirmation popup'); + await driverHelper.waitAndClick(By.xpath('//div[@class="dialog-buttons"]//a[text()="Restart your workspace"]'), 30000); + }); + + test('Wait for workspace start failure and restart', async function (): Promise { + Logger.info('Waiting for workspace to enter Failed phase and restart with default devfile'); + await driverHelper.waitAndClick(By.xpath('//span[text()="Restart with default devfile"]'), 120000); + await driverHelper.waitVisibility(By.xpath(xPathToWaitFor), 180000); + }); + + test('Re-initialize workspace context after restart', function (): void { + kubernetesCommandLineToolsExecutor.getPodAndContainerNames(); + }); + + test('Verify project exists with cloned files after restart', function (): void { + expect(containerTerminal.ls().stdout).includes(projectName); + expect(containerTerminal.ls(`/projects/${projectName}`).stdout).includes('devfile.yaml'); + }); + + test('Verify new file is present in project root after restart', function (): void { + const output: ShellString = containerTerminal.execInContainerCommand(`cat /projects/${projectName}/${testFileName}`); + expect(output.stdout.trim()).to.equal(testFileContent); + }); + + test('Verify devfile schema validation error on invalid env declaration', async function (): Promise { + Logger.info('Opening devfile.yaml via Quick Open (Ctrl+P)'); + const devfileTab: By = By.xpath('//div[contains(@class, "tab") and contains(., "devfile.yaml")]'); + const maxAttempts: number = 3; + for (let attempt: number = 1; attempt <= maxAttempts; attempt++) { + Logger.info(`Attempt ${attempt}/${maxAttempts} to open devfile.yaml`); + await driverHelper.getDriver().actions().keyDown(Key.CONTROL).sendKeys('p').keyUp(Key.CONTROL).perform(); + await driverHelper.wait(1000); + await driverHelper.getDriver().actions().sendKeys(`/projects/${projectName}/devfile.yaml`).perform(); + await driverHelper.wait(1000); + await driverHelper.getDriver().actions().sendKeys(Key.ENTER).perform(); + + if (await driverHelper.waitVisibilityBoolean(devfileTab, 5, 2000)) { + Logger.info('devfile.yaml tab opened successfully'); + break; + } + + if (attempt === maxAttempts) { + throw new Error('Failed to open devfile.yaml in editor after multiple attempts'); + } + + Logger.warn('devfile.yaml tab did not appear, pressing Escape and retrying'); + await driverHelper.getDriver().actions().sendKeys(Key.ESCAPE).perform(); + await driverHelper.wait(1000); + } + + // go to line 9999 to jump to the end of the file + Logger.info('Navigating to end of file and appending invalid env content'); + await driverHelper.getDriver().actions().keyDown(Key.CONTROL).sendKeys('g').keyUp(Key.CONTROL).perform(); + await driverHelper.wait(1000); + await driverHelper.getDriver().actions().sendKeys('9999').perform(); + await driverHelper.getDriver().actions().sendKeys(Key.ENTER).perform(); + await driverHelper.wait(500); + await driverHelper.getDriver().actions().sendKeys(Key.END).perform(); + await driverHelper.getDriver().actions().sendKeys(' env:').perform(); + await driverHelper.getDriver().actions().sendKeys(Key.ESCAPE).perform(); + await driverHelper.wait(300); + await driverHelper.getDriver().actions().sendKeys(Key.ENTER).perform(); + await driverHelper.getDriver().actions().sendKeys(Key.HOME).sendKeys(Key.HOME).perform(); + await driverHelper.getDriver().actions().sendKeys(' - name: test-env').perform(); + await driverHelper.getDriver().actions().sendKeys(Key.ESCAPE).perform(); + await driverHelper.wait(300); + await driverHelper.getDriver().actions().sendKeys(Key.ENTER).perform(); + await driverHelper.getDriver().actions().sendKeys(Key.HOME).sendKeys(Key.HOME).perform(); + await driverHelper.getDriver().actions().sendKeys(' value: true').perform(); + + Logger.info('Saving file with Ctrl+S'); + await driverHelper.getDriver().actions().keyDown(Key.CONTROL).sendKeys('s').keyUp(Key.CONTROL).perform(); + await driverHelper.wait(1000); + + Logger.info('Restart from local devfile'); + await restartFromLocalDevfile(driverHelper, projectName); + + Logger.info('Check error popup'); + const errorDialog: By = By.xpath('//*[@class="dialog-message-text"]'); + const dialogElement: WebElement = await driverHelper.waitVisibility(errorDialog, 40000); + const dialogText: string = await dialogElement.getText(); + Logger.info(`Error dialog title text: ${dialogText}`); + + const errorDetailDialog: By = By.xpath('//*[@class="dialog-message-detail"]'); + const detailElement: WebElement = await driverHelper.waitVisibility(errorDetailDialog, 40000); + const detailText: string = await detailElement.getText(); + Logger.info(`Error dialog detail text: ${detailText}`); + + expect(detailText).to.equal( + 'Devfile schema validation failed. Error: 0: instance.components[0] is not allowed to have the additional property "env"' + ); + }); + + suiteTeardown('Delete DevWorkspace', async function (): Promise { + Logger.debug('Delete DevWorkspace. After each test.'); + if (currentTabHandle !== 'undefined') { + await browserTabsUtil.switchToWindow(currentTabHandle); + } + + await dashboard.openDashboard(); + await browserTabsUtil.closeAllTabsExceptCurrent(); + + if (WorkspaceHandlingTests.getWorkspaceName() !== 'undefined') { + Logger.debug('Workspace name is defined. Deleting workspace...'); + await dashboard.deleteStoppedWorkspaceByUI(WorkspaceHandlingTests.getWorkspaceName()); + } + + WorkspaceHandlingTests.clearWorkspaceName(); + clearCurrentTabHandle(); + + if (originalStorageStrategy !== '' && originalStorageStrategy !== 'per-user') { + const restoreResult: ShellString = shellExecutor.executeCommand( + `oc patch checluster ${cheClusterName} --type=merge ` + + `-p '{"spec":{"devEnvironments":{"storage":{"pvcStrategy":"${originalStorageStrategy}"}}}}'` + ); + expect(restoreResult.code).to.equal(0, 'Failed to restore CheCluster storage strategy'); + } + }); +}); + +suite(`Test case with pvc-fail workspace (bad image restart) ${BASE_TEST_CONSTANTS.TEST_ENVIRONMENT}`, function (): void { + const loginTests: LoginTests = e2eContainer.get(CLASSES.LoginTests); + const workspaceHandlingTests: WorkspaceHandlingTests = e2eContainer.get(CLASSES.WorkspaceHandlingTests); + const dashboard: Dashboard = e2eContainer.get(CLASSES.Dashboard); + const workspaces: Workspaces = e2eContainer.get(CLASSES.Workspaces); + const workspaceDetails: WorkspaceDetails = e2eContainer.get(CLASSES.WorkspaceDetails); + const browserTabsUtil: BrowserTabsUtil = e2eContainer.get(CLASSES.BrowserTabsUtil); + const driverHelper: DriverHelper = e2eContainer.get(CLASSES.DriverHelper); + const containerTerminal: ContainerTerminal = e2eContainer.get(CLASSES.ContainerTerminal); + const kubernetesCommandLineToolsExecutor: KubernetesCommandLineToolsExecutor = e2eContainer.get( + CLASSES.KubernetesCommandLineToolsExecutor + ); + const projectAndFileTests: ProjectAndFileTests = e2eContainer.get(CLASSES.ProjectAndFileTests); + const shellExecutor: ShellExecutor = e2eContainer.get(CLASSES.ShellExecutor); + const testFileName: string = 'test-new-file.txt'; + const testFileContent: string = 'test file content for pvc-fail restart verification'; + const gitRepository: string = 'https://github.com/cgruver/test-workspace/tree/pvc-fail'; + const projectName: string = 'test-workspace'; + let currentTabHandle: string = 'undefined'; + const xPathToWaitFor: string = '//*[@id="workbench.parts.sidebar"]'; + const editorXpath: string = '//*[@id="editor-selector-card-che-incubator/che-code/latest"]'; + const cheClusterName: string = 'devspaces'; + let originalStorageStrategy: string = ''; + + suiteSetup('Skip suite on disconnected clusters', function (): void { + if (BASE_TEST_CONSTANTS.IS_CLUSTER_DISCONNECTED()) { + Logger.info('Test cluster is disconnected. Skipping pvc-fail workspace suite.'); + this.skip(); + } + }); + + suiteSetup('Login into Che and set per-workspace storage strategy', async function (): Promise { + kubernetesCommandLineToolsExecutor.loginToOcp(); + shellExecutor.executeCommand('oc project openshift-devspaces'); + + originalStorageStrategy = shellExecutor + .executeCommand(`oc get checluster/${cheClusterName} -o "jsonpath={.spec.devEnvironments.storage.pvcStrategy}"`) + .stdout.trim(); + + if (originalStorageStrategy !== 'per-workspace') { + const patchResult: ShellString = shellExecutor.executeCommand( + `oc patch checluster ${cheClusterName} --type=merge ` + + '-p \'{"spec":{"devEnvironments":{"storage":{"pvcStrategy":"per-workspace"}}}}\'' + ); + expect(patchResult.code).to.equal(0, 'Failed to patch CheCluster to set per-workspace storage strategy'); + } + + await loginTests.loginIntoChe(); + }); + + function clearCurrentTabHandle(): void { + currentTabHandle = 'undefined'; + } + + test('Create workspace from test-workspace pvc-fail branch', async function (): Promise { + await dashboard.openDashboard(); + currentTabHandle = await browserTabsUtil.getCurrentWindowHandle(); + await dashboard.clickCreateWorkspaceButton(); + await workspaceHandlingTests.createAndOpenWorkspaceWithSpecificEditorAndGitUrl(editorXpath, gitRepository, xPathToWaitFor); + }); + + test('Accept the project as a trusted one', async function (): Promise { + await projectAndFileTests.performTrustAuthorDialog(); + }); + + test('Setup workspace context for API operations', function (): void { + kubernetesCommandLineToolsExecutor.workspaceName = WorkspaceHandlingTests.getWorkspaceName(); + kubernetesCommandLineToolsExecutor.getPodAndContainerNames(); + }); + + test('Create new file in project folder', function (): void { + const output: ShellString = containerTerminal.execInContainerCommand( + `echo "${testFileContent}" > /projects/${projectName}/${testFileName}` + ); + expect(output.code).to.equal(0); + }); + + test('Modify devfile to point to bad image location', function (): void { + Logger.info('Replacing ":latest" with ":badimage" in devfile.yaml using sed'); + const output: ShellString = containerTerminal.execInContainerCommand( + `sed -i 's/:latest/:badimage/g' /projects/${projectName}/devfile.yaml` + ); + expect(output.code).to.equal(0, 'Failed to replace :latest with :badimage in devfile.yaml'); + + const verifyOutput: ShellString = containerTerminal.execInContainerCommand(`cat /projects/${projectName}/devfile.yaml`); + expect(verifyOutput.stdout).to.include(':badimage'); + expect(verifyOutput.stdout).to.not.include(':latest'); + }); + + test('Restart workspace from local devfile with bad image', async function (): Promise { + await restartFromLocalDevfile(driverHelper, projectName); + Logger.info('Waiting for "Restart Workspace" confirmation popup'); + await driverHelper.waitAndClick(By.xpath('//div[@class="dialog-buttons"]//a[text()="Restart your workspace"]'), 30000); + }); + + test('Wait for workspace start failure and restart with default devfile', async function (): Promise { + Logger.info('Waiting for workspace to enter Failed phase and restart with default devfile'); + await driverHelper.waitAndClick(By.xpath('//span[text()="Restart with default devfile"]'), 120000); + await driverHelper.waitVisibility(By.xpath(xPathToWaitFor), 120000); + }); + + test('Re-initialize workspace context after restart', function (): void { + kubernetesCommandLineToolsExecutor.getPodAndContainerNames(); + }); + + test('Verify project exists after restart with default devfile', function (): void { + expect(containerTerminal.ls().stdout).includes(projectName); + expect(containerTerminal.ls(`/projects/${projectName}`).stdout).includes('devfile.yaml'); + }); + + test('Verify new file is present after restart with default devfile', function (): void { + const output: ShellString = containerTerminal.execInContainerCommand(`cat /projects/${projectName}/${testFileName}`); + expect(output.stdout.trim()).to.equal(testFileContent); + }); + + test('Verify storage type is per-workspace in workspace details', async function (): Promise { + const workspaceName: string = WorkspaceHandlingTests.getWorkspaceName(); + + Logger.info('Opening Workspaces page'); + await dashboard.openDashboard(); + await dashboard.clickWorkspacesButton(); + await workspaces.waitPage(); + + Logger.info(`Opening workspace details for: ${workspaceName}`); + await workspaces.clickWorkspaceListItemLink(workspaceName); + await workspaceDetails.waitWorkspaceTitle(workspaceName); + + Logger.info('Verifying storage type is per-workspace on Overview tab'); + const storageTypeLocator: By = By.xpath( + '//span[@aria-label="More info for storage type"]/ancestor::div[contains(@class, "pf-v6-c-form__group")]' + + '//div[contains(@class, "pf-v6-c-form__group-control")]/span' + ); + const storageTypeValue: string = await driverHelper.waitAndGetText(storageTypeLocator); + expect(storageTypeValue).to.equal('per-workspace'); + }); + + suiteTeardown('Delete DevWorkspace', async function (): Promise { + Logger.debug('Delete DevWorkspace. After each test.'); + if (currentTabHandle !== 'undefined') { + await browserTabsUtil.switchToWindow(currentTabHandle); + } + + await dashboard.openDashboard(); + await browserTabsUtil.closeAllTabsExceptCurrent(); + + if (WorkspaceHandlingTests.getWorkspaceName() !== 'undefined') { + Logger.debug('Workspace name is defined. Deleting workspace...'); + await dashboard.deleteStoppedWorkspaceByUI(WorkspaceHandlingTests.getWorkspaceName()); + } + + WorkspaceHandlingTests.clearWorkspaceName(); + clearCurrentTabHandle(); + + if (originalStorageStrategy !== '' && originalStorageStrategy !== 'per-workspace') { + const restoreResult: ShellString = shellExecutor.executeCommand( + `oc patch checluster ${cheClusterName} --type=merge ` + + `-p '{"spec":{"devEnvironments":{"storage":{"pvcStrategy":"${originalStorageStrategy}"}}}}'` + ); + expect(restoreResult.code).to.equal(0, 'Failed to restore CheCluster storage strategy'); + } + }); +}); From 99786184f95a580b66e67cf0cf01987ec8b79912 Mon Sep 17 00:00:00 2001 From: Oleksii Korniienko Date: Thu, 27 Aug 2026 18:09:55 +0200 Subject: [PATCH 4/4] Reworked due to PR review Assisted-by: Claude Code Signed-off-by: Oleksii Korniienko --- tests/e2e/configs/inversify.config.ts | 2 + tests/e2e/configs/inversify.types.ts | 3 +- tests/e2e/index.ts | 1 + .../workspace-details/WorkspaceDetails.ts | 10 ++ .../pageobjects/ide/RestartWorkspaceDialog.ts | 74 ++++++++++++ .../RestartFromLocalDevfile.spec.ts | 114 +++++------------- 6 files changed, 117 insertions(+), 87 deletions(-) create mode 100644 tests/e2e/pageobjects/ide/RestartWorkspaceDialog.ts diff --git a/tests/e2e/configs/inversify.config.ts b/tests/e2e/configs/inversify.config.ts index dd820b18524..42748a68f60 100644 --- a/tests/e2e/configs/inversify.config.ts +++ b/tests/e2e/configs/inversify.config.ts @@ -63,6 +63,7 @@ import { NotificationHandler } from '../pageobjects/ide/NotificationHandler'; import { SourceControlView } from '../pageobjects/ide/SourceControlView'; import { GitHubExtensionDialog } from '../pageobjects/ide/GitHubExtensionDialog'; import { AiCodeSignInDialog } from '../pageobjects/ide/AiCodeSignInDialog'; +import { RestartWorkspaceDialog } from '../pageobjects/ide/RestartWorkspaceDialog'; const e2eContainer: Container = new Container({ defaultScope: 'Transient', skipBaseClassChecks: true }); @@ -111,6 +112,7 @@ e2eContainer.bind(CLASSES.NotificationHandler).to(Notificat e2eContainer.bind(CLASSES.SourceControlView).to(SourceControlView); e2eContainer.bind(CLASSES.GitHubExtensionDialog).to(GitHubExtensionDialog); e2eContainer.bind(CLASSES.AiCodeSignInDialog).to(AiCodeSignInDialog); +e2eContainer.bind(CLASSES.RestartWorkspaceDialog).to(RestartWorkspaceDialog); 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 8471c5b4e9c..9c0ca6daa75 100644 --- a/tests/e2e/configs/inversify.types.ts +++ b/tests/e2e/configs/inversify.types.ts @@ -60,7 +60,8 @@ const CLASSES: any = { NotificationHandler: 'NotificationHandler', SourceControlView: 'SourceControlView', GitHubExtensionDialog: 'GitHubExtensionDialog', - AiCodeSignInDialog: 'AiCodeSignInDialog' + AiCodeSignInDialog: 'AiCodeSignInDialog', + RestartWorkspaceDialog: 'RestartWorkspaceDialog' }; const EXTERNAL_CLASSES: any = { diff --git a/tests/e2e/index.ts b/tests/e2e/index.ts index 6d417490602..c2b7ace81f0 100644 --- a/tests/e2e/index.ts +++ b/tests/e2e/index.ts @@ -36,6 +36,7 @@ export * from './pageobjects/ide/ExplorerView'; export * from './pageobjects/ide/ExtensionsView'; export * from './pageobjects/ide/GitHubExtensionDialog'; export * from './pageobjects/ide/NotificationHandler'; +export * from './pageobjects/ide/RestartWorkspaceDialog'; export * from './pageobjects/ide/RestrictedModeButton'; export * from './pageobjects/ide/SourceControlView'; export * from './pageobjects/ide/ViewsMoreActionsButton'; diff --git a/tests/e2e/pageobjects/dashboard/workspace-details/WorkspaceDetails.ts b/tests/e2e/pageobjects/dashboard/workspace-details/WorkspaceDetails.ts index b2170e18b9e..968cfb91d2f 100644 --- a/tests/e2e/pageobjects/dashboard/workspace-details/WorkspaceDetails.ts +++ b/tests/e2e/pageobjects/dashboard/workspace-details/WorkspaceDetails.ts @@ -26,6 +26,10 @@ export class WorkspaceDetails { private static readonly ENABLED_SAVE_BUTTON: By = By.css('button[name="save-button"][aria-disabled="false"]'); private static readonly WORKSPACE_DETAILS_LOADER: By = By.css('workspace-details-overview md-progress-linear'); private static readonly STORAGE_TYPE_INFO_BUTTON: By = By.css('span[aria-label="More info for storage type"]'); + private static readonly STORAGE_TYPE_VALUE: By = By.xpath( + '//span[@aria-label="More info for storage type"]/ancestor::div[contains(@class, "pf-v6-c-form__group")]' + + '//div[contains(@class, "pf-v6-c-form__group-control")]/span' + ); private static readonly CLOSE_STORAGE_TYPE_INFO_BUTTON: By = By.xpath('//button[@aria-label="Close"]'); private static readonly STORAGE_TYPE_DOC_LINK: By = By.xpath('//div/p/a'); private static readonly DEVFILE_DOC_LINK: By = By.xpath('//a[text()="Devfile Documentation"]'); @@ -126,6 +130,12 @@ export class WorkspaceDetails { await this.clickOnTab(tabTitle, timeout); } + async getStorageTypeValue(timeout: number = TIMEOUT_CONSTANTS.TS_COMMON_DASHBOARD_WAIT_TIMEOUT): Promise { + Logger.debug(); + + return await this.driverHelper.waitAndGetText(WorkspaceDetails.STORAGE_TYPE_VALUE, timeout); + } + async clickStorageTypeInfo(): Promise { Logger.debug(); diff --git a/tests/e2e/pageobjects/ide/RestartWorkspaceDialog.ts b/tests/e2e/pageobjects/ide/RestartWorkspaceDialog.ts new file mode 100644 index 00000000000..351fcf384fc --- /dev/null +++ b/tests/e2e/pageobjects/ide/RestartWorkspaceDialog.ts @@ -0,0 +1,74 @@ +/** ******************************************************************* + * copyright (c) 2026 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 RestartWorkspaceDialog { + private static readonly RESTART_BUTTON: By = By.xpath('//div[@class="dialog-buttons"]//a[text()="Restart"]'); + private static readonly RESTART_WORKSPACE_BUTTON: By = By.xpath('//div[@class="dialog-buttons"]//a[text()="Restart your workspace"]'); + private static readonly RESTART_WITH_DEFAULT_DEVFILE_BUTTON: By = By.xpath('//span[text()="Restart with default devfile"]'); + private static readonly ERROR_DIALOG_TEXT: By = By.xpath('//*[@class="dialog-message-text"]'); + private static readonly ERROR_DIALOG_DETAIL: By = By.xpath('//*[@class="dialog-message-detail"]'); + + constructor( + @inject(CLASSES.DriverHelper) + private readonly driverHelper: DriverHelper + ) {} + + async restartFromLocalDevfile(projectName: string): Promise { + Logger.debug(); + + await this.driverHelper.getDriver().actions().keyDown(Key.CONTROL).sendKeys('p').keyUp(Key.CONTROL).perform(); + await this.driverHelper.wait(TIMEOUT_CONSTANTS.TS_SELENIUM_DEFAULT_POLLING); + + await this.driverHelper.getDriver().actions().sendKeys('>Dev Spaces: Restart Workspace from Local Devfile').perform(); + await this.driverHelper.wait(TIMEOUT_CONSTANTS.TS_SELENIUM_DEFAULT_POLLING); + await this.driverHelper.getDriver().actions().sendKeys(Key.ENTER).perform(); + await this.driverHelper.wait(TIMEOUT_CONSTANTS.TS_SELENIUM_DEFAULT_POLLING); + + await this.driverHelper.getDriver().actions().sendKeys(`/projects/${projectName}/devfile.yaml`).perform(); + await this.driverHelper.wait(TIMEOUT_CONSTANTS.TS_SELENIUM_DEFAULT_POLLING); + await this.driverHelper.getDriver().actions().sendKeys(Key.ENTER).perform(); + await this.driverHelper.wait(TIMEOUT_CONSTANTS.TS_SELENIUM_DEFAULT_POLLING); + + await this.driverHelper.waitAndClick(RestartWorkspaceDialog.RESTART_BUTTON); + } + + async confirmRestartWorkspace(timeout: number = TIMEOUT_CONSTANTS.TS_COMMON_PLUGIN_TEST_TIMEOUT): Promise { + Logger.debug(); + + await this.driverHelper.waitAndClick(RestartWorkspaceDialog.RESTART_WORKSPACE_BUTTON, timeout); + } + + async clickRestartWithDefaultDevfile(timeout: number = TIMEOUT_CONSTANTS.TS_SELENIUM_START_WORKSPACE_TIMEOUT): Promise { + Logger.debug(); + + await this.driverHelper.waitAndClick(RestartWorkspaceDialog.RESTART_WITH_DEFAULT_DEVFILE_BUTTON, timeout); + } + + async getErrorDialogText(timeout: number = TIMEOUT_CONSTANTS.TS_COMMON_PLUGIN_TEST_TIMEOUT): Promise { + Logger.debug(); + + const element: WebElement = await this.driverHelper.waitVisibility(RestartWorkspaceDialog.ERROR_DIALOG_TEXT, timeout); + return element.getText(); + } + + async getErrorDetailText(timeout: number = TIMEOUT_CONSTANTS.TS_COMMON_PLUGIN_TEST_TIMEOUT): Promise { + Logger.debug(); + + const element: WebElement = await this.driverHelper.waitVisibility(RestartWorkspaceDialog.ERROR_DIALOG_DETAIL, timeout); + return element.getText(); + } +} diff --git a/tests/e2e/specs/restart-tests/RestartFromLocalDevfile.spec.ts b/tests/e2e/specs/restart-tests/RestartFromLocalDevfile.spec.ts index 5c3cada3363..796aac6f447 100644 --- a/tests/e2e/specs/restart-tests/RestartFromLocalDevfile.spec.ts +++ b/tests/e2e/specs/restart-tests/RestartFromLocalDevfile.spec.ts @@ -12,6 +12,7 @@ import { ContainerTerminal, KubernetesCommandLineToolsExecutor } from '../../uti import { expect } from 'chai'; import { ShellString } from 'shelljs'; import { BASE_TEST_CONSTANTS } from '../../constants/BASE_TEST_CONSTANTS'; +import { TIMEOUT_CONSTANTS } from '../../constants/TIMEOUT_CONSTANTS'; import { e2eContainer } from '../../configs/inversify.config'; import { CLASSES } from '../../configs/inversify.types'; import { ShellExecutor } from '../../utils/ShellExecutor'; @@ -22,27 +23,11 @@ import { Dashboard } from '../../pageobjects/dashboard/Dashboard'; import { Workspaces } from '../../pageobjects/dashboard/Workspaces'; import { WorkspaceDetails } from '../../pageobjects/dashboard/workspace-details/WorkspaceDetails'; import { BrowserTabsUtil } from '../../utils/BrowserTabsUtil'; -import { Key, WebElement } from 'monaco-page-objects'; import { DriverHelper } from '../../utils/DriverHelper'; import { ProjectAndFileTests } from '../../tests-library/ProjectAndFileTests'; +import { RestartWorkspaceDialog } from '../../pageobjects/ide/RestartWorkspaceDialog'; import { By } from 'selenium-webdriver'; - -async function restartFromLocalDevfile(driverHelper: DriverHelper, projectName: string): Promise { - await driverHelper.getDriver().actions().keyDown(Key.CONTROL).sendKeys('p').keyUp(Key.CONTROL).perform(); - await driverHelper.wait(1000); - - await driverHelper.getDriver().actions().sendKeys('>Dev Spaces: Restart Workspace from Local Devfile').perform(); - await driverHelper.wait(1000); - await driverHelper.getDriver().actions().sendKeys(Key.ENTER).perform(); - await driverHelper.wait(1000); - - await driverHelper.getDriver().actions().sendKeys(`/projects/${projectName}/devfile.yaml`).perform(); - await driverHelper.wait(1000); - await driverHelper.getDriver().actions().sendKeys(Key.ENTER).perform(); - await driverHelper.wait(1000); - - await driverHelper.waitAndClick(By.xpath('//div[@class="dialog-buttons"]//a[text()="Restart"]')); -} +import { registerRunningWorkspace } from '../MochaHooks'; suite(`Test case with empty workspace (per-user storage) ${BASE_TEST_CONSTANTS.TEST_ENVIRONMENT}`, function (): void { const loginTests: LoginTests = e2eContainer.get(CLASSES.LoginTests); @@ -56,6 +41,7 @@ suite(`Test case with empty workspace (per-user storage) ${BASE_TEST_CONSTANTS.T CLASSES.KubernetesCommandLineToolsExecutor ); const shellExecutor: ShellExecutor = e2eContainer.get(CLASSES.ShellExecutor); + const restartWorkspaceDialog: RestartWorkspaceDialog = e2eContainer.get(CLASSES.RestartWorkspaceDialog); const testFileName: string = 'test-new-file.txt'; const testFileContent: string = 'test file content for restart verification'; const gitRepository: string = BASE_TEST_CONSTANTS.IS_CLUSTER_DISCONNECTED() @@ -96,6 +82,7 @@ suite(`Test case with empty workspace (per-user storage) ${BASE_TEST_CONSTANTS.T currentTabHandle = await browserTabsUtil.getCurrentWindowHandle(); await dashboard.clickCreateWorkspaceButton(); await workspaceHandlingTests.createAndOpenWorkspaceWithSpecificEditorAndSample(editorXpath, 'Empty Workspace', xPathToWaitFor); + registerRunningWorkspace(WorkspaceHandlingTests.getWorkspaceName()); }); test('Setup workspace context for API operations', function (): void { @@ -124,15 +111,15 @@ suite(`Test case with empty workspace (per-user storage) ${BASE_TEST_CONSTANTS.T }); test('Restart workspace from local devfile with bad image', async function (): Promise { - await restartFromLocalDevfile(driverHelper, projectName); + await restartWorkspaceDialog.restartFromLocalDevfile(projectName); Logger.info('Waiting for "Restart Workspace" confirmation popup'); - await driverHelper.waitAndClick(By.xpath('//div[@class="dialog-buttons"]//a[text()="Restart your workspace"]'), 30000); + await restartWorkspaceDialog.confirmRestartWorkspace(); }); test('Wait for workspace start failure and restart', async function (): Promise { Logger.info('Waiting for workspace to enter Failed phase and restart with default devfile'); - await driverHelper.waitAndClick(By.xpath('//span[text()="Restart with default devfile"]'), 120000); - await driverHelper.waitVisibility(By.xpath(xPathToWaitFor), 180000); + await restartWorkspaceDialog.clickRestartWithDefaultDevfile(); + await driverHelper.waitVisibility(By.xpath(xPathToWaitFor), TIMEOUT_CONSTANTS.TS_IDE_START_TIMEOUT); }); test('Re-initialize workspace context after restart', function (): void { @@ -150,67 +137,22 @@ suite(`Test case with empty workspace (per-user storage) ${BASE_TEST_CONSTANTS.T }); test('Verify devfile schema validation error on invalid env declaration', async function (): Promise { - Logger.info('Opening devfile.yaml via Quick Open (Ctrl+P)'); - const devfileTab: By = By.xpath('//div[contains(@class, "tab") and contains(., "devfile.yaml")]'); - const maxAttempts: number = 3; - for (let attempt: number = 1; attempt <= maxAttempts; attempt++) { - Logger.info(`Attempt ${attempt}/${maxAttempts} to open devfile.yaml`); - await driverHelper.getDriver().actions().keyDown(Key.CONTROL).sendKeys('p').keyUp(Key.CONTROL).perform(); - await driverHelper.wait(1000); - await driverHelper.getDriver().actions().sendKeys(`/projects/${projectName}/devfile.yaml`).perform(); - await driverHelper.wait(1000); - await driverHelper.getDriver().actions().sendKeys(Key.ENTER).perform(); - - if (await driverHelper.waitVisibilityBoolean(devfileTab, 5, 2000)) { - Logger.info('devfile.yaml tab opened successfully'); - break; - } - - if (attempt === maxAttempts) { - throw new Error('Failed to open devfile.yaml in editor after multiple attempts'); - } - - Logger.warn('devfile.yaml tab did not appear, pressing Escape and retrying'); - await driverHelper.getDriver().actions().sendKeys(Key.ESCAPE).perform(); - await driverHelper.wait(1000); - } - - // go to line 9999 to jump to the end of the file - Logger.info('Navigating to end of file and appending invalid env content'); - await driverHelper.getDriver().actions().keyDown(Key.CONTROL).sendKeys('g').keyUp(Key.CONTROL).perform(); - await driverHelper.wait(1000); - await driverHelper.getDriver().actions().sendKeys('9999').perform(); - await driverHelper.getDriver().actions().sendKeys(Key.ENTER).perform(); - await driverHelper.wait(500); - await driverHelper.getDriver().actions().sendKeys(Key.END).perform(); - await driverHelper.getDriver().actions().sendKeys(' env:').perform(); - await driverHelper.getDriver().actions().sendKeys(Key.ESCAPE).perform(); - await driverHelper.wait(300); - await driverHelper.getDriver().actions().sendKeys(Key.ENTER).perform(); - await driverHelper.getDriver().actions().sendKeys(Key.HOME).sendKeys(Key.HOME).perform(); - await driverHelper.getDriver().actions().sendKeys(' - name: test-env').perform(); - await driverHelper.getDriver().actions().sendKeys(Key.ESCAPE).perform(); - await driverHelper.wait(300); - await driverHelper.getDriver().actions().sendKeys(Key.ENTER).perform(); - await driverHelper.getDriver().actions().sendKeys(Key.HOME).sendKeys(Key.HOME).perform(); - await driverHelper.getDriver().actions().sendKeys(' value: true').perform(); - - Logger.info('Saving file with Ctrl+S'); - await driverHelper.getDriver().actions().keyDown(Key.CONTROL).sendKeys('s').keyUp(Key.CONTROL).perform(); - await driverHelper.wait(1000); + Logger.info('Appending invalid env declaration to devfile.yaml'); + const appendOutput: ShellString = containerTerminal.execInContainerCommand( + `echo " env:" >> /projects/${projectName}/devfile.yaml && ` + + `echo " - name: test-env" >> /projects/${projectName}/devfile.yaml && ` + + `echo " value: true" >> /projects/${projectName}/devfile.yaml` + ); + expect(appendOutput.code).to.equal(0, 'Failed to append invalid env content to devfile.yaml'); Logger.info('Restart from local devfile'); - await restartFromLocalDevfile(driverHelper, projectName); + await restartWorkspaceDialog.restartFromLocalDevfile(projectName); Logger.info('Check error popup'); - const errorDialog: By = By.xpath('//*[@class="dialog-message-text"]'); - const dialogElement: WebElement = await driverHelper.waitVisibility(errorDialog, 40000); - const dialogText: string = await dialogElement.getText(); + const dialogText: string = await restartWorkspaceDialog.getErrorDialogText(); Logger.info(`Error dialog title text: ${dialogText}`); - const errorDetailDialog: By = By.xpath('//*[@class="dialog-message-detail"]'); - const detailElement: WebElement = await driverHelper.waitVisibility(errorDetailDialog, 40000); - const detailText: string = await detailElement.getText(); + const detailText: string = await restartWorkspaceDialog.getErrorDetailText(); Logger.info(`Error dialog detail text: ${detailText}`); expect(detailText).to.equal( @@ -234,6 +176,7 @@ suite(`Test case with empty workspace (per-user storage) ${BASE_TEST_CONSTANTS.T WorkspaceHandlingTests.clearWorkspaceName(); clearCurrentTabHandle(); + registerRunningWorkspace(''); if (originalStorageStrategy !== '' && originalStorageStrategy !== 'per-user') { const restoreResult: ShellString = shellExecutor.executeCommand( @@ -259,6 +202,7 @@ suite(`Test case with pvc-fail workspace (bad image restart) ${BASE_TEST_CONSTAN ); const projectAndFileTests: ProjectAndFileTests = e2eContainer.get(CLASSES.ProjectAndFileTests); const shellExecutor: ShellExecutor = e2eContainer.get(CLASSES.ShellExecutor); + const restartWorkspaceDialog: RestartWorkspaceDialog = e2eContainer.get(CLASSES.RestartWorkspaceDialog); const testFileName: string = 'test-new-file.txt'; const testFileContent: string = 'test file content for pvc-fail restart verification'; const gitRepository: string = 'https://github.com/cgruver/test-workspace/tree/pvc-fail'; @@ -304,6 +248,7 @@ suite(`Test case with pvc-fail workspace (bad image restart) ${BASE_TEST_CONSTAN currentTabHandle = await browserTabsUtil.getCurrentWindowHandle(); await dashboard.clickCreateWorkspaceButton(); await workspaceHandlingTests.createAndOpenWorkspaceWithSpecificEditorAndGitUrl(editorXpath, gitRepository, xPathToWaitFor); + registerRunningWorkspace(WorkspaceHandlingTests.getWorkspaceName()); }); test('Accept the project as a trusted one', async function (): Promise { @@ -335,15 +280,15 @@ suite(`Test case with pvc-fail workspace (bad image restart) ${BASE_TEST_CONSTAN }); test('Restart workspace from local devfile with bad image', async function (): Promise { - await restartFromLocalDevfile(driverHelper, projectName); + await restartWorkspaceDialog.restartFromLocalDevfile(projectName); Logger.info('Waiting for "Restart Workspace" confirmation popup'); - await driverHelper.waitAndClick(By.xpath('//div[@class="dialog-buttons"]//a[text()="Restart your workspace"]'), 30000); + await restartWorkspaceDialog.confirmRestartWorkspace(); }); test('Wait for workspace start failure and restart with default devfile', async function (): Promise { Logger.info('Waiting for workspace to enter Failed phase and restart with default devfile'); - await driverHelper.waitAndClick(By.xpath('//span[text()="Restart with default devfile"]'), 120000); - await driverHelper.waitVisibility(By.xpath(xPathToWaitFor), 120000); + await restartWorkspaceDialog.clickRestartWithDefaultDevfile(); + await driverHelper.waitVisibility(By.xpath(xPathToWaitFor), TIMEOUT_CONSTANTS.TS_IDE_START_TIMEOUT); }); test('Re-initialize workspace context after restart', function (): void { @@ -373,11 +318,7 @@ suite(`Test case with pvc-fail workspace (bad image restart) ${BASE_TEST_CONSTAN await workspaceDetails.waitWorkspaceTitle(workspaceName); Logger.info('Verifying storage type is per-workspace on Overview tab'); - const storageTypeLocator: By = By.xpath( - '//span[@aria-label="More info for storage type"]/ancestor::div[contains(@class, "pf-v6-c-form__group")]' + - '//div[contains(@class, "pf-v6-c-form__group-control")]/span' - ); - const storageTypeValue: string = await driverHelper.waitAndGetText(storageTypeLocator); + const storageTypeValue: string = await workspaceDetails.getStorageTypeValue(); expect(storageTypeValue).to.equal('per-workspace'); }); @@ -397,6 +338,7 @@ suite(`Test case with pvc-fail workspace (bad image restart) ${BASE_TEST_CONSTAN WorkspaceHandlingTests.clearWorkspaceName(); clearCurrentTabHandle(); + registerRunningWorkspace(''); if (originalStorageStrategy !== '' && originalStorageStrategy !== 'per-workspace') { const restoreResult: ShellString = shellExecutor.executeCommand(