Skip to content

Commit

Permalink
chore(test): initial draft for catalog model download e2e (#2133)
Browse files Browse the repository at this point in the history
* chore(test): initial draft for catalog model download e2e

Signed-off-by: Vladimir Lazar <[email protected]>
  • Loading branch information
cbr7 authored Nov 25, 2024
1 parent c41bed4 commit 40ab7d5
Show file tree
Hide file tree
Showing 5 changed files with 205 additions and 33 deletions.
79 changes: 47 additions & 32 deletions tests/playwright/src/ai-lab-extension.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
***********************************************************************/

import type { Page } from '@playwright/test';
import type { NavigationBar, ExtensionsPage, Runner } from '@podman-desktop/tests-playwright';
import type { NavigationBar, ExtensionsPage } from '@podman-desktop/tests-playwright';
import {
expect as playExpect,
test,
Expand All @@ -29,15 +29,15 @@ import {
import { AILabPage } from './model/ai-lab-page';
import type { AILabRecipesCatalogPage } from './model/ai-lab-recipes-catalog-page';
import { AILabExtensionDetailsPage } from './model/podman-extension-ai-lab-details-page';
import type { AILabCatalogPage } from './model/ai-lab-catalog-page';
import { handleWebview } from './utils/webviewHandler';

const AI_LAB_EXTENSION_OCI_IMAGE =
process.env.EXTENSION_OCI_IMAGE ?? 'ghcr.io/containers/podman-desktop-extension-ai-lab:nightly';
const AI_LAB_EXTENSION_PREINSTALLED: boolean = process.env.EXTENSION_PREINSTALLED === 'true';
const AI_LAB_CATALOG_EXTENSION_LABEL: string = 'redhat.ai-lab';
const AI_LAB_CATALOG_EXTENSION_NAME: string = 'Podman AI Lab extension';
const AI_LAB_CATALOG_STATUS_ACTIVE: string = 'ACTIVE';
const AI_LAB_NAVBAR_EXTENSION_LABEL: string = 'AI Lab';
const AI_LAB_PAGE_BODY_LABEL: string = 'Webview AI Lab';

let webview: Page;
let aiLabPage: AILabPage;
Expand Down Expand Up @@ -109,14 +109,43 @@ test.describe.serial(`AI Lab extension installation and verification`, { tag: '@
});
});

[
'Audio to Text',
'ChatBot',
'Summarizer',
'Code Generation',
/* 'Object Detection', */ // Object detection does not work without model upload
/* RAG Chatbot seems */ // too demanding on resources
].forEach(appName => {
['ggerganov/whisper.cpp', 'facebook/detr-resnet-101'].forEach(modelName => {
test.describe.serial(`Model download and deletion`, () => {
let catalogPage: AILabCatalogPage;

test.beforeEach(`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`, async () => {
test.setTimeout(310_000);
playExpect(await catalogPage.isModelDownloaded(modelName)).toBeFalsy();
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(`Delete ${modelName} model`, async () => {
test.skip(isWindows, 'Model deletion is currently very buggy in azure cicd');
test.setTimeout(310_000);
playExpect(await catalogPage.isModelDownloaded(modelName)).toBeTruthy();
await catalogPage.deleteModel(modelName);
await playExpect
// eslint-disable-next-line sonarjs/no-nested-functions
.poll(async () => await waitForCatalogModel(modelName), { timeout: 300_000, intervals: [2_500] })
.toBeFalsy();
});
});
});

['Audio to Text', 'ChatBot', 'Summarizer', 'Code Generation'].forEach(appName => {
test.describe.serial(`AI Recipe installation`, () => {
let recipesCatalogPage: AILabRecipesCatalogPage;

Expand Down Expand Up @@ -181,26 +210,12 @@ async function deleteUnusedImages(navigationBar: NavigationBar): Promise<void> {
await playExpect.poll(async () => await imagesPage.getCountOfImagesByStatus('UNUSED'), { timeout: 60_000 }).toBe(0);
}

async function handleWebview(runner: Runner, page: Page, navigationBar: NavigationBar): 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] = runner.getElectronApp().windows();
await mainPage.evaluate(() => {
const element = document.querySelector('webview');
if (element) {
(element as HTMLElement).focus();
} else {
console.log(`element is null`);
}
});
async function waitForCatalogModel(modelName: string): Promise<boolean> {
const recipeCatalogOage = await aiLabPage.navigationBar.openRecipesCatalog();
await recipeCatalogOage.waitForLoad();

const catalogPage = await aiLabPage.navigationBar.openCatalog();
await catalogPage.waitForLoad();

return [mainPage, webViewPage];
return await catalogPage.isModelDownloaded(modelName);
}
2 changes: 1 addition & 1 deletion tests/playwright/src/model/ai-lab-base-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import type { Locator, Page } from '@playwright/test';
export abstract class AILabBasePage {
readonly page: Page;
readonly webview: Page;
heading: Locator;
readonly heading: Locator;

constructor(page: Page, webview: Page, heading: string | undefined) {
this.page = page;
Expand Down
102 changes: 102 additions & 0 deletions tests/playwright/src/model/ai-lab-catalog-page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/**********************************************************************
* 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 AILabCatalogPage extends AILabBasePage {
readonly catalogTable: Locator;
readonly modelsGroup: Locator;

constructor(page: Page, webview: Page) {
super(page, webview, 'Models');
this.catalogTable = this.webview.getByRole('table', { name: 'model' });
this.modelsGroup = this.catalogTable.getByRole('rowgroup').nth(1);
}

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

async getModelRowByName(modelName: string): Promise<Locator | undefined> {
const modelRows = await this.getAllModelRows();
for (const modelRow of modelRows) {
const modelNameCell = modelRow.getByText(modelName, { exact: true });
if ((await modelNameCell.count()) > 0) {
return modelRow;
}
}

return undefined;
}

async downloadModel(modelName: string): Promise<void> {
const modelRow = await this.getModelRowByName(modelName);
if (!modelRow) {
throw new Error(`Model ${modelName} not found`);
}
const downloadButton = modelRow.getByRole('button', { name: 'Download Model' });
await playExpect(downloadButton).toBeEnabled();
await downloadButton.focus();
await downloadButton.click();
}

async createModelService(modelName: string): Promise<void> {
const modelRow = await this.getModelRowByName(modelName);
if (!modelRow) {
throw new Error(`Model ${modelName} not found`);
}
const createServiceButton = modelRow.getByRole('button', { name: 'Create Model Service' });
await playExpect(createServiceButton).toBeEnabled();
await createServiceButton.focus();
await createServiceButton.click();

throw new Error('Not implemented');
}

async deleteModel(modelName: string): Promise<void> {
const modelRow = await this.getModelRowByName(modelName);
if (!modelRow) {
throw new Error(`Model ${modelName} not found`);
}
const deleteButton = modelRow.getByRole('button', { name: 'Delete Model' });
await playExpect(deleteButton).toBeEnabled();
await deleteButton.focus();
await deleteButton.click();
await this.page.waitForTimeout(1_000);
await handleConfirmationDialog(this.page, 'Podman AI Lab', true, 'Confirm');
}

async isModelDownloaded(modelName: string): Promise<boolean> {
const modelRow = await this.getModelRowByName(modelName);
if (!modelRow) {
return false;
}

const deleteButton = modelRow.getByRole('button', { name: 'Delete Model' });
return (await deleteButton.count()) > 0;
}

private async getAllModelRows(): Promise<Locator[]> {
return this.modelsGroup.getByRole('row').all();
}
}
7 changes: 7 additions & 0 deletions tests/playwright/src/model/ai-lab-navigation-bar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { AILabBasePage } from './ai-lab-base-page';
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';

export class AILabNavigationBar extends AILabBasePage {
readonly navigationBar: Locator;
Expand Down Expand Up @@ -64,4 +65,10 @@ export class AILabNavigationBar extends AILabBasePage {
await this.servicesButton.click();
return new AiModelServicePage(this.page, this.webview);
}

async openCatalog(): Promise<AILabCatalogPage> {
await playExpect(this.catalogButton).toBeEnabled();
await this.catalogButton.click();
return new AILabCatalogPage(this.page, this.webview);
}
}
48 changes: 48 additions & 0 deletions tests/playwright/src/utils/webviewHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**********************************************************************
* 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 type { NavigationBar, Runner } from '@podman-desktop/tests-playwright';
import { expect as playExpect } from '@podman-desktop/tests-playwright';

export async function handleWebview(runner: Runner, page: Page, navigationBar: NavigationBar): Promise<[Page, Page]> {
const AI_LAB_NAVBAR_EXTENSION_LABEL: string = 'AI Lab';
const AI_LAB_PAGE_BODY_LABEL: string = 'Webview AI Lab';

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] = runner.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];
}

0 comments on commit 40ab7d5

Please sign in to comment.