mirror of
https://github.com/mudler/LocalAI.git
synced 2026-10-28 09:08:54 -04:00
test(ui): drive the media history and TTS specs through the Studio add-on
The image, video, sound and speech generators are pages of the Studio add-on now, and the old addresses redirect to it. Install the add-on with the Studio fixtures, open its pages and use its compose card, so the specs run against the page that exists. Assisted-by: Claude Code:claude-sonnet-5-5 playwright Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
2 files changed
+54
-44
No files matched your search
@@ -1,12 +1,11 @@
|
||||
import { test, expect } from './coverage-fixtures.js'
|
||||
import { mockStudio, stubMedia } from './studio-fixtures.js'
|
||||
|
||||
function mockCapabilities(page, capabilities) {
|
||||
return page.route('**/api/models/capabilities', (route) => {
|
||||
route.fulfill({
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ data: capabilities }),
|
||||
})
|
||||
})
|
||||
// The generators are pages of the Studio add-on: install it, and serve the
|
||||
// models the test names through the capabilities call it reads.
|
||||
async function mockCapabilities(page, capabilities) {
|
||||
await mockStudio(page, { capabilityModels: capabilities })
|
||||
await stubMedia(page)
|
||||
}
|
||||
|
||||
function mockImageGeneration(page, images) {
|
||||
@@ -73,11 +72,11 @@ test.describe('Media History - Image Generation', () => {
|
||||
test('history entry appears after image generation', async ({ page }) => {
|
||||
await mockImageGeneration(page, [{ url: '/generated-images/test.png' }])
|
||||
|
||||
await page.goto('/app/image')
|
||||
await page.goto('/app/studio/images')
|
||||
await expect(page.getByRole('button', { name: 'test-image-model' })).toBeVisible({ timeout: 10_000 })
|
||||
|
||||
await page.locator('.textarea').first().fill('a beautiful sunset')
|
||||
await page.locator('button[type="submit"]').click()
|
||||
await page.locator('[data-testid="ws-compose"] textarea').first().fill('a beautiful sunset')
|
||||
await page.locator('[data-testid="ws-compose"] button[type="submit"]').click()
|
||||
|
||||
// Image should appear in preview
|
||||
await expect(page.locator('.media-result-grid img')).toBeVisible({ timeout: 10_000 })
|
||||
@@ -100,17 +99,17 @@ test.describe('Media History - Image Generation', () => {
|
||||
})
|
||||
})
|
||||
|
||||
await page.goto('/app/image')
|
||||
await page.goto('/app/studio/images')
|
||||
await expect(page.getByRole('button', { name: 'test-image-model' })).toBeVisible({ timeout: 10_000 })
|
||||
|
||||
// Generate first image
|
||||
await page.locator('.textarea').first().fill('first prompt')
|
||||
await page.locator('button[type="submit"]').click()
|
||||
await page.locator('[data-testid="ws-compose"] textarea').first().fill('first prompt')
|
||||
await page.locator('[data-testid="ws-compose"] button[type="submit"]').click()
|
||||
await expect(page.locator('.media-result-grid img')).toBeVisible({ timeout: 10_000 })
|
||||
|
||||
// Generate second image
|
||||
await page.locator('.textarea').first().fill('second prompt')
|
||||
await page.locator('button[type="submit"]').click()
|
||||
await page.locator('[data-testid="ws-compose"] textarea').first().fill('second prompt')
|
||||
await page.locator('[data-testid="ws-compose"] button[type="submit"]').click()
|
||||
await expect(page.locator('.media-result-grid img[src="/generated-images/img2.png"]')).toBeVisible({ timeout: 10_000 })
|
||||
|
||||
// Click first history entry (second in list since newest first)
|
||||
@@ -125,11 +124,11 @@ test.describe('Media History - Image Generation', () => {
|
||||
test('history persists across navigation', async ({ page }) => {
|
||||
await mockImageGeneration(page, [{ url: '/generated-images/persist.png' }])
|
||||
|
||||
await page.goto('/app/image')
|
||||
await page.goto('/app/studio/images')
|
||||
await expect(page.getByRole('button', { name: 'test-image-model' })).toBeVisible({ timeout: 10_000 })
|
||||
|
||||
await page.locator('.textarea').first().fill('persist test')
|
||||
await page.locator('button[type="submit"]').click()
|
||||
await page.locator('[data-testid="ws-compose"] textarea').first().fill('persist test')
|
||||
await page.locator('[data-testid="ws-compose"] button[type="submit"]').click()
|
||||
await expect(page.getByTestId('media-history-item')).toHaveCount(1, { timeout: 10_000 })
|
||||
|
||||
// Wait for debounced save
|
||||
@@ -137,7 +136,7 @@ test.describe('Media History - Image Generation', () => {
|
||||
|
||||
// Navigate away and back
|
||||
await page.goto('/app')
|
||||
await page.goto('/app/image')
|
||||
await page.goto('/app/studio/images')
|
||||
|
||||
// Re-register the mock for capabilities after navigation
|
||||
await expect(page.getByTestId('media-history')).toBeVisible({ timeout: 10_000 })
|
||||
@@ -148,11 +147,11 @@ test.describe('Media History - Image Generation', () => {
|
||||
test('delete removes a history entry', async ({ page }) => {
|
||||
await mockImageGeneration(page, [{ url: '/generated-images/del.png' }])
|
||||
|
||||
await page.goto('/app/image')
|
||||
await page.goto('/app/studio/images')
|
||||
await expect(page.getByRole('button', { name: 'test-image-model' })).toBeVisible({ timeout: 10_000 })
|
||||
|
||||
await page.locator('.textarea').first().fill('delete me')
|
||||
await page.locator('button[type="submit"]').click()
|
||||
await page.locator('[data-testid="ws-compose"] textarea').first().fill('delete me')
|
||||
await page.locator('[data-testid="ws-compose"] button[type="submit"]').click()
|
||||
await expect(page.getByTestId('media-history-item')).toHaveCount(1, { timeout: 10_000 })
|
||||
|
||||
// Hover and click delete
|
||||
@@ -175,16 +174,16 @@ test.describe('Media History - Image Generation', () => {
|
||||
})
|
||||
})
|
||||
|
||||
await page.goto('/app/image')
|
||||
await page.goto('/app/studio/images')
|
||||
await expect(page.getByRole('button', { name: 'test-image-model' })).toBeVisible({ timeout: 10_000 })
|
||||
|
||||
// Generate two images
|
||||
await page.locator('.textarea').first().fill('first')
|
||||
await page.locator('button[type="submit"]').click()
|
||||
await page.locator('[data-testid="ws-compose"] textarea').first().fill('first')
|
||||
await page.locator('[data-testid="ws-compose"] button[type="submit"]').click()
|
||||
await expect(page.getByTestId('media-history-item')).toHaveCount(1, { timeout: 10_000 })
|
||||
|
||||
await page.locator('.textarea').first().fill('second')
|
||||
await page.locator('button[type="submit"]').click()
|
||||
await page.locator('[data-testid="ws-compose"] textarea').first().fill('second')
|
||||
await page.locator('[data-testid="ws-compose"] button[type="submit"]').click()
|
||||
await expect(page.getByTestId('media-history-item')).toHaveCount(2, { timeout: 10_000 })
|
||||
|
||||
// Click clear all
|
||||
@@ -201,11 +200,11 @@ test.describe('Media History - TTS', () => {
|
||||
test('TTS history entry appears with server URL from Content-Disposition', async ({ page }) => {
|
||||
await mockTTSGeneration(page, 'tts.wav')
|
||||
|
||||
await page.goto('/app/tts')
|
||||
await page.goto('/app/studio/tts')
|
||||
await expect(page.getByRole('button', { name: 'test-tts-model' })).toBeVisible({ timeout: 10_000 })
|
||||
|
||||
await page.getByPlaceholder('Enter text to synthesize...').fill('hello world')
|
||||
await page.locator('button[type="submit"]').click()
|
||||
await page.locator('[data-testid="ws-compose"] button[type="submit"]').click()
|
||||
|
||||
// History entry should appear
|
||||
await expect(page.getByTestId('media-history-item')).toHaveCount(1, { timeout: 10_000 })
|
||||
@@ -227,13 +226,13 @@ test.describe('Media History - Sound Generation', () => {
|
||||
test('Sound generation history entry appears', async ({ page }) => {
|
||||
await mockSoundGeneration(page, 'sound.wav')
|
||||
|
||||
await page.goto('/app/sound')
|
||||
await page.goto('/app/studio/sound')
|
||||
await expect(page.getByRole('button', { name: 'test-sound-model' })).toBeVisible({ timeout: 10_000 })
|
||||
|
||||
await page.getByRole('button', { name: 'Prompt expansion model', exact: true }).click()
|
||||
await page.getByRole('option', { name: 'Backend default', exact: true }).click()
|
||||
await page.locator('.textarea').first().fill('upbeat jazz')
|
||||
await page.locator('button[type="submit"]').click()
|
||||
await page.locator('[data-testid="ws-compose"] textarea').first().fill('upbeat jazz')
|
||||
await page.locator('[data-testid="ws-compose"] button[type="submit"]').click()
|
||||
|
||||
await expect(page.getByTestId('media-history-item')).toHaveCount(1, { timeout: 10_000 })
|
||||
await expect(page.getByTestId('media-history-item')).toContainText('upbeat jazz')
|
||||
@@ -248,11 +247,11 @@ test.describe('Media History - Video Generation', () => {
|
||||
test('Video history entry appears after generation', async ({ page }) => {
|
||||
await mockVideoGeneration(page, [{ url: '/generated-videos/test.mp4' }])
|
||||
|
||||
await page.goto('/app/video')
|
||||
await page.goto('/app/studio/video')
|
||||
await expect(page.getByRole('button', { name: 'test-video-model' })).toBeVisible({ timeout: 10_000 })
|
||||
|
||||
await page.locator('.textarea').first().fill('a running cat')
|
||||
await page.locator('button[type="submit"]').click()
|
||||
await page.locator('[data-testid="ws-compose"] textarea').first().fill('a running cat')
|
||||
await page.locator('[data-testid="ws-compose"] button[type="submit"]').click()
|
||||
|
||||
await expect(page.getByTestId('media-history-item')).toHaveCount(1, { timeout: 10_000 })
|
||||
await expect(page.getByTestId('media-history-item')).toContainText('a running cat')
|
||||
@@ -262,7 +261,7 @@ test.describe('Media History - Video Generation', () => {
|
||||
let requestBody
|
||||
await mockVideoGeneration(page, [{ url: '/generated-videos/avatar.mp4' }], (body) => { requestBody = body })
|
||||
|
||||
await page.goto('/app/video')
|
||||
await page.goto('/app/studio/video')
|
||||
await expect(page.getByRole('button', { name: 'test-video-model' })).toBeVisible({ timeout: 10_000 })
|
||||
await page.getByRole('button', { name: 'Avatar audio' }).click()
|
||||
await page.getByLabel('Avatar audio', { exact: true }).setInputFiles({
|
||||
@@ -270,8 +269,8 @@ test.describe('Media History - Video Generation', () => {
|
||||
mimeType: 'audio/wav',
|
||||
buffer: Buffer.from('avatar speech'),
|
||||
})
|
||||
await page.locator('.textarea').first().fill('a presenter speaking to camera')
|
||||
await page.locator('button[type="submit"]').click()
|
||||
await page.locator('[data-testid="ws-compose"] textarea').first().fill('a presenter speaking to camera')
|
||||
await page.locator('[data-testid="ws-compose"] button[type="submit"]').click()
|
||||
|
||||
await expect.poll(() => requestBody?.audio).toBe(Buffer.from('avatar speech').toString('base64'))
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { test, expect } from './coverage-fixtures.js'
|
||||
import { mockStudio, stubMedia } from './studio-fixtures.js'
|
||||
|
||||
const VOICE_ID = '00000000-0000-0000-0000-000000000001'
|
||||
|
||||
@@ -21,6 +22,19 @@ function pcmWav(seconds = 2) {
|
||||
return buffer
|
||||
}
|
||||
|
||||
const TTS_MODELS = [
|
||||
{ id: 'qwen-base', capabilities: ['FLAG_TTS'], backend: 'qwen3-tts-cpp', voice_cloning: { reference_transcript_required: true, accepted_audio_formats: ['audio/wav'] } },
|
||||
{ id: 'piper-default', capabilities: ['FLAG_TTS'], backend: 'piper' },
|
||||
]
|
||||
|
||||
// The speech page is a page of the Studio add-on: install it, and serve the
|
||||
// same models through the capabilities call it reads.
|
||||
async function openStudioSpeech(page, query = '') {
|
||||
await mockStudio(page, { capabilityModels: TTS_MODELS })
|
||||
await stubMedia(page)
|
||||
await page.goto(`/app/studio/tts${query}`)
|
||||
}
|
||||
|
||||
const profile = {
|
||||
id: VOICE_ID,
|
||||
name: 'Documentary narrator',
|
||||
@@ -39,10 +53,7 @@ async function mockVoiceAPIs(page) {
|
||||
await page.route('**/api/models/capabilities', route => route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ data: [
|
||||
{ id: 'qwen-base', capabilities: ['FLAG_TTS'], backend: 'qwen3-tts-cpp', voice_cloning: { reference_transcript_required: true, accepted_audio_formats: ['audio/wav'] } },
|
||||
{ id: 'piper-default', capabilities: ['FLAG_TTS'], backend: 'piper' },
|
||||
] }),
|
||||
body: JSON.stringify({ data: TTS_MODELS }),
|
||||
}))
|
||||
await page.route('**/api/voice-profiles', async route => {
|
||||
if (route.request().method() === 'POST') {
|
||||
@@ -152,7 +163,7 @@ test.describe('Speech voices', () => {
|
||||
})
|
||||
})
|
||||
|
||||
await page.goto(`/app/tts?voice=${VOICE_ID}`)
|
||||
await openStudioSpeech(page, `?voice=${VOICE_ID}`)
|
||||
await expect(page.locator('#tts-voice')).toHaveValue(VOICE_ID)
|
||||
await page.getByPlaceholder('Enter text to synthesize...').fill('Hello from the saved voice.')
|
||||
await page.getByRole('button', { name: /Generate$/ }).click()
|
||||
@@ -176,7 +187,7 @@ test.describe('Speech voices', () => {
|
||||
})
|
||||
})
|
||||
|
||||
await page.goto('/app/tts')
|
||||
await openStudioSpeech(page)
|
||||
await page.getByPlaceholder('Enter text to synthesize...').fill('Read this sentence.')
|
||||
// Delivery instructions sit in the Advanced fold of the workspace.
|
||||
await page.getByRole('button', { name: /^Instructions/ }).click()
|
||||
|
||||
Reference in new issue
Block a user