diff --git a/tests/playwright/src/model/pages/main-page.ts b/tests/playwright/src/model/pages/main-page.ts index 699ac7e6bad..68c289b223c 100644 --- a/tests/playwright/src/model/pages/main-page.ts +++ b/tests/playwright/src/model/pages/main-page.ts @@ -18,6 +18,7 @@ import type { Locator, Page } from '@playwright/test'; +import { waitUntil } from '../../utility/wait'; import { BasePage } from './base-page'; /** @@ -70,6 +71,10 @@ export abstract class MainPage extends BasePage { return this.content.getByRole('table'); } + async rowsAreVisible(): Promise { + return await this.page.getByRole('row').first().isVisible(); + } + async getRowFromTableByName(name: string): Promise { if (await this.pageIsEmpty()) { return undefined; @@ -94,4 +99,25 @@ export abstract class MainPage extends BasePage { } return undefined; } + + async getRowsFromTableByStatus(status: string): Promise { + await waitUntil(async () => await this.rowsAreVisible(), { sendError: false }); + + const table = this.content.getByRole('table'); + const rows = await table.getByRole('row').all(); + const filteredRows = []; + for (let rowNum = 1; rowNum < rows.length; rowNum++) { + //skip header + const statusCount = await rows[rowNum].getByRole('cell').nth(2).getByTitle(status, { exact: true }).count(); + if (statusCount > 0) filteredRows.push(rows[rowNum]); + } + return filteredRows; + } + + async countRowsFromTable(): Promise { + await waitUntil(async () => await this.rowsAreVisible(), { sendError: false }); + const table = this.content.getByRole('table'); + const rows = await table.getByRole('row').all(); + return rows.length > 1 ? rows.length - 1 : 0; + } } diff --git a/tests/playwright/src/model/pages/volumes-page.ts b/tests/playwright/src/model/pages/volumes-page.ts index 2cadd3f87a9..966a69d54fc 100644 --- a/tests/playwright/src/model/pages/volumes-page.ts +++ b/tests/playwright/src/model/pages/volumes-page.ts @@ -19,7 +19,9 @@ import type { Locator, Page } from '@playwright/test'; import { expect as playExpect } from '@playwright/test'; +import { handleConfirmationDialog } from '../../utility/operations'; import { waitUntil, waitWhile } from '../../utility/wait'; +import { VolumeState } from '../core/states'; import { CreateVolumePage } from './create-volume-page'; import { MainPage } from './main-page'; import { VolumeDetailsPage } from './volume-details-page'; @@ -58,6 +60,19 @@ export class VolumesPage extends MainPage { return new VolumeDetailsPage(this.page, volumeName); } + async deleteVolume(volumeName: string): Promise { + const volumeRow = await this.getVolumeRowByName(volumeName); + if (volumeRow === undefined) { + throw Error(`Volume: ${volumeName} does not exist`); + } + const containerRowDeleteButton = volumeRow.getByRole('button', { name: 'Delete Volume' }); + await playExpect(containerRowDeleteButton).toBeEnabled(); + await containerRowDeleteButton.click(); + await handleConfirmationDialog(this.page); + + return this; + } + async getVolumeRowByName(name: string): Promise { return this.getRowFromTableByName(name); } @@ -67,6 +82,14 @@ export class VolumesPage extends MainPage { return result !== undefined; } + async countVolumesFromTable(): Promise { + return this.countRowsFromTable(); + } + + async countUsedVolumesFromTable(): Promise { + return (await this.getRowsFromTableByStatus(VolumeState.Used)).length; + } + async waitForVolumeExists(name: string): Promise { await waitUntil(async () => await this.volumeExists(name)); return true; @@ -76,4 +99,11 @@ export class VolumesPage extends MainPage { await waitWhile(async () => await this.volumeExists(name)); return true; } + + async pruneVolumes(): Promise { + await playExpect(this.pruneVolumesButton).toBeEnabled(); + await this.pruneVolumesButton.click(); + await handleConfirmationDialog(this.page, 'Prune'); + return this; + } } diff --git a/tests/playwright/src/specs/volume-smoke.spec.ts b/tests/playwright/src/specs/volume-smoke.spec.ts index 923a1d04591..3723438a146 100644 --- a/tests/playwright/src/specs/volume-smoke.spec.ts +++ b/tests/playwright/src/specs/volume-smoke.spec.ts @@ -20,6 +20,8 @@ import type { Page } from '@playwright/test'; import { expect as playExpect } from '@playwright/test'; import { afterAll, beforeAll, beforeEach, describe, test } from 'vitest'; +import { ContainerState, VolumeState } from '../model/core/states'; +import type { ContainerInteractiveParams } from '../model/core/types'; import { WelcomePage } from '../model/pages/welcome-page'; import { NavigationBar } from '../model/workbench/navigation'; import { PodmanDesktopRunner } from '../runner/podman-desktop-runner'; @@ -30,6 +32,11 @@ let pdRunner: PodmanDesktopRunner; let page: Page; let navBar: NavigationBar; +const imageToPull = 'quay.io/centos-bootc/bootc-image-builder'; +const imageTag = 'latest'; +const containerToRun = 'bootc-image-builder'; +const containerStartParams: ContainerInteractiveParams = { attachTerminal: false }; + beforeAll(async () => { pdRunner = new PodmanDesktopRunner(); page = await pdRunner.start(); @@ -55,12 +62,11 @@ describe('Volume workflow verification', async () => { test('Create new Volume', async () => { let volumesPage = await navBar.openVolumes(); await playExpect(volumesPage.heading).toBeVisible(); - const createVolumePage = await volumesPage.openCreateVolumePage(volumeName); volumesPage = await createVolumePage.createVolume(volumeName); - await playExpect.poll(async () => await volumesPage.waitForVolumeExists(volumeName)).toBeTruthy(); }); + test('Test navigation between pages', async () => { const volumesPage = await navBar.openVolumes(); await playExpect(volumesPage.heading).toBeVisible(); @@ -77,10 +83,29 @@ describe('Volume workflow verification', async () => { await volumeDetails.closeButton.click(); await playExpect(volumesPage.heading).toBeVisible(); }); + + test('Delete volume from Volumes page', async () => { + let volumesPage = await navBar.openVolumes(); + await playExpect(volumesPage.heading).toBeVisible(); + const volumeRow = await volumesPage.getVolumeRowByName(volumeName); + playExpect(volumeRow).not.toBeUndefined(); + volumesPage = await volumesPage.deleteVolume(volumeName); + await playExpect.poll(async () => await volumesPage.waitForVolumeDelete(volumeName)).toBeTruthy(); + }); + test('Delete volume through details page', async () => { + //re-create a new volume let volumesPage = await navBar.openVolumes(); await playExpect(volumesPage.heading).toBeVisible(); + const createVolumePage = await volumesPage.openCreateVolumePage(volumeName); + volumesPage = await createVolumePage.createVolume(volumeName); + + await playExpect.poll(async () => await volumesPage.waitForVolumeExists(volumeName)).toBeTruthy(); + + //delete it from the details page + volumesPage = await navBar.openVolumes(); + await playExpect(volumesPage.heading).toBeVisible(); const volumeRow = await volumesPage.getVolumeRowByName(volumeName); playExpect(volumeRow).not.toBeUndefined(); @@ -89,4 +114,69 @@ describe('Volume workflow verification', async () => { await playExpect.poll(async () => await volumesPage.waitForVolumeDelete(volumeName)).toBeTruthy(); }); + + test('Create volumes from bootc-image-builder', async () => { + //count the number of existing volumes + const navigationBar = new NavigationBar(page); + let volumesPage = await navigationBar.openVolumes(); + let previousVolumes = await volumesPage.countVolumesFromTable(); + + //if there are volumes, check how many are used + if (previousVolumes > 0) { + const usedVolumes = await volumesPage.countUsedVolumesFromTable(); + //if there are unused volumes, prune them + if (previousVolumes - usedVolumes > 0) { + volumesPage = await volumesPage.pruneVolumes(); + await playExpect + .poll(async () => (await volumesPage.getRowsFromTableByStatus(VolumeState.Unused)).length, { timeout: 10000 }) + .toBe(0); + previousVolumes = await volumesPage.countVolumesFromTable(); + } + } + + //pull image from quay.io/centos-bootc/bootc-image-builder + let images = await navigationBar.openImages(); + const pullImagePage = await images.openPullImage(); + images = await pullImagePage.pullImage(imageToPull, imageTag); + await playExpect.poll(async () => await images.waitForImageExists(imageToPull)).toBeTruthy(); + + //start a container from the image (generates 4 new volumes) + const imageDetails = await images.openImageDetails(imageToPull); + const runImage = await imageDetails.openRunImage(); + let containers = await runImage.startContainer(containerToRun, containerStartParams); + await playExpect(containers.header).toBeVisible(); + await playExpect + .poll(async () => await containers.containerExists(containerToRun), { timeout: 10000 }) + .toBeTruthy(); + await containers.startContainer(containerToRun); + + //check that four volumes are created (in addition to the existing ones) + volumesPage = await navigationBar.openVolumes(); + await playExpect(volumesPage.heading).toBeVisible(); + const newVolumes = await volumesPage.countVolumesFromTable(); + playExpect(newVolumes - previousVolumes).toBe(4); + + //check the container is stopped and delete it + containers = await navigationBar.openContainers(); + const containerDetails = await containers.openContainersDetails(containerToRun); + await playExpect + .poll(async () => containerDetails.getState(), { timeout: 20000 }) + .toContain(ContainerState.Exited.toLowerCase()); + await playExpect(await containerDetails.getStateLocator()).toHaveText(ContainerState.Exited.toLowerCase()); + containers = await navigationBar.openContainers(); + const containersPage = await containers.deleteContainer(containerToRun); + await playExpect(containersPage.heading).toBeVisible(); + await playExpect + .poll(async () => await containersPage.containerExists(containerToRun), { timeout: 15000 }) + .toBeFalsy(); + + //prune unused volumes + volumesPage = await navigationBar.openVolumes(); + volumesPage = await volumesPage.pruneVolumes(); + await playExpect + .poll(async () => (await volumesPage.getRowsFromTableByStatus(VolumeState.Unused)).length, { timeout: 10000 }) + .toBe(0); + const finalVolumes = await volumesPage.countVolumesFromTable(); + playExpect(finalVolumes - previousVolumes).toBe(0); + }); });