Skip to content

Commit

Permalink
Extending e2e graphical happy path scenario (#1353)
Browse files Browse the repository at this point in the history
* chore(test): implementing happy-path graphical test flow

Signed-off-by: Tibor Dancs <[email protected]>

* chore(test): refactoring ai-lab-page.ts

Signed-off-by: Tibor Dancs <[email protected]>

* chore(test): pr-review - update button locators to use getByRole 'link'

Signed-off-by: Tibor Dancs <[email protected]>

* chore(test): pr-review - implemented ai-lab-base-page.ts, refactored ai-lab-app-details-page.ts

Signed-off-by: Tibor Dancs <[email protected]>

* chore(test): pr-review - fixing thisdot

Signed-off-by: Tibor Dancs <[email protected]>

* chore(test): pr-review - fixing base page abstraction

Signed-off-by: Tibor Dancs <[email protected]>

---------

Signed-off-by: Tibor Dancs <[email protected]>
  • Loading branch information
ScrewTSW authored Aug 7, 2024
1 parent 6cda898 commit 59ef588
Show file tree
Hide file tree
Showing 7 changed files with 346 additions and 10 deletions.
62 changes: 52 additions & 10 deletions tests/playwright/src/ai-lab-extension.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,27 @@
* SPDX-License-Identifier: Apache-2.0
***********************************************************************/

import type { Locator, Page } from '@playwright/test';
import type { Page } from '@playwright/test';
import { expect as playExpect } from '@playwright/test';
import { afterAll, beforeAll, beforeEach, describe, test } from 'vitest';
import type { DashboardPage, ExtensionsPage, RunnerTestContext } from '@podman-desktop/tests-playwright';
import { NavigationBar, WelcomePage, PodmanDesktopRunner } from '@podman-desktop/tests-playwright';
import { AILabPage } from './model/ai-lab-page';
import type { AILabRecipesCatalogPage } from './model/ai-lab-recipes-catalog-page';
import type { AILabAppDetailsPage } from './model/ai-lab-app-details-page';

const AI_LAB_EXTENSION_OCI_IMAGE: string = 'ghcr.io/containers/podman-desktop-extension-ai-lab:nightly';
const AI_LAB_EXTENSION_OCI_IMAGE: string =
process.env.AI_LAB_OCI ?? 'ghcr.io/containers/podman-desktop-extension-ai-lab:nightly';
const AI_LAB_CATALOG_EXTENSION_LABEL: string = 'redhat.ai-lab';
const AI_LAB_NAVBAR_EXTENSION_LABEL: string = 'AI Lab';
const AI_LAB_PAGE_BODY_LABEL: string = 'Webview AI Lab';
const AI_LAB_AI_APP_NAME: string = 'ChatBot';

let pdRunner: PodmanDesktopRunner;
let page: Page;
let webview: Page;
let aiLabPage: AILabPage;
let recipesCatalogPage: AILabRecipesCatalogPage;

let navigationBar: NavigationBar;
let dashboardPage: DashboardPage;
Expand Down Expand Up @@ -62,19 +70,53 @@ describe(`AI Lab extension installation and verification`, async () => {
test(`Install AI Lab extension`, async () => {
await extensionsPage.installExtensionFromOCIImage(AI_LAB_EXTENSION_OCI_IMAGE);
await playExpect
.poll(async () => await extensionsPage.extensionIsInstalled(AI_LAB_CATALOG_EXTENSION_LABEL), { timeout: 30000 })
.poll(async () => await extensionsPage.extensionIsInstalled(AI_LAB_CATALOG_EXTENSION_LABEL), {
timeout: 30_000,
})
.toBeTruthy();
});
});
describe(`AI Lab extension verification`, async () => {
test(`Verify AI Lab is present in notification bar and open it`, async () => {
const aiLabNavBarItem: Locator = navigationBar.navigationLocator.getByLabel(AI_LAB_NAVBAR_EXTENSION_LABEL);
await playExpect(aiLabNavBarItem).toBeVisible();
await aiLabNavBarItem.click();
test(`Verify AI Lab is responsive`, async () => {
[page, webview] = await handleWebview();
aiLabPage = new AILabPage(page, webview);
await aiLabPage.waitForLoad();
});
test(`Verify AI Lab is running`, async () => {
const aiLabWebview: Locator = page.getByLabel(AI_LAB_PAGE_BODY_LABEL);
await playExpect(aiLabWebview).toBeVisible();
test(`Open Recipes Catalog`, async () => {
recipesCatalogPage = await aiLabPage.navigationBar.openRecipesCatalog();
await recipesCatalogPage.waitForLoad();
});
test(`Install ChatBot example app`, { timeout: 780_000 }, async () => {
const chatBotApp: AILabAppDetailsPage = await recipesCatalogPage.openRecipesCatalogApp(
recipesCatalogPage.recipesCatalogNaturalLanguageProcessing,
AI_LAB_AI_APP_NAME,
);
await chatBotApp.waitForLoad();
await chatBotApp.startNewDeployment();
});
});
});

async function handleWebview(): Promise<[Page, Page]> {
const aiLabPodmanExtensionButton = navigationBar.navigationLocator.getByRole('link', {
name: AI_LAB_NAVBAR_EXTENSION_LABEL,
});
await playExpect(aiLabPodmanExtensionButton).toBeEnabled();
await aiLabPodmanExtensionButton.click();
await page.waitForTimeout(2_000);

const webView = page.getByRole('document', { name: AI_LAB_PAGE_BODY_LABEL });
await playExpect(webView).toBeVisible();
await new Promise(resolve => setTimeout(resolve, 1_000));
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];
}
57 changes: 57 additions & 0 deletions tests/playwright/src/model/ai-lab-app-details-page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**********************************************************************
* 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 { expect as playExpect } from '@playwright/test';
import type { Locator, Page } from '@playwright/test';
import { AILabBasePage } from './ai-lab-base-page';
import { AILabStartRecipePage } from './ai-lab-start-recipe-page';

export class AILabAppDetailsPage extends AILabBasePage {
readonly appName: string;
readonly startRecipeButton: Locator;

constructor(page: Page, webview: Page, appName: string) {
super(page, webview, appName);
this.appName = appName;
this.startRecipeButton = this.webview.getByRole('button', { name: 'Start recipe' });
}

async waitForLoad(): Promise<void> {
await playExpect(this.heading).toBeVisible();
}

async deleteLocalClone(): Promise<void> {
throw new Error('Method Not implemented');
}

async startNewDeployment(): Promise<void> {
await playExpect(this.startRecipeButton).toBeEnabled();
await this.startRecipeButton.click();
const starRecipePage: AILabStartRecipePage = new AILabStartRecipePage(this.page, this.webview);
await starRecipePage.waitForLoad();
await starRecipePage.startRecipe();
}

async openRunningApps(): Promise<void> {
throw new Error('Method Not implemented');
}

async deleteRunningApp(_containerName: string): Promise<void> {
throw new Error('Method Not implemented');
}
}
33 changes: 33 additions & 0 deletions tests/playwright/src/model/ai-lab-base-page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**********************************************************************
* 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';

export abstract class AILabBasePage {
readonly page: Page;
readonly webview: Page;
heading: Locator;

constructor(page: Page, webview: Page, heading: string | undefined) {
this.page = page;
this.webview = webview;
this.heading = webview.getByRole('heading', { name: heading });
}

abstract waitForLoad(): Promise<void>;
}
51 changes: 51 additions & 0 deletions tests/playwright/src/model/ai-lab-navigation-bar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**********************************************************************
* 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 { expect as playExpect } from '@playwright/test';
import type { Locator, Page } from '@playwright/test';
import { AILabBasePage } from './ai-lab-base-page';
import { AILabRecipesCatalogPage } from './ai-lab-recipes-catalog-page';

export class AILabNavigationBar extends AILabBasePage {
readonly navigationBar: Locator;
readonly recipesCatalogButton: Locator;
readonly runningAppsButton: Locator;
readonly catalogButton: Locator;
readonly servicesButton: Locator;
readonly playgroundsButton: Locator;

constructor(page: Page, webview: Page) {
super(page, webview, undefined);
this.navigationBar = this.webview.getByRole('navigation', { name: 'PreferencesNavigation' });
this.recipesCatalogButton = this.navigationBar.getByRole('link', { name: 'Recipes Catalog', exact: true });
this.runningAppsButton = this.navigationBar.getByRole('link', { name: 'Running' });
this.catalogButton = this.navigationBar.getByRole('link', { name: 'Catalog', exact: true });
this.servicesButton = this.navigationBar.getByRole('link', { name: 'Services' });
this.playgroundsButton = this.navigationBar.getByRole('link', { name: 'Playgrounds' });
}

async waitForLoad(): Promise<void> {
await playExpect(this.navigationBar).toBeVisible();
}

async openRecipesCatalog(): Promise<AILabRecipesCatalogPage> {
await playExpect(this.recipesCatalogButton).toBeEnabled();
await this.recipesCatalogButton.click();
return new AILabRecipesCatalogPage(this.page, this.webview);
}
}
36 changes: 36 additions & 0 deletions tests/playwright/src/model/ai-lab-page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**********************************************************************
* 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 { Page } from '@playwright/test';
import { expect as playExpect } from '@playwright/test';
import { AILabBasePage } from './ai-lab-base-page';
import { AILabNavigationBar } from './ai-lab-navigation-bar';

export class AILabPage extends AILabBasePage {
readonly navigationBar: AILabNavigationBar;

constructor(page: Page, webview: Page) {
super(page, webview, 'Welcome to Podman AI Lab');
this.navigationBar = new AILabNavigationBar(this.page, this.webview);
}

async waitForLoad(): Promise<void> {
await playExpect(this.heading).toBeVisible();
await this.navigationBar.waitForLoad();
}
}
63 changes: 63 additions & 0 deletions tests/playwright/src/model/ai-lab-recipes-catalog-page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**********************************************************************
* 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 { expect as playExpect } from '@playwright/test';
import type { Locator, Page } from '@playwright/test';
import { AILabBasePage } from './ai-lab-base-page';
import { AILabAppDetailsPage } from './ai-lab-app-details-page';

export class AILabRecipesCatalogPage extends AILabBasePage {
readonly recipesCatalogPage: Locator;
readonly recipesCatalogContent: Locator;
readonly recipesCatalogNaturalLanguageProcessing: Locator;
readonly recipesCatalogAudio: Locator;
readonly recipesCatalogComputerVision: Locator;

constructor(page: Page, webview: Page) {
super(page, webview, 'Recipe Catalog');
this.recipesCatalogPage = this.webview.getByRole('region', { name: 'Recipe Catalog' });
this.recipesCatalogContent = this.recipesCatalogPage.getByRole('region', { name: 'content', exact: true }).first();
this.recipesCatalogNaturalLanguageProcessing = this.recipesCatalogContent.getByRole('region', {
name: 'Natural Language Processing',
exact: true,
});
this.recipesCatalogAudio = this.recipesCatalogContent.getByRole('region', { name: 'Audio', exact: true });
this.recipesCatalogComputerVision = this.recipesCatalogContent.getByRole('region', {
name: 'Computer Vision',
exact: true,
});
}

async waitForLoad(): Promise<void> {
await playExpect(this.heading).toBeVisible();
await playExpect(this.recipesCatalogPage).toBeVisible();
}

async openRecipesCatalogApp(category: Locator, appName: string): Promise<AILabAppDetailsPage> {
await playExpect(category).toBeVisible();
await playExpect(this.getAppDetailsLocator(appName)).toBeEnabled();
await this.getAppDetailsLocator(appName).click();
return new AILabAppDetailsPage(this.page, this.webview, appName);
}

private getAppDetailsLocator(appName: string): Locator {
return this.recipesCatalogContent
.getByRole('region', { name: appName, exact: true })
.getByRole('button', { name: 'More details' });
}
}
54 changes: 54 additions & 0 deletions tests/playwright/src/model/ai-lab-start-recipe-page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**********************************************************************
* 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 { expect as playExpect } from '@playwright/test';
import type { Locator, Page } from '@playwright/test';
import { AILabBasePage } from './ai-lab-base-page';
import { handleConfirmationDialog } from '@podman-desktop/tests-playwright';

export class AILabStartRecipePage extends AILabBasePage {
readonly recipeStatus: Locator;
readonly applicationDetailsPanel: Locator;
readonly startRecipeButton: Locator;
readonly openAIAppButton: Locator;
readonly deleteAIAppButton: Locator;

constructor(page: Page, webview: Page) {
super(page, webview, 'Start Recipe');
this.recipeStatus = this.webview.getByRole('status');
this.applicationDetailsPanel = this.webview.getByLabel('application details panel');
this.startRecipeButton = this.webview.getByRole('button', { name: /Start(\s+([A-Za-z]+\s+)+)recipe/i });
this.openAIAppButton = this.applicationDetailsPanel.getByRole('button', { name: 'Open AI App' });
this.deleteAIAppButton = this.applicationDetailsPanel.getByRole('button', { name: 'Delete AI App' });
}

async waitForLoad(): Promise<void> {
await playExpect(this.heading).toBeVisible();
}

async startRecipe(): Promise<void> {
await playExpect(this.startRecipeButton).toBeEnabled();
await this.startRecipeButton.click();
try {
await handleConfirmationDialog(this.page, 'Podman AI Lab', true, 'Reset');
} catch (error) {
console.warn(`Warning: Could not reset the app, repository probably clean.\n\t${error}`);
}
await playExpect(this.recipeStatus).toContainText('AI App is running', { timeout: 720_000 });
}
}

0 comments on commit 59ef588

Please sign in to comment.