Skip to content

Commit

Permalink
chore(test): add POM page for bootc (podman-desktop#469)
Browse files Browse the repository at this point in the history
* chore(test): add POM page for bootc
  • Loading branch information
cbr7 authored May 21, 2024
1 parent ec2507b commit 889e3cf
Show file tree
Hide file tree
Showing 3 changed files with 231 additions and 36 deletions.
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"]');
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!`,
);
}
}

0 comments on commit 889e3cf

Please sign in to comment.