diff --git a/tests/playwright/resources/bootable-containerfile b/tests/playwright/resources/bootable-containerfile index 2df480d9..de594264 100644 --- a/tests/playwright/resources/bootable-containerfile +++ b/tests/playwright/resources/bootable-containerfile @@ -16,7 +16,7 @@ * SPDX-License-Identifier: Apache-2.0 ***********************************************************************/ -FROM quay.io/centos-bootc/fedora-bootc:eln +FROM quay.io/centos-bootc/centos-bootc:stream9 # Change the root password RUN echo "root:supersecret" | chpasswd diff --git a/tests/playwright/src/bootc-extension.spec.ts b/tests/playwright/src/bootc-extension.spec.ts index 832ef16d..c7e1e25a 100644 --- a/tests/playwright/src/bootc-extension.spec.ts +++ b/tests/playwright/src/bootc-extension.spec.ts @@ -18,17 +18,27 @@ import type { Page } from '@playwright/test'; import { afterAll, beforeAll, test, describe, beforeEach } from 'vitest'; -import { NavigationBar, PodmanDesktopRunner, WelcomePage, deleteImage } from '@podman-desktop/tests-playwright'; +import { + ImageDetailsPage, + NavigationBar, + PodmanDesktopRunner, + WelcomePage, + deleteImage, +} from '@podman-desktop/tests-playwright'; import { expect as playExpect } from '@playwright/test'; import { RunnerTestContext } from '@podman-desktop/tests-playwright'; import * as path from 'node:path'; import * as os from 'node:os'; +import { BootcPage } from './model/bootc-page'; +import { ArchitectureType } from '@podman-desktop/tests-playwright'; let pdRunner: PodmanDesktopRunner; let page: Page; +let webview: Page; let navBar: NavigationBar; let extensionInstalled = false; -const imageName = 'quay.io/centos-bootc/fedora-bootc'; +const imageName = 'quay.io/centos-bootc/centos-bootc'; +const imageTag = 'stream9'; const extensionName = 'bootc'; const extensionLabel = 'redhat.bootc'; const containerFilePath = path.resolve(__dirname, '..', 'resources', 'bootable-containerfile'); @@ -53,6 +63,8 @@ beforeAll(async () => { afterAll(async () => { try { await deleteImage(page, imageName); + } catch (error) { + console.log(`Error deleting image: ${error}`); } finally { await pdRunner.close(); } @@ -86,40 +98,43 @@ describe('BootC Extension', async () => { 200000, ); - test('Build bootc image from containerfile', async () => { - let imagesPage = await navBar.openImages(); - await playExpect(imagesPage.heading).toBeVisible(); - - const buildImagePage = await imagesPage.openBuildImage(); - await playExpect(buildImagePage.heading).toBeVisible(); - - imagesPage = await buildImagePage.buildImage(`${imageName}:eln`, containerFilePath, contextDirectory); - await playExpect.poll(async () => await imagesPage.waitForImageExists(imageName)).toBeTruthy(); - }, 150000); - - test.skipIf(isLinux).each([ - ['QCOW2', 'ARM64'], - ['QCOW2', 'AMD64'], - ['AMI', 'ARM64'], - ['AMI', 'AMD64'], - ['RAW', 'ARM64'], - ['RAW', 'AMD64'], - ['ISO', 'ARM64'], - ['ISO', 'AMD64'], - ])( - 'Building bootable image type: %s for architecture: %s', - async (type, architecture) => { - const imagesPage = await navBar.openImages(); - await playExpect(imagesPage.heading).toBeVisible(); - - const imageDetailPage = await imagesPage.openImageDetails(imageName); - await playExpect(imageDetailPage.heading).toBeVisible(); - - const pathToStore = path.resolve(__dirname, '..', 'output', 'images', `${type}-${architecture}`); - const result = await imageDetailPage.buildDiskImage(pdRunner, type, architecture, pathToStore); - playExpect(result).toBeTruthy(); + describe.each([ArchitectureType.ARM64, ArchitectureType.AMD64])( + 'Bootc images for architecture: %s', + async architecture => { + test('Build bootc image from containerfile', async () => { + let imagesPage = await navBar.openImages(); + await playExpect(imagesPage.heading).toBeVisible(); + + const buildImagePage = await imagesPage.openBuildImage(); + await playExpect(buildImagePage.heading).toBeVisible(); + + imagesPage = await buildImagePage.buildImage( + `${imageName}:${imageTag}`, + containerFilePath, + contextDirectory, + architecture, + ); + await playExpect.poll(async () => await imagesPage.waitForImageExists(imageName)).toBeTruthy(); + }, 150000); + + test.skipIf(isLinux).each(['QCOW2', 'AMI', 'RAW', 'VMDK', 'ISO'])( + `Building bootable image type: %s`, + async type => { + const imagesPage = await navBar.openImages(); + await playExpect(imagesPage.heading).toBeVisible(); + + const imageDetailPage = await imagesPage.openImageDetails(imageName); + await playExpect(imageDetailPage.heading).toBeVisible(); + + const pathToStore = path.resolve(__dirname, '..', 'tests', 'output', 'images', `${type}-${architecture}`); + [page, webview] = await handleWebview(imageDetailPage); + const bootcPage = new BootcPage(page, webview); + const result = await bootcPage.buildDiskImage(`${imageName}:${imageTag}`, pathToStore, type, architecture); + playExpect(result).toBeTruthy(); + }, + 350000, + ); }, - 350000, ); test('Remove bootc extension through Settings', async () => { @@ -139,3 +154,25 @@ async function ensureBootcIsRemoved(): Promise { .poll(async () => await extensionsPage.extensionIsInstalled(extensionLabel), { timeout: 30000 }) .toBeFalsy(); } + +async function handleWebview(imageDetailsPage: ImageDetailsPage): Promise<[Page, Page]> { + await imageDetailsPage.actionsButton.click(); + await playExpect(imageDetailsPage.buildDiskImageButton).toBeEnabled(); + await imageDetailsPage.buildDiskImageButton.click(); + await page.waitForTimeout(2000); + + const webView = page.getByRole('document', { name: 'Bootable Containers' }); + await playExpect(webView).toBeVisible(); + await new Promise(resolve => setTimeout(resolve, 1000)); + const [mainPage, webViewPage] = pdRunner.getElectronApp().windows(); + await mainPage.evaluate(() => { + const element = document.querySelector('webview'); + if (element) { + (element as HTMLElement).focus(); + } else { + console.log(`element is null`); + } + }); + + return [mainPage, webViewPage]; +} diff --git a/tests/playwright/src/model/bootc-page.ts b/tests/playwright/src/model/bootc-page.ts new file mode 100644 index 00000000..e6afb66b --- /dev/null +++ b/tests/playwright/src/model/bootc-page.ts @@ -0,0 +1,158 @@ +/********************************************************************** + * Copyright (C) 2024 Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ***********************************************************************/ + +import type { Locator, Page } from '@playwright/test'; +import { expect as playExpect } from '@playwright/test'; +import { waitUntil, waitWhile } from '@podman-desktop/tests-playwright'; +import { ArchitectureType } from '@podman-desktop/tests-playwright'; + +export class BootcPage { + readonly page: Page; + readonly webview: Page; + readonly heading: Locator; + readonly outputFolderPath: Locator; + readonly rawCheckbox: Locator; + readonly qcow2Checkbox: Locator; + readonly isoCheckbox: Locator; + readonly vmdkCheckbox: Locator; + readonly amiCheckbox: Locator; + readonly amd64Button: Locator; + readonly arm64Button: Locator; + readonly buildButton: Locator; + readonly imageSelect: Locator; + readonly goBackButton: Locator; + readonly rowGroup: Locator; + readonly latestBuiltImage: Locator; + readonly getCurrentStatusOfLatestBuildImage: Locator; + + constructor(page: Page, webview: Page) { + this.page = page; + this.webview = webview; + this.heading = webview.getByLabel('Build Disk Image'); + this.outputFolderPath = webview.getByLabel('folder-select'); + this.imageSelect = webview.getByLabel('image-select'); + this.rawCheckbox = webview.locator('label[for="raw"]'); + this.qcow2Checkbox = webview.locator('label[for="qcow2"]'); + this.isoCheckbox = webview.locator('label[for="iso"]'); + this.vmdkCheckbox = webview.locator('label[for="vmdk"]'); + this.amiCheckbox = webview.locator('label[for="ami"]'); + this.amd64Button = webview.locator('label[for="amd64"]'); + this.arm64Button = webview.locator('label[for="arm64"]'); + this.buildButton = webview.getByRole('button', { name: 'Build' }); + this.goBackButton = webview.getByRole('button', { name: 'Go Back' }); + this.rowGroup = webview.getByRole('rowgroup').nth(1); + this.latestBuiltImage = this.rowGroup.getByRole('row').first(); + this.getCurrentStatusOfLatestBuildImage = this.latestBuiltImage.getByRole('status'); + } + + async buildDiskImage( + imageName: string, + pathToStore: string, + type: string, + architecture: ArchitectureType, + ): Promise { + let result = false; + + if (await this.buildButton.isEnabled()) { + await this.buildButton.click(); + } + + await playExpect(this.buildButton).toBeDisabled(); + await this.imageSelect.selectOption({ label: imageName }); + + await this.outputFolderPath.fill(pathToStore); + await this.uncheckedAllCheckboxes(); + + switch (type.toLocaleLowerCase()) { + case 'raw': + await this.rawCheckbox.check(); + break; + case 'qcow2': + await this.qcow2Checkbox.check(); + break; + case 'iso': + await this.isoCheckbox.check(); + break; + case 'vmdk': + await this.vmdkCheckbox.check(); + break; + case 'ami': + await this.amiCheckbox.check(); + break; + default: + throw new Error(`Unknown type: ${type}`); + } + + switch (architecture) { + case ArchitectureType.AMD64: + await playExpect(this.amd64Button).toBeEnabled(); + await this.amd64Button.click(); + break; + case ArchitectureType.ARM64: + await playExpect(this.arm64Button).toBeEnabled(); + await this.arm64Button.click(); + break; + default: + throw new Error(`Unknown architecture: ${architecture}`); + } + + await playExpect(this.buildButton).toBeEnabled(); + await this.buildButton.click(); + + await playExpect(this.goBackButton).toBeEnabled(); + await this.goBackButton.click(); + + await this.waitUntilCurrentBuildIsFinished(); + if ((await this.getCurrentStatusOfLatestEntry()) === 'error') return false; + + const dialogMessageLocator = this.page.getByLabel('Dialog Message'); + result = (await dialogMessageLocator.innerText()).includes('Success!'); + const okButtonLocator = this.page.getByRole('button', { name: 'OK' }); + await playExpect(okButtonLocator).toBeEnabled(); + await okButtonLocator.click(); + + return result; + } + + private async uncheckedAllCheckboxes(): Promise { + await this.rawCheckbox.uncheck(); + await this.qcow2Checkbox.uncheck(); + await this.isoCheckbox.uncheck(); + await this.vmdkCheckbox.uncheck(); + await this.amiCheckbox.uncheck(); + } + + async getCurrentStatusOfLatestEntry(): Promise { + const status = await this.getCurrentStatusOfLatestBuildImage.getAttribute('title'); + + if (status) return status; + return ''; + } + + async waitUntilCurrentBuildIsFinished(): Promise { + await waitUntil( + async () => + (await this.getCurrentStatusOfLatestEntry()).toLocaleLowerCase() === 'error' || + (await this.getCurrentStatusOfLatestEntry()).toLocaleLowerCase() === 'success', + 340000, + 2500, + true, + `Build didn't finish before timeout!`, + ); + } +}