Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(test): add POM page for bootc #469

Merged
merged 15 commits into from
May 21, 2024
2 changes: 1 addition & 1 deletion tests/playwright/resources/bootable-containerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
107 changes: 72 additions & 35 deletions tests/playwright/src/bootc-extension.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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();
}
Expand Down Expand Up @@ -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 () => {
Expand All @@ -139,3 +154,25 @@ async function ensureBootcIsRemoved(): Promise<void> {
.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];
}
158 changes: 158 additions & 0 deletions tests/playwright/src/model/bootc-page.ts
Original file line number Diff line number Diff line change
@@ -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"]');
cbr7 marked this conversation as resolved.
Show resolved Hide resolved
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<boolean> {
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<void> {
await this.rawCheckbox.uncheck();
await this.qcow2Checkbox.uncheck();
await this.isoCheckbox.uncheck();
await this.vmdkCheckbox.uncheck();
await this.amiCheckbox.uncheck();
}

async getCurrentStatusOfLatestEntry(): Promise<string> {
const status = await this.getCurrentStatusOfLatestBuildImage.getAttribute('title');

if (status) return status;
return '';
}

async waitUntilCurrentBuildIsFinished(): Promise<void> {
await waitUntil(
async () =>
(await this.getCurrentStatusOfLatestEntry()).toLocaleLowerCase() === 'error' ||
(await this.getCurrentStatusOfLatestEntry()).toLocaleLowerCase() === 'success',
340000,
2500,
true,
`Build didn't finish before timeout!`,
);
}
}