Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions WebUI/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,10 @@ backend-shared/
.qwen/
.opencode/
skills-lock.json

# Playwright
test-results/
playwright-report/
playwright-report-e2e/
blob-report/
playwright/.cache/
5 changes: 4 additions & 1 deletion WebUI/.prettierignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
src/auto-import.d.ts
workflows_intel/
*.whl
*.whl
test-results/
playwright-report/
playwright-report-e2e/
160 changes: 160 additions & 0 deletions WebUI/e2e-real/app-lifecycle.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { test, expect } from './fixtures'

test.describe.serial('Full App Lifecycle', () => {
test('app shows the setup wizard on fresh start', async ({ window }) => {
const wizardTitle = window.getByText('AI Playground Setup')
await expect(wizardTitle).toBeVisible({ timeout: 30_000 })
})

test('setup wizard displays hardware modes', async ({ window }) => {
const wizard = window.getByText('AI Playground Setup')
await expect(wizard).toBeVisible({ timeout: 30_000 })

await expect(window.getByText('HARDWARE MODE')).toBeVisible()
const modeLabels = window.locator('label').filter({ hasText: /PLAYGROUND/ })
const count = await modeLabels.count()
expect(count).toBeGreaterThanOrEqual(2)
})

test('setup wizard displays backend components', async ({ window }) => {
await expect(window.getByText('AI Playground Setup')).toBeVisible({ timeout: 30_000 })

const componentHeading = window.locator('h2').filter({ hasText: 'COMPONENTS' })
await expect(componentHeading).toBeVisible({ timeout: 10_000 })
await expect(window.getByText('Llama.cpp - GGUF')).toBeVisible()
})

test('can select essentials mode', async ({ window }) => {
await expect(window.getByText('AI Playground Setup')).toBeVisible({ timeout: 30_000 })

const essentialsLabel = window.locator('label').filter({ hasText: 'essentials' })
await essentialsLabel.click()
await expect(essentialsLabel).toHaveClass(/border-primary/)
})

test('"Install & Continue" transitions to running state', async ({ window }) => {
await expect(window.getByText('AI Playground Setup')).toBeVisible({ timeout: 30_000 })

// Toggle off unsupported backends (OpenVINO, ComfyUI) that fail on Linux
const switches = window.locator('button[role="switch"]')
const switchCount = await switches.count()
for (let i = 0; i < switchCount; i++) {
const sw = switches.nth(i)
const row = sw.locator('xpath=ancestor::div[contains(@class,"rounded-lg")]').first()
const rowText = await row.innerText()
if (
(rowText.includes('OpenVINO') || rowText.includes('ComfyUI')) &&
(await sw.getAttribute('data-state')) === 'checked'
) {
await sw.click()
}
}

const installButton = window.getByRole('button', { name: /Install & Continue|Continue/ })
await installButton.click()

const promptArea = window.locator('#prompt-area')
const continueButton = window.getByRole('button', { name: 'Continue' })
await expect(promptArea.or(continueButton)).toBeVisible({ timeout: 5 * 60_000 })

if (await continueButton.isVisible()) {
await continueButton.click()
}
await expect(promptArea).toBeVisible({ timeout: 30_000 })
})

test('main app shows header, prompt input, and footer', async ({ window }) => {
// This test gets a fresh Electron instance, ensure it reaches running state
const promptArea = window.locator('#prompt-area')
const wizard = window.getByText('AI Playground Setup')
await expect(promptArea.or(wizard)).toBeVisible({ timeout: 30_000 })

if (await wizard.isVisible()) {
const btn = window.getByRole('button', { name: /Continue/ })
if (await btn.isVisible()) await btn.click()
}
await expect(promptArea).toBeVisible({ timeout: 30_000 })

const header = window.locator('header.main-title')
await expect(header).toContainText('AI')
await expect(header).toContainText('PLAYGROUND')
await expect(window.locator('#prompt-input')).toBeVisible()
await expect(window.locator('#send-button')).toBeVisible()
await expect(window.getByText('AI Playground version:')).toBeVisible()
})

test('can type in the prompt textarea', async ({ window }) => {
const promptArea = window.locator('#prompt-area')
const wizard = window.getByText('AI Playground Setup')
await expect(promptArea.or(wizard)).toBeVisible({ timeout: 30_000 })
if (await wizard.isVisible()) {
const btn = window.getByRole('button', { name: /Continue/ })
if (await btn.isVisible()) await btn.click()
}
await expect(promptArea).toBeVisible({ timeout: 30_000 })

const textarea = window.locator('#prompt-input')
await textarea.fill('Hello world')
await expect(textarea).toHaveValue('Hello world')
await textarea.fill('')
})

test('can open and close the history panel', async ({ window }) => {
const promptArea = window.locator('#prompt-area')
const wizard = window.getByText('AI Playground Setup')
await expect(promptArea.or(wizard)).toBeVisible({ timeout: 30_000 })
if (await wizard.isVisible()) {
const btn = window.getByRole('button', { name: /Continue/ })
if (await btn.isVisible()) await btn.click()
}
await expect(promptArea).toBeVisible({ timeout: 30_000 })

const historyButton = window.locator('#show-history-button')
await historyButton.click()
const historyPanel = window.locator('#history-panel')
await expect(historyPanel).toBeVisible({ timeout: 5_000 })

const closeButton = historyPanel.locator('.i-close')
await closeButton.click({ force: true })
await expect(historyButton).toBeVisible({ timeout: 5_000 })
})

test('can open and close the app settings sidebar', async ({ window }) => {
const promptArea = window.locator('#prompt-area')
const wizard = window.getByText('AI Playground Setup')
await expect(promptArea.or(wizard)).toBeVisible({ timeout: 30_000 })
if (await wizard.isVisible()) {
const btn = window.getByRole('button', { name: /Continue/ })
if (await btn.isVisible()) await btn.click()
}
await expect(promptArea).toBeVisible({ timeout: 30_000 })

await window.locator('#app-settings-button button').click()
const sidebar = window.locator('#app-settings-sidebar')
await expect(sidebar).toBeVisible({ timeout: 5_000 })
await expect(sidebar).toContainText('App Settings')
await expect(sidebar).toContainText('Backend Status')

const closeBtn = sidebar.locator('.i-close')
await closeBtn.click({ force: true })
await expect(sidebar).not.toBeVisible({ timeout: 5_000 })
})

test('footer can be hidden and shown', async ({ window }) => {
const promptArea = window.locator('#prompt-area')
const wizard = window.getByText('AI Playground Setup')
await expect(promptArea.or(wizard)).toBeVisible({ timeout: 30_000 })
if (await wizard.isVisible()) {
const btn = window.getByRole('button', { name: /Continue/ })
if (await btn.isVisible()) await btn.click()
}
await expect(promptArea).toBeVisible({ timeout: 30_000 })

await expect(window.getByText('AI Playground version:')).toBeVisible({ timeout: 10_000 })
await window.getByText('HIDE FOOTER').click()
await expect(window.getByText('AI Playground version:')).not.toBeVisible({ timeout: 3_000 })

await window.getByText('SHOW FOOTER').click()
await expect(window.getByText('AI Playground version:')).toBeVisible({ timeout: 3_000 })
})
})
121 changes: 121 additions & 0 deletions WebUI/e2e-real/chat-inference.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { test as base, expect, type ElectronApplication, type Page } from '@playwright/test'
import { launchElectronApp } from './fixtures'
import { getMainWindow } from './helpers'

async function ensureRunningState(window: Page): Promise<void> {
const promptArea = window.locator('#prompt-area')
const wizardTitle = window.getByText('AI Playground Setup')
await expect(promptArea.or(wizardTitle)).toBeVisible({ timeout: 30_000 })

if (await wizardTitle.isVisible()) {
const installButton = window.getByRole('button', { name: /Install & Continue/ })
const continueButton = window.getByRole('button', { name: 'Continue' })

if (await installButton.isVisible()) {
await installButton.click()
await expect(promptArea.or(continueButton)).toBeVisible({ timeout: 5 * 60_000 })
}
if (await continueButton.isVisible()) {
await continueButton.click()
}
}
await expect(promptArea).toBeVisible({ timeout: 30_000 })
}

let electronApp: ElectronApplication
let window: Page

base.describe.serial('Chat Inference End-to-End', () => {
base.beforeAll(async () => {
electronApp = await launchElectronApp()
window = await getMainWindow(electronApp)
await ensureRunningState(window)
})

base.afterAll(async () => {
if (electronApp) await electronApp.close()
})

base('select the test model via chat settings', async () => {
await window.locator('#advanced-settings-button').click()
const sidebar = window.locator('#advanced-settings-sidebar')
await expect(sidebar).toBeVisible({ timeout: 5_000 })

const modelLabel = sidebar.getByText('Model', { exact: true })
await expect(modelLabel).toBeVisible()

// The model dropdown trigger is in the same grid row as the Model label
const modelRow = sidebar
.locator('.grid')
.filter({ has: sidebar.getByText('Model', { exact: true }) })
const trigger = modelRow.locator('button').first()
await trigger.click()

const testModelItem = window.getByText('LFM2.5-350M', { exact: false }).first()
await expect(testModelItem).toBeVisible({ timeout: 10_000 })
await testModelItem.click()

const closeBtn = sidebar.locator('.i-close')
await closeBtn.click({ force: true })
await expect(sidebar).not.toBeVisible({ timeout: 5_000 })
})

base('send a message and receive a streamed response', async () => {
const textarea = window.locator('#prompt-input')
await textarea.fill('Say hello in one short sentence.')
await window.locator('#send-button').click()

// Wait for the chat panel (model may need to download ~255MB + load)
const chatPanel = window.locator('#chatPanel')
await expect(chatPanel).toBeVisible({ timeout: 5 * 60_000 })

// Wait for assistant response
const assistantIcon = chatPanel.locator('img[src*="ai-icon"]').first()
await expect(assistantIcon).toBeVisible({ timeout: 5 * 60_000 })
})

base('user message is visible in the chat', async () => {
const chatPanel = window.locator('#chatPanel')
await expect(chatPanel.getByText('Say hello in one short sentence.')).toBeVisible({
timeout: 10_000,
})
})

base('assistant response contains text', async () => {
const chatPanel = window.locator('#chatPanel')
const assistantBlocks = chatPanel.locator('.flex.items-start.gap-3').last()
const text = await assistantBlocks.innerText()
expect(text.length).toBeGreaterThan(10)
})

base('copy button is available on messages', async () => {
const chatPanel = window.locator('#chatPanel')
const copyButton = chatPanel.getByText('Copy').last()
await expect(copyButton).toBeVisible({ timeout: 5_000 })
})

base('can send a follow-up message', async () => {
const textarea = window.locator('#prompt-input')
await textarea.fill('What is 2 + 2?')
await window.locator('#send-button').click()

const chatPanel = window.locator('#chatPanel')
const aiIcons = chatPanel.locator('img[src*="ai-icon"]')
await expect(aiIcons).toHaveCount(2, { timeout: 5 * 60_000 })
})

base('conversation is listed in history', async () => {
const historyButton = window.locator('#show-history-button')
await historyButton.click()

const historyPanel = window.locator('#history-panel')
await expect(historyPanel).toBeVisible({ timeout: 5_000 })

await expect(historyPanel.getByText('hello', { exact: false })).toBeVisible({
timeout: 10_000,
})

const closeBtn = historyPanel.locator('.i-close')
await closeBtn.click({ force: true })
})
})
Loading
Loading