diff --git a/tests/playwright/src/ai-lab-extension.spec.ts b/tests/playwright/src/ai-lab-extension.spec.ts index e9b06ec25..c2c09d544 100644 --- a/tests/playwright/src/ai-lab-extension.spec.ts +++ b/tests/playwright/src/ai-lab-extension.spec.ts @@ -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; @@ -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]; +} diff --git a/tests/playwright/src/model/ai-lab-app-details-page.ts b/tests/playwright/src/model/ai-lab-app-details-page.ts new file mode 100644 index 000000000..24099b578 --- /dev/null +++ b/tests/playwright/src/model/ai-lab-app-details-page.ts @@ -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 { + await playExpect(this.heading).toBeVisible(); + } + + async deleteLocalClone(): Promise { + throw new Error('Method Not implemented'); + } + + async startNewDeployment(): Promise { + 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 { + throw new Error('Method Not implemented'); + } + + async deleteRunningApp(_containerName: string): Promise { + throw new Error('Method Not implemented'); + } +} diff --git a/tests/playwright/src/model/ai-lab-base-page.ts b/tests/playwright/src/model/ai-lab-base-page.ts new file mode 100644 index 000000000..3b6bb25bf --- /dev/null +++ b/tests/playwright/src/model/ai-lab-base-page.ts @@ -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; +} diff --git a/tests/playwright/src/model/ai-lab-navigation-bar.ts b/tests/playwright/src/model/ai-lab-navigation-bar.ts new file mode 100644 index 000000000..c313efdfa --- /dev/null +++ b/tests/playwright/src/model/ai-lab-navigation-bar.ts @@ -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 { + await playExpect(this.navigationBar).toBeVisible(); + } + + async openRecipesCatalog(): Promise { + await playExpect(this.recipesCatalogButton).toBeEnabled(); + await this.recipesCatalogButton.click(); + return new AILabRecipesCatalogPage(this.page, this.webview); + } +} diff --git a/tests/playwright/src/model/ai-lab-page.ts b/tests/playwright/src/model/ai-lab-page.ts new file mode 100644 index 000000000..ca8348f9f --- /dev/null +++ b/tests/playwright/src/model/ai-lab-page.ts @@ -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 { + await playExpect(this.heading).toBeVisible(); + await this.navigationBar.waitForLoad(); + } +} diff --git a/tests/playwright/src/model/ai-lab-recipes-catalog-page.ts b/tests/playwright/src/model/ai-lab-recipes-catalog-page.ts new file mode 100644 index 000000000..67b4125b5 --- /dev/null +++ b/tests/playwright/src/model/ai-lab-recipes-catalog-page.ts @@ -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 { + await playExpect(this.heading).toBeVisible(); + await playExpect(this.recipesCatalogPage).toBeVisible(); + } + + async openRecipesCatalogApp(category: Locator, appName: string): Promise { + 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' }); + } +} diff --git a/tests/playwright/src/model/ai-lab-start-recipe-page.ts b/tests/playwright/src/model/ai-lab-start-recipe-page.ts new file mode 100644 index 000000000..813276532 --- /dev/null +++ b/tests/playwright/src/model/ai-lab-start-recipe-page.ts @@ -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 { + await playExpect(this.heading).toBeVisible(); + } + + async startRecipe(): Promise { + 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 }); + } +}