diff --git a/tests/playwright/src/ai-lab-extension.spec.ts b/tests/playwright/src/ai-lab-extension.spec.ts index 65de008cc..546cd6a2b 100644 --- a/tests/playwright/src/ai-lab-extension.spec.ts +++ b/tests/playwright/src/ai-lab-extension.spec.ts @@ -32,6 +32,7 @@ import { AILabExtensionDetailsPage } from './model/podman-extension-ai-lab-detai import type { AILabCatalogPage } from './model/ai-lab-catalog-page'; import { handleWebview } from './utils/webviewHandler'; import type { AILabServiceDetailsPage } from './model/ai-lab-service-details-page'; +import type { AILabPlaygroundsPage } from './model/ai-lab-playgrounds-page'; const AI_LAB_EXTENSION_OCI_IMAGE = process.env.EXTENSION_OCI_IMAGE ?? 'ghcr.io/containers/podman-desktop-extension-ai-lab:nightly'; @@ -204,6 +205,63 @@ test.describe.serial(`AI Lab extension installation and verification`, { tag: '@ }); }); + ['instructlab/granite-7b-lab-GGUF'].forEach(modelName => { + test.describe.serial(`AI Lan playground creation and deletion`, () => { + let catalogPage: AILabCatalogPage; + let playgroundsPage: AILabPlaygroundsPage; + + const playgroundName = 'test playground'; + + test.skip(isLinux, `Skipping model service creation on Linux`); + test.beforeAll(`Open AI Lab Catalog`, async ({ runner, page, navigationBar }) => { + [page, webview] = await handleWebview(runner, page, navigationBar); + aiLabPage = new AILabPage(page, webview); + await aiLabPage.navigationBar.waitForLoad(); + + catalogPage = await aiLabPage.navigationBar.openCatalog(); + await catalogPage.waitForLoad(); + }); + + test(`Download ${modelName} model if not available`, async () => { + test.setTimeout(310_000); + if (!(await catalogPage.isModelDownloaded(modelName))) { + await catalogPage.downloadModel(modelName); + } + await playExpect + // eslint-disable-next-line sonarjs/no-nested-functions + .poll(async () => await waitForCatalogModel(modelName), { timeout: 300_000, intervals: [5_000] }) + .toBeTruthy(); + }); + + test(`Create AI Lab playground for ${modelName}`, async () => { + playgroundsPage = await aiLabPage.navigationBar.openPlaygrounds(); + await playgroundsPage.waitForLoad(); + + await playgroundsPage.createNewPlayground(playgroundName); + await playgroundsPage.waitForLoad(); + await playExpect + // eslint-disable-next-line sonarjs/no-nested-functions + .poll(async () => await playgroundsPage.doesPlaygroundExist(playgroundName), { timeout: 60_000 }) + .toBeTruthy(); + }); + + test(`Delete AI Lab playground for ${modelName}`, async () => { + await playgroundsPage.deletePlayground(playgroundName); + await playgroundsPage.waitForLoad(); + + await playExpect + // eslint-disable-next-line sonarjs/no-nested-functions + .poll(async () => await playgroundsPage.doesPlaygroundExist(playgroundName), { timeout: 60_000 }) + .toBeFalsy(); + }); + + test.afterAll(`Cleaning up service model`, async () => { + test.setTimeout(60_000); + await cleanupServiceModels(); + }); + }); + }); + ['Audio to Text', 'ChatBot', 'Summarizer', 'Code Generation'].forEach(appName => { test.describe.serial(`AI Recipe installation`, () => { let recipesCatalogPage: AILabRecipesCatalogPage; diff --git a/tests/playwright/src/model/ai-lab-navigation-bar.ts b/tests/playwright/src/model/ai-lab-navigation-bar.ts index 640317a92..b4e95b9f2 100644 --- a/tests/playwright/src/model/ai-lab-navigation-bar.ts +++ b/tests/playwright/src/model/ai-lab-navigation-bar.ts @@ -23,6 +23,7 @@ import { AILabRecipesCatalogPage } from './ai-lab-recipes-catalog-page'; import { AiRunningAppsPage } from './ai-lab-running-apps-page'; import { AiModelServicePage } from './ai-lab-model-service-page'; import { AILabCatalogPage } from './ai-lab-catalog-page'; +import { AILabPlaygroundsPage } from './ai-lab-playgrounds-page'; export class AILabNavigationBar extends AILabBasePage { readonly navigationBar: Locator; @@ -71,4 +72,10 @@ export class AILabNavigationBar extends AILabBasePage { await this.catalogButton.click(); return new AILabCatalogPage(this.page, this.webview); } + + async openPlaygrounds(): Promise { + await playExpect(this.playgroundsButton).toBeEnabled(); + await this.playgroundsButton.click(); + return new AILabPlaygroundsPage(this.page, this.webview); + } } diff --git a/tests/playwright/src/model/ai-lab-playgrounds-page.ts b/tests/playwright/src/model/ai-lab-playgrounds-page.ts new file mode 100644 index 000000000..d424aaa50 --- /dev/null +++ b/tests/playwright/src/model/ai-lab-playgrounds-page.ts @@ -0,0 +1,76 @@ +/********************************************************************** + * 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 { AILabBasePage } from './ai-lab-base-page'; +import { handleConfirmationDialog } from '@podman-desktop/tests-playwright'; + +export class AILabPlaygroundsPage extends AILabBasePage { + readonly additionalActions: Locator; + readonly newPlaygroundButton: Locator; + readonly playgroundNameInput: Locator; + readonly createPlaygroundButton: Locator; + + constructor(page: Page, webview: Page) { + super(page, webview, 'Playground Environments'); + this.additionalActions = this.webview.getByRole('group', { name: 'additionalActions' }); + this.newPlaygroundButton = this.additionalActions.getByRole('button', { name: 'New Playground', exact: true }); + this.playgroundNameInput = this.webview.getByRole('textbox', { name: 'playgroundName' }); + this.createPlaygroundButton = this.webview.getByRole('button', { name: 'Create playground', exact: true }); + } + + async waitForLoad(): Promise { + await playExpect(this.heading).toBeVisible(); + } + + async createNewPlayground(name: string): Promise { + await playExpect(this.newPlaygroundButton).toBeEnabled(); + await this.newPlaygroundButton.click(); + await playExpect(this.playgroundNameInput).toBeVisible(); + await this.playgroundNameInput.fill(name); + await playExpect(this.playgroundNameInput).toHaveValue(name); + await playExpect(this.createPlaygroundButton).toBeEnabled(); + await this.createPlaygroundButton.click(); + return this; + } + + async deletePlayground(playgroundName: string): Promise { + const playgroundRow = await this.getPlaygroundRowByName(playgroundName); + if (!playgroundRow) { + throw new Error(`Playground ${playgroundName} not found`); + } + const deleteButton = playgroundRow.getByRole('button', { name: 'Delete conversation', exact: true }); + await playExpect(deleteButton).toBeEnabled(); + await deleteButton.click(); + await handleConfirmationDialog(this.page, 'Podman AI Lab', true, 'Confirm'); + return this; + } + + async doesPlaygroundExist(playgroundName: string): Promise { + return (await this.getPlaygroundRowByName(playgroundName)) !== undefined; + } + + private async getPlaygroundRowByName(playgroundName: string): Promise { + const row = this.webview.getByRole('row', { name: playgroundName, exact: true }); + if ((await row.count()) > 0) { + return row; + } + return undefined; + } +}