chore(tests): introduce globalSetup file and extract shared configuration (#3291)

* feat(tests): introduce globalSetup, extract shared configuration, page object model and podman desktop runner
fixes #3288, #3290, #3302

Signed-off-by: Ondrej Dockal <odockal@redhat.com>
This commit is contained in:
Ondrej Dockal 2023-07-21 12:14:37 +02:00 committed by GitHub
parent 5b79dca233
commit 13d2c3f4c6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 360 additions and 50 deletions

View file

@ -1,51 +1,46 @@
/**********************************************************************
* Copyright (C) 2023 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 { BrowserWindow } from 'electron';
import type { ElectronApplication, JSHandle, Page } from 'playwright';
import { _electron as electron } from 'playwright';
import type { JSHandle, Page } from 'playwright';
import { afterAll, beforeAll, expect, test, describe } from 'vitest';
import { expect as playExpect } from '@playwright/test';
import { existsSync } from 'node:fs';
import { rm } from 'node:fs/promises';
import { PodmanDesktopRunner } from './runner/podman-desktop-runner';
import { WelcomePage } from './model/pages/welcome-page';
import { DashboardPage } from './model/pages/dashboard-page';
const navBarItems = ['Dashboard', 'Containers', 'Images', 'Pods', 'Volumes', 'Settings'];
let electronApp: ElectronApplication;
let pdRunner: PodmanDesktopRunner;
let page: Page;
beforeAll(async () => {
// remove all videos/screenshots
if (existsSync('tests/output')) {
console.log('Cleaning up output folder...');
await rm('tests/output', { recursive: true, force: true });
}
const env: { [key: string]: string } = Object.assign({}, process.env as { [key: string]: string });
env.PODMAN_DESKTOP_HOME_DIR = 'tests/output/podman-desktop';
electronApp = await electron.launch({
args: ['.'],
env,
recordVideo: {
dir: 'tests/output/videos',
size: {
width: 1050,
height: 700,
},
},
});
page = await electronApp.firstWindow();
pdRunner = new PodmanDesktopRunner();
page = await pdRunner.start();
});
afterAll(async () => {
await electronApp.close();
await pdRunner.close();
});
describe('Basic e2e verification of podman desktop start', async () => {
describe('Welcome page handling', async () => {
test('Check the Welcome page is displayed', async () => {
// Direct Electron console to Node terminal.
page.on('console', console.log);
const window: JSHandle<BrowserWindow> = await electronApp.browserWindow(page);
const window: JSHandle<BrowserWindow> = await pdRunner.getBrowserWindow();
const windowState = await window.evaluate(
(mainWindow): Promise<{ isVisible: boolean; isDevToolsOpened: boolean; isCrashed: boolean }> => {
@ -69,40 +64,37 @@ describe('Basic e2e verification of podman desktop start', async () => {
expect(windowState.isCrashed, 'The app has crashed').toBeFalsy();
expect(windowState.isVisible, 'The main window was not visible').toBeTruthy();
await page.screenshot({ path: 'tests/output/screenshots/screenshot-welcome-page-init.png', fullPage: true });
await pdRunner.screenshot('welcome-page-init.png');
const welcomeMessage = page.locator('text=/Welcome to Podman Desktop.*/');
await playExpect(welcomeMessage).toBeVisible();
const welcomePage = new WelcomePage(page);
await playExpect(welcomePage.welcomeMessage).toBeVisible();
});
test('Telemetry checkbox is present, set to true, consent can be changed', async () => {
// wait for the initial screen to be loaded
const telemetryConsent = page.getByText('Telemetry');
expect(telemetryConsent).not.undefined;
expect(await telemetryConsent.isChecked()).to.be.true;
const welcomePage = new WelcomePage(page);
await playExpect(welcomePage.telemetryConsent).toBeVisible();
playExpect(await welcomePage.telemetryConsent.isChecked()).toBeTruthy();
await telemetryConsent.click();
expect(await telemetryConsent.isChecked()).to.be.false;
await welcomePage.turnOffTelemetry();
playExpect(await welcomePage.telemetryConsent.isChecked()).toBeFalsy();
});
test('Redirection from Welcome page to Dashboard works', async () => {
const goToPodmanDesktopButton = page.locator('button:text("Go to Podman Desktop")');
const welcomePage = new WelcomePage(page);
// wait for visibility
await goToPodmanDesktopButton.waitFor({ state: 'visible' });
await welcomePage.goToPodmanDesktopButton.waitFor({ state: 'visible' });
await page.screenshot({ path: 'tests/output/screenshots/screenshot-welcome-page-display.png', fullPage: true });
await pdRunner.screenshot('welcome-page-display.png');
// click on the button
await goToPodmanDesktopButton.click();
await welcomePage.goToPodmanDesktopButton.click();
await page.screenshot({
path: 'tests/output/screenshots/screenshot-welcome-page-redirect-to-dashboard.png',
fullPage: true,
});
await pdRunner.screenshot('welcome-page-redirect-to-dashboard.png');
// check we have the dashboard page
const dashboardTitle = page.getByRole('heading', { name: 'Dashboard' });
await playExpect(dashboardTitle).toBeVisible();
const dashboardPage = new DashboardPage(page);
await playExpect(dashboardPage.heading).toBeVisible();
});
});

View file

@ -0,0 +1,37 @@
/**********************************************************************
* Copyright (C) 2023 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 { removeFolderIfExists } from '../utility/cleanup';
let setupCalled = false;
let teardownCalled = false;
export async function setup() {
if (!setupCalled) {
// remove all previous testing output files
await removeFolderIfExists('tests/output');
setupCalled = true;
}
}
export async function teardown() {
if (!teardownCalled) {
// here comes teardown logic
teardownCalled = true;
}
}

View file

@ -0,0 +1,31 @@
/**********************************************************************
* Copyright (C) 2023 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 { Page } from 'playwright';
export class PodmanDesktopPage {
readonly page: Page;
constructor(page: Page) {
this.page = page;
}
public getPage(): Page {
return this.page;
}
}

View file

@ -0,0 +1,29 @@
/**********************************************************************
* Copyright (C) 2023 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';
import { PodmanDesktopPage } from './base-page';
export class DashboardPage extends PodmanDesktopPage {
readonly heading: Locator;
constructor(page: Page) {
super(page);
this.heading = page.getByRole('heading', { name: 'Dashboard' });
}
}

View file

@ -0,0 +1,58 @@
/**********************************************************************
* Copyright (C) 2023 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';
import { PodmanDesktopPage } from './base-page';
import { expect } from '@playwright/test';
export class WelcomePage extends PodmanDesktopPage {
readonly welcomeMessage: Locator;
readonly telemetryConsent: Locator;
readonly goToPodmanDesktopButton: Locator;
constructor(page: Page) {
super(page);
this.welcomeMessage = page.getByText('Welcome to Podman Desktop');
this.telemetryConsent = page.getByText('Telemetry');
this.goToPodmanDesktopButton = page.getByRole('button', { name: 'Go to Podman Desktop', exact: true });
}
async turnOffTelemetry() {
await this.telemetryConsent.uncheck();
}
async closeWelcomePage() {
await this.goToPodmanDesktopButton.click();
}
async waitForInitialization() {
// wait for an application to initialize
const checkLoader = this.page.getByRole('heading', { name: 'Initializing...' });
await expect(checkLoader).toHaveCount(0, { timeout: 5000 });
}
/**
* Waits for application to initialize, turn off telemetry and closes welcome page
*/
async handleWelcomePage() {
await this.waitForInitialization();
await this.turnOffTelemetry();
await this.closeWelcomePage();
await expect(this.welcomeMessage).toHaveCount(0, { timeout: 3000 });
}
}

View file

@ -0,0 +1,131 @@
/**********************************************************************
* Copyright (C) 2023 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 { ElectronApplication, JSHandle, Page } from 'playwright';
import { _electron as electron } from 'playwright';
import { join } from 'node:path';
import type { BrowserWindow } from 'electron';
export class PodmanDesktopRunner {
private _options: object;
private _running: boolean;
private _app: ElectronApplication | undefined;
private _page: Page | undefined;
private readonly _profile: string;
private readonly _testOutput: string;
constructor(profile = '') {
this._running = false;
this._profile = profile;
this._testOutput = join('tests', 'output', this._profile);
this._options = this.defaultOptions();
}
public async start(): Promise<Page> {
if (this.isRunning()) {
throw Error('Podman Desktop is already running');
}
// start the app with given properties
this._app = await electron.launch({
...this._options,
});
// setup state
this._running = true;
this._page = await this.getElectronApp().firstWindow();
// Direct Electron console to Node terminal.
this.getPage().on('console', console.log);
return this._page;
}
public getPage(): Page {
if (this._page) {
return this._page;
} else {
throw Error('Application was not started yet');
}
}
public getElectronApp(): ElectronApplication {
if (this._app) {
return this._app;
} else {
throw Error('Application was not started yet');
}
}
public async getBrowserWindow(): Promise<JSHandle<BrowserWindow>> {
return await this.getElectronApp().browserWindow(this.getPage());
}
public async screenshot(filename: string) {
await this.getPage().screenshot({ path: join(this._testOutput, 'screenshots', filename), fullPage: true });
}
public async close() {
if (!this.isRunning()) {
throw Error('Podman Desktop is not running');
}
if (this.getElectronApp()) {
await this.getElectronApp().close();
}
this._running = false;
}
private defaultOptions() {
const directory = join(this._testOutput, 'videos');
console.log(`video will be written to: ${directory}`);
const env = this.setupPodmanDesktopCustomFolder();
return {
args: ['.'],
env,
recordVideo: {
dir: directory,
size: {
width: 1050,
height: 700,
},
},
};
}
private setupPodmanDesktopCustomFolder(): object {
const env: { [key: string]: string } = Object.assign({}, process.env as { [key: string]: string });
const dir = join(this._testOutput, 'podman-desktop');
console.log(`podman desktop custom config will be written to: ${dir}`);
env.PODMAN_DESKTOP_HOME_DIR = dir;
return env;
}
public isRunning(): boolean {
return this._running;
}
public setOptions(value: object) {
this._options = value;
}
public getTestOutput(): string {
return this._testOutput;
}
public get options(): object {
return this._options;
}
}

View file

@ -0,0 +1,31 @@
/**********************************************************************
* Copyright (C) 2023 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 { existsSync } from 'node:fs';
import { rm } from 'node:fs/promises';
/**
* Force remove recursively folder, if exists
* @param path path to a folder to be force removed recursively
*/
export async function removeFolderIfExists(path: string) {
if (existsSync(path)) {
console.log(`Cleaning up folder: ${path}`);
await rm(path, { recursive: true, force: true });
}
}

View file

@ -28,6 +28,7 @@ const config = {
test: {
globals: true,
environment: 'jsdom',
globalSetup: './tests/src/globalSetup/global-setup.ts',
/**
* By default, vitest search test files in all packages.
* For e2e tests have sense search only is project root tests folder