Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ export class WorkspaceDetails {
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"]');
private static readonly RENAME_WORKSPACE_BUTTON: By = By.xpath('//button[@title="Edit Workspace Name"]');
private static readonly RENAME_WORKSPACE_INPUT: By = By.id('edit-workspace-name');
private static readonly RENAME_SAVE_BUTTON: By = By.xpath('//button[@data-testid="edit-workspace-name-save"]');
private static readonly RENAME_CANCEL_BUTTON: By = By.xpath('//button[@data-testid="edit-workspace-name-cancel"]');

constructor(
@inject(CLASSES.DriverHelper)
Expand Down Expand Up @@ -144,6 +148,43 @@ export class WorkspaceDetails {
return await this.driverHelper.waitAndGetElementAttribute(WorkspaceDetails.DEVFILE_DOC_LINK, 'href');
}

/**
* devSpaces Dashboard does not allow editing the workspace display name while the workspace is running.
*/
async waitRenameWorkspaceNotPossibleWhileWorkspaceRunning(): Promise<void> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I would suggest making the method name more specific, like checkRenameButtonIsAbsent()

Logger.debug();

await this.driverHelper.waitDisappearance(WorkspaceDetails.RENAME_WORKSPACE_BUTTON);
}

/**
* rename a stopped workspace from the Overview tab (fill name + save).
*/
async renameStoppedWorkspaceTo(newDisplayName: string): Promise<void> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The fact that workspace is stopped doesn't matter in context of Workspace Details page.

We are just trying to rename workspace, nothing else.

Let's name the method correspondingly, like renameWorkspace().

Logger.debug(`newDisplayName: "${newDisplayName}"`);
await this.driverHelper.waitAndClick(WorkspaceDetails.RENAME_WORKSPACE_BUTTON, TIMEOUT_CONSTANTS.TS_SELENIUM_LOAD_PAGE_TIMEOUT);
await this.driverHelper.type(WorkspaceDetails.RENAME_WORKSPACE_INPUT, newDisplayName);
await this.driverHelper.waitAndClick(WorkspaceDetails.RENAME_SAVE_BUTTON);
await this.waitWorkspaceTitle(newDisplayName);
}

async attemptRenameWorkspaceName(desiredName: string): Promise<void> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

attemptRenameWorkspaceName() is really test-level assertion logic - it's verifying a negative scenario (save button disabled, then cancel). That kind of logic belongs in the spec file (RenameWorkspace test), not in the page object.

Logger.debug(`desiredName: "${desiredName}"`);

await this.driverHelper.waitAndClick(WorkspaceDetails.RENAME_WORKSPACE_BUTTON, TIMEOUT_CONSTANTS.TS_SELENIUM_LOAD_PAGE_TIMEOUT);
await this.driverHelper.type(WorkspaceDetails.RENAME_WORKSPACE_INPUT, desiredName);
await this.driverHelper.waitAttributePresent(
WorkspaceDetails.RENAME_SAVE_BUTTON,
'disabled',
TIMEOUT_CONSTANTS.TS_COMMON_DASHBOARD_WAIT_TIMEOUT
);
await this.closeRenameWorkspaceForm();
}

async closeRenameWorkspaceForm(): Promise<void> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

closeRenameWorkspaceForm() has the same problem - it's only ever called from attemptRenameWorkspaceName, which itself is test-specific logic.

If attemptRenameWorkspaceName moves to the RenameWorkspace test, closeRenameWorkspaceForm() has no reason to stay in the page object either.

await this.driverHelper.waitAndClick(WorkspaceDetails.RENAME_CANCEL_BUTTON, TIMEOUT_CONSTANTS.TS_SELENIUM_LOAD_PAGE_TIMEOUT);
}

private getWorkspaceTitleLocator(workspaceName: string): By {
return By.xpath(`//h1[text()='${workspaceName}']`);
}
Expand Down
135 changes: 135 additions & 0 deletions tests/e2e/specs/miscellaneous/RenameWorkspace.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/** *******************************************************************
* 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 { e2eContainer } from '../../configs/inversify.config';
import { CLASSES } from '../../configs/inversify.types';
import { expect } from 'chai';
import { WorkspaceHandlingTests } from '../../tests-library/WorkspaceHandlingTests';
import { ProjectAndFileTests } from '../../tests-library/ProjectAndFileTests';
import { LoginTests } from '../../tests-library/LoginTests';
import { registerRunningWorkspace } from '../MochaHooks';
import { BrowserTabsUtil } from '../../utils/BrowserTabsUtil';
import { BASE_TEST_CONSTANTS } from '../../constants/BASE_TEST_CONSTANTS';
import { Dashboard } from '../../pageobjects/dashboard/Dashboard';
import { Workspaces } from '../../pageobjects/dashboard/Workspaces';
import { WorkspaceDetails } from '../../pageobjects/dashboard/workspace-details/WorkspaceDetails';
import { TIMEOUT_CONSTANTS } from '../../constants/TIMEOUT_CONSTANTS';

const stackName: string = BASE_TEST_CONSTANTS.TS_SELENIUM_DASHBOARD_SAMPLE_NAME || 'Empty Workspace';
const RENAMED_WORKSPACE_NAME: string = 'new-ws';

suite(`Rename workspace ${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 browserTabsUtil: BrowserTabsUtil = e2eContainer.get(CLASSES.BrowserTabsUtil);
const dashboard: Dashboard = e2eContainer.get(CLASSES.Dashboard);
const workspaces: Workspaces = e2eContainer.get(CLASSES.Workspaces);
const workspaceDetails: WorkspaceDetails = e2eContainer.get(CLASSES.WorkspaceDetails);

let firstWorkspaceName: string = '';
let secondWorkspaceName: string = '';

async function openDashboardWorkspacesList(): Promise<void> {
await dashboard.openDashboard();
await dashboard.clickWorkspacesButton();
await workspaces.waitPage();
}

async function openWorkspaceDetailsOverview(workspaceName: string): Promise<void> {
await openDashboardWorkspacesList();
await workspaces.clickWorkspaceListItemLink(workspaceName);
await workspaceDetails.waitWorkspaceTitle(workspaceName);
await workspaceDetails.waitLoaderDisappearance();
}

suiteSetup('Login', async function (): Promise<void> {
await loginTests.loginIntoChe();
});

test(`Create workspace from sample (${stackName})`, async function (): Promise<void> {
await workspaceHandlingTests.createAndOpenWorkspace(stackName);
await workspaceHandlingTests.obtainWorkspaceNameFromStartingPage();
firstWorkspaceName = WorkspaceHandlingTests.getWorkspaceName();
registerRunningWorkspace(firstWorkspaceName);

await projectAndFileTests.waitWorkspaceReadinessForCheCodeEditor();
});

test('Workspace details: rename must not be available while the workspace is running', async function (): Promise<void> {
await openWorkspaceDetailsOverview(firstWorkspaceName);
await workspaceDetails.waitRenameWorkspaceNotPossibleWhileWorkspaceRunning();
});

test('Stop the first workspace from the dashboard', async function (): Promise<void> {
await workspaceHandlingTests.stopWorkspace(firstWorkspaceName);
await browserTabsUtil.closeAllTabsExceptCurrent();
});

test(`Rename stopped workspace to "${RENAMED_WORKSPACE_NAME}" from workspace details`, async function (): Promise<void> {
await openWorkspaceDetailsOverview(firstWorkspaceName);
await workspaceDetails.renameStoppedWorkspaceTo(RENAMED_WORKSPACE_NAME);
firstWorkspaceName = RENAMED_WORKSPACE_NAME;
});

test(`Dashboard lists and details show "${RENAMED_WORKSPACE_NAME}"`, async function (): Promise<void> {
await openDashboardWorkspacesList();
await workspaces.waitWorkspaceListItem(RENAMED_WORKSPACE_NAME, TIMEOUT_CONSTANTS.TS_SELENIUM_LOAD_PAGE_TIMEOUT);
await workspaces.clickWorkspaceListItemLink(RENAMED_WORKSPACE_NAME);
await workspaceDetails.waitWorkspaceTitle(RENAMED_WORKSPACE_NAME);
});

test(`Start "${RENAMED_WORKSPACE_NAME}" and wait until it is Running`, async function (): Promise<void> {
await openDashboardWorkspacesList();

await workspaceHandlingTests.openWorkspace(RENAMED_WORKSPACE_NAME);
await workspaceHandlingTests.obtainWorkspaceNameFromStartingPage();
const startedWorkspaceName: string = WorkspaceHandlingTests.getWorkspaceName();
expect(WorkspaceHandlingTests.getWorkspaceName()).equal(RENAMED_WORKSPACE_NAME);
registerRunningWorkspace(startedWorkspaceName);
await projectAndFileTests.waitWorkspaceReadinessForCheCodeEditor();
});

test(`Stop "${RENAMED_WORKSPACE_NAME}" again`, async function (): Promise<void> {
await workspaceHandlingTests.stopWorkspace(RENAMED_WORKSPACE_NAME);
await browserTabsUtil.closeAllTabsExceptCurrent();
});

test(`Workspace details: setting name to "${RENAMED_WORKSPACE_NAME}" when it is already the current name is rejected`, async function (): Promise<void> {
await openWorkspaceDetailsOverview(RENAMED_WORKSPACE_NAME);
await workspaceDetails.attemptRenameWorkspaceName(RENAMED_WORKSPACE_NAME);
});

test(`Create a second workspace from sample (${stackName})`, async function (): Promise<void> {
await workspaceHandlingTests.createAndOpenWorkspace(stackName);
await workspaceHandlingTests.obtainWorkspaceNameFromStartingPage();
secondWorkspaceName = WorkspaceHandlingTests.getWorkspaceName();
registerRunningWorkspace(secondWorkspaceName);
await projectAndFileTests.waitWorkspaceReadinessForCheCodeEditor();
});

test(`Second workspace details: rename to "${RENAMED_WORKSPACE_NAME}" shows conflict and does not apply`, async function (): Promise<void> {
await workspaceHandlingTests.stopWorkspace(secondWorkspaceName);
await browserTabsUtil.closeAllTabsExceptCurrent();

await openWorkspaceDetailsOverview(secondWorkspaceName);
await workspaceDetails.attemptRenameWorkspaceName(RENAMED_WORKSPACE_NAME);
});

suiteTeardown('Stop and delete workspaces created in this suite', async function (): Promise<void> {
await openDashboardWorkspacesList();

await dashboard.deleteStoppedWorkspaceByUI(secondWorkspaceName);
await dashboard.deleteStoppedWorkspaceByUI(RENAMED_WORKSPACE_NAME);
});

suiteTeardown('Unregister running workspace', function (): void {
registerRunningWorkspace('');
});
});
1 change: 1 addition & 0 deletions tests/e2e/suites/online-ocp/UITest.suite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@ import '../../specs/dashboard-samples/Documentation.spec';
import '../../specs/devconsole-intergration/DevConsoleIntegration.spec';
import '../../specs/miscellaneous/CreateWorkspaceWithExistingNameFromGitUrl.spec';
import '../../specs/miscellaneous/KubedockPodmanTest.spec';
import '../../specs/miscellaneous/RenameWorkspace.spec';
import '../../specs/miscellaneous/WorkspaceWithParent.spec';
import '../../specs/miscellaneous/PredefinedNamespace.spec';
12 changes: 11 additions & 1 deletion tests/e2e/tests-library/WorkspaceHandlingTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { ApiUrlResolver } from '../utils/workspace/ApiUrlResolver';
import { TIMEOUT_CONSTANTS } from '../constants/TIMEOUT_CONSTANTS';
import { DriverHelper } from '../utils/DriverHelper';
import { By, error } from 'selenium-webdriver';
import { Workspaces } from '../pageobjects/dashboard/Workspaces';

@injectable()
export class WorkspaceHandlingTests {
Expand All @@ -38,7 +39,9 @@ export class WorkspaceHandlingTests {
@inject(CLASSES.ApiUrlResolver)
private readonly apiUrlResolver: ApiUrlResolver,
@inject(CLASSES.DriverHelper)
private readonly driverHelper: DriverHelper
private readonly driverHelper: DriverHelper,
@inject(CLASSES.Workspaces)
private readonly workspaces: Workspaces
) {}

static getWorkspaceName(): string {
Expand All @@ -62,6 +65,13 @@ export class WorkspaceHandlingTests {
await this.browserTabsUtil.waitAndSwitchToAnotherWindow(WorkspaceHandlingTests.parentGUID, TIMEOUT_CONSTANTS.TS_IDE_LOAD_TIMEOUT);
}

async openWorkspace(workspaceName: string): Promise<void> {
await this.workspaces.clickOpenButton(workspaceName);
await this.apiUrlResolver.getWorkspacesApiUrl();
WorkspaceHandlingTests.parentGUID = await this.browserTabsUtil.getCurrentWindowHandle();
await this.browserTabsUtil.waitAndSwitchToAnotherWindow(WorkspaceHandlingTests.parentGUID, TIMEOUT_CONSTANTS.TS_IDE_LOAD_TIMEOUT);
}

async createAndOpenWorkspaceFromGitRepository(
factoryUrl: string,
branchName?: string,
Expand Down
18 changes: 18 additions & 0 deletions tests/e2e/utils/DriverHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,24 @@ export class DriverHelper {
);
}

/**
* waits until the attribute is present on the element (e.g. boolean HTML `disabled`),
* without requiring a specific attribute value.
*/
async waitAttributePresent(elementLocator: By, attribute: string, timeout: number): Promise<void> {
Logger.trace(`${elementLocator}`);

await this.driver.wait(
async (): Promise<boolean> => {
const attributeValue: string | null = await this.waitAndGetElementAttribute(elementLocator, attribute, timeout);

return attributeValue != null;
},
timeout,
`The '${attribute}' attribute is not present on '${elementLocator}'`
);
}

async type(elementLocator: By, text: string, timeout: number = TIMEOUT_CONSTANTS.TS_SELENIUM_CLICK_ON_VISIBLE_ITEM): Promise<void> {
const polling: number = TIMEOUT_CONSTANTS.TS_SELENIUM_DEFAULT_POLLING;
const attempts: number = Math.ceil(timeout / polling);
Expand Down
Loading