mirror of
https://github.com/mudler/LocalAI.git
synced 2026-10-28 09:08:54 -04:00
test(ui): run the Studio and Talk specs against the add-on bundles
The pages are add-ons now, so a spec that draws one installs the real built bundle first: the apps list names it and its module and style sheet are served from the build output, the way the add-on owner serves them. studio-addon.js holds that helper, and the Studio and Talk fixtures call it. Update the specs for the new addresses (/app/addons/studio/studio, the model as ?model=), the session permission the add-on route needs, the legacy redirects, the Home /studio command that appears only while the add-on is installed, and the sidebar without a Create group. Add specs for the add-ons themselves: every tab draws with no script error, the shell titles the page once, the old addresses say so when the add-on is not installed, a button of the page reaches a core page with the way back, and the drawer lists both only after they are installed. Assisted-by: Claude Code:claude-sonnet-5-5 node Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
24 files changed
+308
-56
No files matched your search
@@ -3,18 +3,21 @@ import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { ADMIN, MEMBER, app, sessionFor, mockAddons } from './addons-fixtures.js'
|
||||
import { installStudioAddons, packagedApp } from './studio-addon.js'
|
||||
import { FACE_MODEL, clipFile, faces, match, mockIdentity, people, photoFile } from './identity-fixtures.js'
|
||||
|
||||
const STUDIO = { id: 'studio', kind: 'page', titleKey: 'items.studio', icon: 'palette', mainPath: '/app/studio' }
|
||||
const TALK = { id: 'talk', kind: 'page', titleKey: 'items.talk', icon: 'phone', mainPath: '/app/talk' }
|
||||
|
||||
async function setup(page, { apps = [], pins = ['studio', 'talk'], addons = true, builtin = [STUDIO, TALK] } = {}) {
|
||||
// Studio and Talk are packaged add-ons, not built-in tiles: the specs install
|
||||
// the real packages. No page is built in today, so `builtin` is empty unless a
|
||||
// spec makes one up to check the built-in rules.
|
||||
async function setup(page, { apps = [], pins = ['studio', 'talk'], addons = true, builtin = [], studio = true } = {}) {
|
||||
let saved = pins
|
||||
await page.route('**/api/auth/status', route => route.fulfill({ json: sessionFor(MEMBER, { addons }) }))
|
||||
await page.route('**/api/features', route => route.fulfill({ json: { addon_navigation: true } }))
|
||||
await page.route('**/api/addons/builtin', route => route.fulfill({ json: { builtin } }))
|
||||
// mockAddons installs the Apps routes with the real response shape ({ apps }).
|
||||
await mockAddons(page, { apps })
|
||||
// The packages are served from the build output, so a page that opens one runs it.
|
||||
if (studio) await installStudioAddons(page, { apps: [packagedApp('studio'), packagedApp('talk'), ...apps] })
|
||||
else await mockAddons(page, { apps })
|
||||
await page.route('**/api/addons/pins', async route => {
|
||||
if (route.request().method() === 'PUT') saved = route.request().postDataJSON()
|
||||
await route.fulfill({ json: saved })
|
||||
@@ -81,22 +84,33 @@ test.describe('Add-on navigation', () => {
|
||||
|
||||
test('member without the add-ons feature sees the built-in tiles and no error', async ({ page }) => {
|
||||
let appsCalled = false
|
||||
await setup(page, { addons: false })
|
||||
// No page is built in today, so the spec makes one up.
|
||||
await setup(page, { addons: false, builtin: [{ id: 'guide', kind: 'page', titleKey: 'items.chat', icon: 'chat', mainPath: '/app/chat' }] })
|
||||
// Registered after setup, so it wins over the mocked Apps route.
|
||||
await page.route(/\/api\/addons\/apps$/, route => { appsCalled = true; return route.fulfill({ status: 403, json: {} }) })
|
||||
await page.goto('/app/addons')
|
||||
await expect(page.getByTestId('addon-tile')).toHaveCount(2)
|
||||
await expect(page.getByTestId('addon-tile')).toHaveCount(1)
|
||||
await expect(page.getByTestId('addons-browse')).toHaveCount(0)
|
||||
await expect(page.getByRole('alert')).toHaveCount(0)
|
||||
expect(appsCalled).toBe(false)
|
||||
})
|
||||
|
||||
test('member without the add-ons feature and with no built-in tile goes home, and Studio and Talk are not built in', async ({ page }) => {
|
||||
await setup(page, { addons: false })
|
||||
await page.goto('/app/addons')
|
||||
await expect(page).toHaveURL(/\/app$/)
|
||||
await expect(page.getByRole('navigation').getByRole('link', { name: 'Add-ons' })).toHaveCount(0)
|
||||
})
|
||||
|
||||
test('the legacy flag brings the old sidebar back', async ({ page }) => {
|
||||
await setup(page)
|
||||
await page.route('**/api/features', route => route.fulfill({ json: { addon_navigation: false } }))
|
||||
await page.goto('/app')
|
||||
await expect(page.getByTestId('pinned-nav')).toHaveCount(0)
|
||||
await expect(page.getByRole('navigation').getByRole('link', { name: 'Studio' })).toBeVisible()
|
||||
// Home, Models and Chat head the old sidebar; Studio and Talk are add-ons and are not in it.
|
||||
await expect(page.getByRole('navigation').getByRole('link', { name: 'Chat' })).toBeVisible()
|
||||
await expect(page.getByRole('navigation').getByRole('link', { name: 'Studio' })).toHaveCount(0)
|
||||
await expect(page.locator('.sidebar-section-title', { hasText: 'Create' })).toHaveCount(0)
|
||||
})
|
||||
|
||||
test('the layout holds at phone width', async ({ page }) => {
|
||||
@@ -132,7 +146,7 @@ test.describe('Add-on navigation', () => {
|
||||
// been drawn from, not before, when a missing pin proves nothing.
|
||||
await page.route(/\/api\/addons\/apps$/, async route => {
|
||||
await new Promise(resolve => setTimeout(resolve, 4000))
|
||||
await route.fulfill({ json: { apps: [] } })
|
||||
await route.fulfill({ json: { apps: [packagedApp('studio')] } })
|
||||
})
|
||||
const listed = page.waitForResponse(res => /\/api\/addons\/apps$/.test(res.url()))
|
||||
await page.goto('/app')
|
||||
@@ -191,13 +205,13 @@ test.describe('Add-on navigation: the Add-ons hub tab and the route in both layo
|
||||
test('an old generator link keeps its model, query and hash', async ({ page }) => {
|
||||
await setup(page)
|
||||
await page.goto('/app/image/stable%2Fdiffusion?x=1#top')
|
||||
await expect(page).toHaveURL(/\/app\/studio\/images\/stable%2Fdiffusion\?x=1#top$/)
|
||||
await expect(page).toHaveURL(/\/app\/addons\/studio\/studio\/images\?x=1&model=stable%2Fdiffusion#top$/)
|
||||
})
|
||||
|
||||
test('the legacy ?tab= link of Studio still works', async ({ page }) => {
|
||||
await setup(page)
|
||||
await page.goto('/app/studio?tab=tts')
|
||||
await expect(page).toHaveURL(/\/app\/studio\/tts$/)
|
||||
await expect(page).toHaveURL(/\/app\/addons\/studio\/studio\/tts$/)
|
||||
})
|
||||
|
||||
test('a viewer without the 3d feature still ends on the app home', async ({ page }) => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { test, expect } from './coverage-fixtures.js'
|
||||
import { installStudioAddons } from './studio-addon.js'
|
||||
|
||||
function mockCapabilities(page, capabilities) {
|
||||
return page.route('**/api/models/capabilities', (route) => {
|
||||
@@ -95,6 +96,8 @@ function makeToneWav(name, freq = 1000, seconds = 0.4, sampleRate = 16000) {
|
||||
|
||||
test.describe('Audio Transform', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Studio is an add-on: the page opens inside it.
|
||||
await installStudioAddons(page, { ids: ['studio'] })
|
||||
await mockCapabilities(page, [
|
||||
{ id: 'localvqe', capabilities: ['FLAG_AUDIO_TRANSFORM'] },
|
||||
])
|
||||
|
||||
+3
@@ -2,6 +2,7 @@
|
||||
// written to the browser storage the Chat page reads, and a completion stream
|
||||
// the spec drives by hand.
|
||||
import { mockHome, seedChats, CHATS_KEY } from './home-fixtures.js'
|
||||
import { installStudioAddons } from './studio-addon.js'
|
||||
|
||||
export { CHATS_KEY, seedChats }
|
||||
|
||||
@@ -106,6 +107,8 @@ export const PNG = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAA
|
||||
// The Talk page's WebRTC, faked: the page connects as it would, and the spec
|
||||
// plays the server's events with window.__talk.emit(event).
|
||||
export async function fakeRealtime(page, { pipelines = true, micError = null } = {}) {
|
||||
// Talk is an add-on: install the built bundle that draws the page.
|
||||
await installStudioAddons(page, { ids: ['talk'] })
|
||||
await page.route('**/api/pipeline-models', route => route.fulfill({
|
||||
json: pipelines
|
||||
? [
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { test, expect } from './coverage-fixtures.js'
|
||||
import { installStudioAddons } from './studio-addon.js'
|
||||
|
||||
// A standing guard against the two defects an earlier automated edit left
|
||||
// scattered through the pages: icons that render nothing at all (here, an svg
|
||||
@@ -14,6 +15,8 @@ const ROUTES = [
|
||||
]
|
||||
|
||||
test('no page renders a dead icon or a default-chrome control', async ({ page }) => {
|
||||
// Studio and Talk are add-ons; /app/studio and /app/talk open them.
|
||||
await installStudioAddons(page)
|
||||
// One test walks every route, so its budget has to scale with the list rather
|
||||
// than sit on Playwright's per-test default of 30s. At 25 routes that default
|
||||
// allows ~1.2s per navigation, which holds on a developer machine and does
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
import { test, expect } from './coverage-fixtures.js'
|
||||
import { installStudioAddons } from './studio-addon.js'
|
||||
|
||||
const profiles = { version: 1, encoder: { identity: `sha256:${'a'.repeat(64)}`, dimension: 2 }, speakers: [
|
||||
{ speaker: 9, clean_duration: 0, intervals: [], unavailable_reason: 'insufficient_clean_speech' },
|
||||
@@ -21,11 +22,13 @@ const file = { name: 'meeting.wav', mimeType: 'audio/wav', buffer: Buffer.alloc(
|
||||
async function setup(page, permission = true, diarizationPermission = true) {
|
||||
await page.route('**/api/**', route => {
|
||||
const url = route.request().url()
|
||||
const data = url.endsWith('/auth/status') ? { authEnabled: true, user: { role: 'user', permissions: { audio_diarization: diarizationPermission, voice_recognition: permission } } }
|
||||
const data = url.endsWith('/auth/status') ? { authEnabled: true, user: { role: 'user', permissions: { addons: true, audio_diarization: diarizationPermission, voice_recognition: permission } } }
|
||||
: url.endsWith('/models/capabilities') ? { data: ['diarizer', 'other'].map(id => ({ id, capabilities: ['FLAG_DIARIZATION'] })) } : {}
|
||||
return route.fulfill({ json: data })
|
||||
})
|
||||
await page.route('**/v1/audio/diarization', route => route.fulfill({ json: result }))
|
||||
// Studio is an add-on. Installed after the catch-all above, so its routes win.
|
||||
await installStudioAddons(page, { ids: ['studio'] })
|
||||
await page.goto('/app/diarization')
|
||||
if (!diarizationPermission) return
|
||||
await expect(page.getByRole('button', { name: 'diarizer', exact: true })).toBeVisible()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { test, expect } from './coverage-fixtures.js'
|
||||
import { mockHome, mockUser, seedChats, sampleChats, CHATS_KEY, LOADED } from './home-fixtures.js'
|
||||
import { installStudioAddons } from './studio-addon.js'
|
||||
|
||||
// The Home console: command bar, slash menu, memory strip, resume list, first
|
||||
// run and the phone layout. Data is stubbed the way the other Home specs do it;
|
||||
@@ -39,10 +40,26 @@ test.describe('Home slash menu', () => {
|
||||
expect(names).toEqual([
|
||||
'/model', '/new', '/chat', '/assistant',
|
||||
'/gallery', '/installed', '/import', '/stop',
|
||||
'/studio', '/settings', '/docs',
|
||||
'/settings', '/docs',
|
||||
])
|
||||
})
|
||||
|
||||
// Studio is an add-on: its command is offered only while it is installed.
|
||||
test('the /studio command appears once the Studio add-on is installed', async ({ page }) => {
|
||||
await installStudioAddons(page, { ids: ['studio'] })
|
||||
await ready(page)
|
||||
await page.locator('.home-textarea').fill('/')
|
||||
await expect(page.getByTestId('home-slash-studio')).toBeVisible()
|
||||
await page.getByTestId('home-slash-studio').click()
|
||||
await expect(page).toHaveURL(/\/app\/addons\/studio\/studio/)
|
||||
})
|
||||
|
||||
test('the /studio command is not offered while the Studio add-on is not installed', async ({ page }) => {
|
||||
await ready(page)
|
||||
await page.locator('.home-textarea').fill('/stu')
|
||||
await expect(page.getByTestId('home-slash-studio')).toHaveCount(0)
|
||||
})
|
||||
|
||||
test('filters as you type and marks the typed part', async ({ page }) => {
|
||||
await ready(page)
|
||||
await page.locator('.home-textarea').fill('/ga')
|
||||
@@ -93,8 +110,8 @@ test.describe('Home slash menu', () => {
|
||||
test('a click on an action runs it and clears the box', async ({ page }) => {
|
||||
await ready(page)
|
||||
await page.locator('.home-textarea').fill('/')
|
||||
await page.getByTestId('home-slash-studio').click()
|
||||
await expect(page).toHaveURL(/\/app\/studio/)
|
||||
await page.getByTestId('home-slash-installed').click()
|
||||
await expect(page).toHaveURL(/\/app\/models\?view=installed/)
|
||||
})
|
||||
|
||||
test('"/" outside a text field starts a command', async ({ page }) => {
|
||||
@@ -138,7 +155,7 @@ test.describe('Home slash menu', () => {
|
||||
await ready(page)
|
||||
await page.locator('.home-textarea').fill('/')
|
||||
const names = await page.locator('.dk-cmd-name').allTextContents()
|
||||
expect(names).toEqual(['/model', '/new', '/chat', '/studio', '/docs'])
|
||||
expect(names).toEqual(['/model', '/new', '/chat', '/docs'])
|
||||
})
|
||||
|
||||
test('and the library row keeps only the documentation link', async ({ page }) => {
|
||||
|
||||
@@ -389,7 +389,7 @@ test.describe('Voices: states of the page', () => {
|
||||
await page.goto('/app/voice?tab=recording')
|
||||
await expect(page.getByTestId('recording-tab')).toContainText('Name the speakers in a recording')
|
||||
await page.getByRole('link', { name: /Open the diarization workspace/ }).click()
|
||||
await expect(page).toHaveURL(/\/app\/studio\/diarization/)
|
||||
await expect(page).toHaveURL(/\/app\/addons\/studio\/studio\/diarization/)
|
||||
})
|
||||
|
||||
test('the route /app/voice/:model keeps working', async ({ page }) => {
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { MEMBER, sessionFor, mockAddons } from './addons-fixtures.js'
|
||||
import { MEMBER, sessionFor } from './addons-fixtures.js'
|
||||
import { installStudioAddons } from './studio-addon.js'
|
||||
import { MIGRATION_ROUTES } from '../src/utils/legacyRoutes.js'
|
||||
|
||||
// Every moved generator route, with and without a model: 14 old paths. For
|
||||
// each, in both flag states, the final location is the Studio tab and that
|
||||
// generator is drawn (its tab is active), not the Studio overview fallback.
|
||||
// each, in both flag states, the final location is the tab of the Studio add-on
|
||||
// and that generator is drawn (its tab is active), not the overview fallback.
|
||||
// A model travels as ?model=, because an add-on path segment refuses "/" and ":".
|
||||
const MOVED = MIGRATION_ROUTES.filter(r => r.kind === 'studio-tab')
|
||||
const CASES = MOVED.flatMap(r => [
|
||||
{ from: r.from, to: r.to, tab: r.to.split('/').pop() },
|
||||
{ from: `${r.from}/my-model`, to: `${r.to}/my-model`, tab: r.to.split('/').pop() },
|
||||
{ from: `${r.from}/my-model`, to: `${r.to}?model=my-model`, tab: r.to.split('/').pop() },
|
||||
])
|
||||
|
||||
const STUDIO = { id: 'studio', kind: 'page', titleKey: 'items.studio', icon: 'palette', mainPath: '/app/studio' }
|
||||
|
||||
async function setup(page, flag) {
|
||||
await page.route('**/api/auth/status', route => route.fulfill({ json: sessionFor(MEMBER, { addons: true, '3d': true, audio_diarization: true, audio_transform: true }) }))
|
||||
await page.route('**/api/features', route => route.fulfill({ json: { addon_navigation: flag } }))
|
||||
await page.route('**/api/addons/builtin', route => route.fulfill({ json: { builtin: [STUDIO] } }))
|
||||
await mockAddons(page, { apps: [] })
|
||||
await page.route('**/api/addons/builtin', route => route.fulfill({ json: { builtin: [] } }))
|
||||
await installStudioAddons(page, { ids: ['studio'] })
|
||||
await page.route('**/api/addons/pins', route => route.fulfill({ json: [] }))
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ for (const flag of [true, false]) {
|
||||
test(`${c.from} opens ${c.to}`, async ({ page }) => {
|
||||
await setup(page, flag)
|
||||
await page.goto(c.from)
|
||||
await expect(page).toHaveURL(new RegExp(`${c.to.replace(/\//g, '\\/')}$`))
|
||||
await expect(page).toHaveURL(new RegExp(`${c.to.replace(/[/?]/g, '\\$&')}$`))
|
||||
await expect(page.locator(`.studio-tab-active[data-tab="${c.tab}"]`)).toBeVisible()
|
||||
await expect(page.getByTestId('studio-overview')).toHaveCount(0)
|
||||
})
|
||||
|
||||
@@ -26,8 +26,10 @@ test.describe('Navigation', () => {
|
||||
for (const href of ['/app/chat', '/app/models', '/app/addons', '/app/settings']) {
|
||||
await expect(page.locator(`.sidebar-nav a.nav-item[href="${href}"]`)).toBeVisible()
|
||||
}
|
||||
// Studio and Talk are built-in tiles of the Add-ons drawer; the sidebar lists them only as pins.
|
||||
await expect(page.locator('.sidebar-nav a.nav-item[href="/app/studio"]')).toHaveCount(await page.getByTestId('pinned-nav').locator('a[href="/app/studio"]').count())
|
||||
// Studio and Talk are add-ons: the sidebar lists them only as pins, and only while they are installed.
|
||||
for (const href of ['/app/studio', '/app/talk']) {
|
||||
await expect(page.locator(`.sidebar-nav a.nav-item[href="${href}"]`)).toHaveCount(0)
|
||||
}
|
||||
})
|
||||
|
||||
test('Build and Operate sit under a Workspace label', async ({ page }) => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { test, expect } from './coverage-fixtures.js'
|
||||
import { installStudioAddons } from './studio-addon.js'
|
||||
|
||||
// No coloured strip on the left edge of a tile, card, row or message. A rail
|
||||
// can be drawn four ways, and each is checked from computed style:
|
||||
@@ -16,6 +17,8 @@ const ROUTES = [
|
||||
]
|
||||
|
||||
test('no element on the main routes carries a left accent rail', async ({ page }) => {
|
||||
// Studio and Talk are add-ons; /app/studio and /app/talk open them.
|
||||
await installStudioAddons(page)
|
||||
test.setTimeout(ROUTES.length * 8_000)
|
||||
const findings = []
|
||||
for (const theme of ['light', 'dark']) {
|
||||
|
||||
@@ -4,7 +4,6 @@ import { test, expect } from './coverage-fixtures.js'
|
||||
// not repeated above the title as a small crumb: the sidebar and the hub tab
|
||||
// bar already say where the page lives.
|
||||
const PAGES = [
|
||||
['/app/studio', 'Studio'],
|
||||
['/app/agents', 'Agents'],
|
||||
['/app/activity', 'Activity'],
|
||||
]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { test, expect } from './coverage-fixtures.js'
|
||||
import { installStudioAddons } from './studio-addon.js'
|
||||
|
||||
// Render-smoke coverage. Each page is lazy-loaded and runs its full render +
|
||||
// initial effects on mount, so a bare visit captures the bulk of a page's
|
||||
@@ -13,8 +14,6 @@ import { test, expect } from './coverage-fixtures.js'
|
||||
// Asserts the page mounted (its .page-title header is visible) and that it did
|
||||
// not bounce to a gate redirect (/login or back to /app home).
|
||||
const PAGES = [
|
||||
['/app/talk', 'Talk'],
|
||||
['/app/studio/threed', '3D Generation'],
|
||||
['/app/usage', 'Usage'],
|
||||
['/app/traffic', 'Traffic overview'],
|
||||
['/app/traffic/models', 'Traffic models'],
|
||||
@@ -23,7 +22,6 @@ const PAGES = [
|
||||
['/app/traces', 'Traces'],
|
||||
['/app/middleware', 'Middleware'],
|
||||
['/app/account', 'Account'],
|
||||
['/app/studio', 'Studio'],
|
||||
['/app/models', 'Models'],
|
||||
['/app/operate', 'Operate overview'],
|
||||
['/app/backends', 'Backends'],
|
||||
@@ -47,10 +45,30 @@ test.describe('Page render smoke', () => {
|
||||
test(`renders ${label} (${path})`, async ({ page }) => {
|
||||
await page.goto(path)
|
||||
// .page-title for the normal header; .empty-state-title for pages that
|
||||
// render a gated/empty state (e.g. Account when auth is disabled); Talk
|
||||
// carries its own header.
|
||||
await expect(page.locator('.page-title, .view-bar__title, .empty-state-title, .dk-empty-title, .talk-hd h1, .op-status__title, .op-runtime h1, .sw-page h1, .tf-title, .st-title').first()).toBeVisible({ timeout: 15_000 })
|
||||
// render a gated/empty state (e.g. Account when auth is disabled).
|
||||
await expect(page.locator('.page-title, .view-bar__title, .empty-state-title, .dk-empty-title, .op-status__title, .op-runtime h1, .sw-page h1, .tf-title, .st-title').first()).toBeVisible({ timeout: 15_000 })
|
||||
await expect(page).toHaveURL(new RegExp(path.replace(/\//g, '\\/') + '$'))
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Studio and Talk are add-ons: the pages draw inside the add-on shell once the
|
||||
// built bundles are installed, and the old addresses open them there.
|
||||
const ADDON_PAGES = [
|
||||
['/app/addons/talk/talk', 'Talk', '.talk-page'],
|
||||
['/app/addons/studio/studio', 'Studio', '[data-testid="studio-overview"]'],
|
||||
['/app/addons/studio/studio/threed', 'Studio 3D', '.ws-title'],
|
||||
['/app/talk', 'Talk (old address)', '.talk-page'],
|
||||
['/app/studio/threed', 'Studio 3D (old address)', '.ws-title'],
|
||||
]
|
||||
|
||||
test.describe('Add-on page render smoke', () => {
|
||||
for (const [path, label, marker] of ADDON_PAGES) {
|
||||
test(`renders ${label} (${path})`, async ({ page }) => {
|
||||
await installStudioAddons(page)
|
||||
await page.goto(path)
|
||||
await expect(page.locator(marker).first()).toBeVisible({ timeout: 15_000 })
|
||||
await expect(page).toHaveURL(/\/app\/addons\/(studio|talk)\//)
|
||||
})
|
||||
}
|
||||
})
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
// Studio and Talk are packaged add-ons. A spec that draws one of them installs
|
||||
// the real built bundle here: the app list names it, and its module and style
|
||||
// sheet are served from the build output the way the add-on owner serves them.
|
||||
// The old /app/studio and /app/talk addresses redirect to the add-on page, so a
|
||||
// spec can still open those.
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { app, mockAddons } from './addons-fixtures.js'
|
||||
|
||||
const addons = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../../addons')
|
||||
const DIGEST = 'e'.repeat(64)
|
||||
const TYPES = { js: 'text/javascript', css: 'text/css', png: 'image/png' }
|
||||
const built = new Map()
|
||||
|
||||
// Builds the bundle once per worker, so a spec never runs against a stale one.
|
||||
function build(id) {
|
||||
if (!built.has(id)) built.set(id, import(path.join(addons, id, 'ui/build.mjs')).then(m => m.build()))
|
||||
return built.get(id)
|
||||
}
|
||||
|
||||
export function packagedApp(id, over = {}) {
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(addons, id, 'manifest.json'), 'utf8'))
|
||||
const ui = manifest.capabilities.ui
|
||||
const base = `/api/addons/${id}/assets/${DIGEST}`
|
||||
return app({
|
||||
id, name: id, title: ui.title, description: ui.description, icon: undefined, generation: 1, version: manifest.version,
|
||||
mainPage: `/app/addons/${id}/${ui.mainPage}`, tools: [], toolCount: 0,
|
||||
ui: { entry: `${base}/${ui.entry}`, css: ui.css.map(c => `${base}/${c}`), sdk: ui.sdk, pages: ui.pages, navigation: ui.navigation },
|
||||
...over,
|
||||
})
|
||||
}
|
||||
|
||||
// Installs the add-ons and the routes that serve them. `ids` picks which.
|
||||
// Returns the state of mockAddons.
|
||||
export async function installStudioAddons(page, { ids = ['studio', 'talk'], ...options } = {}) {
|
||||
const dirs = await Promise.all(ids.map(build))
|
||||
await page.route(/\/api\/addons\/(studio|talk)\/assets\//, route => {
|
||||
const parts = new URL(route.request().url()).pathname.split('/')
|
||||
const id = parts[3]
|
||||
const file = parts.slice(6).join('/')
|
||||
const dir = dirs[ids.indexOf(id)]
|
||||
const ext = file.split('.').pop()
|
||||
if (!dir || !TYPES[ext] || !fs.existsSync(path.join(dir, file))) return route.fulfill({ status: 403, json: { error: { type: 'addon_error', message: 'Addon request denied' } } })
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: TYPES[ext],
|
||||
headers: { 'cache-control': 'private, max-age=31536000, immutable', 'x-content-type-options': 'nosniff' },
|
||||
body: fs.readFileSync(path.join(dir, file)),
|
||||
})
|
||||
})
|
||||
return mockAddons(page, { apps: ids.map(id => packagedApp(id)), ...options })
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import { test, expect } from './coverage-fixtures.js'
|
||||
import { installStudioAddons } from './studio-addon.js'
|
||||
import { mockStudio } from './studio-fixtures.js'
|
||||
import { fakeRealtime } from './chat-fixtures.js'
|
||||
|
||||
// Studio and Talk are packaged add-ons. These specs run the real built bundles
|
||||
// inside the add-on shell: the pages draw, the old addresses open them, the
|
||||
// links they hold reach the core pages, and nothing is left when they are not
|
||||
// installed.
|
||||
|
||||
function watchErrors(page) {
|
||||
const errors = []
|
||||
page.on('pageerror', e => errors.push(String(e)))
|
||||
page.on('console', m => { if (m.type() === 'error') errors.push(m.text()) })
|
||||
return errors
|
||||
}
|
||||
|
||||
test.describe('Studio add-on', () => {
|
||||
for (const tab of ['', '/images', '/video', '/threed', '/tts', '/sound', '/transform', '/diarization']) {
|
||||
test(`the ${tab ? tab.slice(1) : 'overview'} tab draws inside the add-on shell with no script error`, async ({ page }) => {
|
||||
const errors = watchErrors(page)
|
||||
await mockStudio(page)
|
||||
await page.goto(`/app/addons/studio/studio${tab}`)
|
||||
await expect(page.locator('[data-testid="addon-surface"][data-addon="studio"] .studio-tabs')).toBeVisible({ timeout: 15_000 })
|
||||
if (tab) await expect(page.locator(`.studio-tab-active[data-tab="${tab.slice(1)}"]`)).toBeVisible()
|
||||
else await expect(page.getByTestId('studio-overview')).toBeVisible()
|
||||
expect(errors).toEqual([])
|
||||
})
|
||||
}
|
||||
|
||||
test('the shell titles the page once, and the page keeps its own words from the media locale', async ({ page }) => {
|
||||
await mockStudio(page)
|
||||
await page.goto('/app/addons/studio/studio')
|
||||
await expect(page.getByTestId('studio-overview')).toBeVisible({ timeout: 15_000 })
|
||||
await expect(page.locator('h1.page-title', { hasText: /^Studio$/ })).toHaveCount(1)
|
||||
await expect(page.getByText('What do you want to make?')).toBeVisible()
|
||||
// No raw key is drawn where words belong.
|
||||
await expect(page.locator('.studio-front').getByText(/studio\.[a-z]+\.[A-Za-z.]+/)).toHaveCount(0)
|
||||
})
|
||||
|
||||
test('a model id with a slash and a colon travels as ?model= and reaches the page', async ({ page }) => {
|
||||
await mockStudio(page)
|
||||
await page.goto('/app/studio/images/sd-1.5-lcm?size=768x768')
|
||||
await expect(page).toHaveURL(/\/app\/addons\/studio\/studio\/images\?size=768x768&model=sd-1\.5-lcm$/)
|
||||
await expect(page.locator('.studio-tab-active[data-tab="images"]')).toBeVisible({ timeout: 15_000 })
|
||||
})
|
||||
|
||||
test('the old Studio addresses say so plainly when the add-on is not installed', async ({ page }) => {
|
||||
for (const address of ['/app/studio', '/app/studio/tts', '/app/image', '/app/talk']) {
|
||||
await page.goto(address)
|
||||
await expect(page.getByTestId('addon-unavailable')).toBeVisible({ timeout: 15_000 })
|
||||
await expect(page).toHaveURL(/\/app\/addons\/(studio|talk)\//)
|
||||
}
|
||||
await expect(page.locator('.studio-tabs')).toHaveCount(0)
|
||||
})
|
||||
|
||||
test('the Studio style sheet reaches the page and nothing outside it', async ({ page }) => {
|
||||
await mockStudio(page)
|
||||
await page.goto('/app/addons/studio/studio/images')
|
||||
await expect(page.locator('.studio-tab-active')).toBeVisible({ timeout: 15_000 })
|
||||
const css = await page.evaluate(() => [...document.querySelectorAll('style[data-addon-style="studio"]')].map(el => el.textContent).join('\n'))
|
||||
expect(css).toContain('[data-addon="studio"]')
|
||||
expect(css).not.toMatch(/(^|\})\s*\.studio-tab\s*\{/)
|
||||
// The sidebar is untouched by the sheet.
|
||||
const sidebarFont = await page.locator('.sidebar-nav .nav-item').first().evaluate(el => getComputedStyle(el).fontFamily)
|
||||
expect(sidebarFont.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Talk add-on', () => {
|
||||
test('the page draws inside the add-on shell, titled once, with no script error', async ({ page }) => {
|
||||
const errors = watchErrors(page)
|
||||
await fakeRealtime(page)
|
||||
await page.goto('/app/addons/talk/talk')
|
||||
await expect(page.getByTestId('talk-start')).toBeEnabled({ timeout: 15_000 })
|
||||
await expect(page.locator('h1')).toHaveCount(1)
|
||||
await expect(page.locator('h1')).toHaveText('Talk')
|
||||
expect(errors).toEqual([])
|
||||
})
|
||||
|
||||
test('a button of the page opens a core page, and the way back to Talk travels with the editor link', async ({ page }) => {
|
||||
await fakeRealtime(page, { pipelines: false })
|
||||
await page.goto('/app/addons/talk/talk')
|
||||
const card = page.getByTestId('talk-no-pipeline')
|
||||
await expect(card).toBeVisible({ timeout: 15_000 })
|
||||
await card.getByRole('button', { name: 'Create a pipeline model' }).click()
|
||||
await expect(page).toHaveURL(/\/app\/model-editor\?template=pipeline/)
|
||||
expect(await page.evaluate(() => window.history.state?.usr)).toEqual({ from: '/app/addons/talk/talk', fromLabel: 'Talk' })
|
||||
await page.goBack()
|
||||
await expect(page.getByTestId('talk-no-pipeline')).toBeVisible()
|
||||
await page.getByTestId('talk-no-pipeline').getByRole('button', { name: /gallery/i }).click()
|
||||
await expect(page).toHaveURL(/\/app\/models/)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Without the add-ons', () => {
|
||||
test('the Add-ons drawer does not list Studio or Talk, and the sidebar has no Create group', async ({ page }) => {
|
||||
await page.goto('/app/addons')
|
||||
await expect(page.getByTestId('addon-tile').filter({ hasText: /Studio|Talk/ })).toHaveCount(0)
|
||||
await expect(page.locator('.sidebar-nav a[href="/app/studio"], .sidebar-nav a[href="/app/talk"]')).toHaveCount(0)
|
||||
await expect(page.locator('.sidebar-section-title', { hasText: 'Create' })).toHaveCount(0)
|
||||
})
|
||||
|
||||
test('installing them lists both in the drawer', async ({ page }) => {
|
||||
await installStudioAddons(page)
|
||||
await page.goto('/app/addons')
|
||||
await expect(page.getByTestId('addon-tile').filter({ hasText: 'Studio' })).toBeVisible({ timeout: 15_000 })
|
||||
await expect(page.getByTestId('addon-tile').filter({ hasText: 'Talk' })).toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -162,10 +162,10 @@ test.describe('Studio composer: keys', () => {
|
||||
await page.goto('/app/studio')
|
||||
await prompt(page).focus()
|
||||
await page.keyboard.press('Control+Enter')
|
||||
await expect(page).toHaveURL(/\/app\/studio$/)
|
||||
await expect(page).toHaveURL(/\/app\/addons\/studio\/studio$/)
|
||||
await prompt(page).fill('a brass orrery')
|
||||
await page.keyboard.press('Control+Enter')
|
||||
await expect(page).toHaveURL(/\/app\/studio\/images\?/)
|
||||
await expect(page).toHaveURL(/\/app\/addons\/studio\/studio\/images\?/)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -222,7 +222,7 @@ test.describe('Studio composer: hand-off to the workspaces', () => {
|
||||
await page.locator('[data-testid="studio-size"]').selectOption('768x768')
|
||||
await page.locator('[data-testid="studio-count"]').selectOption('3')
|
||||
await generate(page).click()
|
||||
await expect(page).toHaveURL(/\/app\/studio\/images\?/)
|
||||
await expect(page).toHaveURL(/\/app\/addons\/studio\/studio\/images\?/)
|
||||
await expect(page.locator('[data-testid="ws-compose"] textarea').first()).toHaveValue('A brass orrery on a walnut desk')
|
||||
await expect(page.locator('[data-testid="ws-size"]').first()).toHaveValue('768x768')
|
||||
await expect(page.locator('[data-testid="ws-count"]').first()).toHaveValue('3')
|
||||
@@ -238,7 +238,7 @@ test.describe('Studio composer: hand-off to the workspaces', () => {
|
||||
await prompt(page).fill('Waves rolling onto black sand, drone shot')
|
||||
await page.locator('[data-testid="studio-size"]').selectOption('1280x720')
|
||||
await generate(page).click()
|
||||
await expect(page).toHaveURL(/\/app\/studio\/video\?/)
|
||||
await expect(page).toHaveURL(/\/app\/addons\/studio\/studio\/video\?/)
|
||||
await expect(page.locator('[data-testid="ws-compose"] textarea').first()).toHaveValue('Waves rolling onto black sand, drone shot')
|
||||
await expect(page.locator('[data-testid="ws-size"]').first()).toHaveValue('1280x720')
|
||||
await expect(page.locator('[data-testid="ws-compose"]')).toContainText('wan2.1-t2v-1.3b')
|
||||
@@ -250,7 +250,7 @@ test.describe('Studio composer: hand-off to the workspaces', () => {
|
||||
await pickType(page, 'tts')
|
||||
await prompt(page).fill('Welcome to the harbour tour.')
|
||||
await generate(page).click()
|
||||
await expect(page).toHaveURL(/\/app\/studio\/tts\?/)
|
||||
await expect(page).toHaveURL(/\/app\/addons\/studio\/studio\/tts\?/)
|
||||
await expect(page.locator('[data-testid="ws-compose"] textarea').first()).toHaveValue('Welcome to the harbour tour.')
|
||||
await expect(page.locator('[data-testid="ws-compose"]')).toContainText('kokoro-82m')
|
||||
})
|
||||
@@ -261,7 +261,7 @@ test.describe('Studio composer: hand-off to the workspaces', () => {
|
||||
await pickType(page, 'sound')
|
||||
await prompt(page).fill('Wind through pines, no music')
|
||||
await generate(page).click()
|
||||
await expect(page).toHaveURL(/\/app\/studio\/sound\?/)
|
||||
await expect(page).toHaveURL(/\/app\/addons\/studio\/studio\/sound\?/)
|
||||
await expect(page.locator('[data-testid="ws-compose"] textarea').first()).toHaveValue('Wind through pines, no music')
|
||||
})
|
||||
|
||||
@@ -273,7 +273,7 @@ test.describe('Studio composer: hand-off to the workspaces', () => {
|
||||
await pickType(page, 'threed')
|
||||
await pickSource(page, /ceramic bowls/)
|
||||
await generate(page).click()
|
||||
await expect(page).toHaveURL(/\/app\/studio\/threed\?.*from=img-bowls.*edge=to-3d/)
|
||||
await expect(page).toHaveURL(/\/app\/addons\/studio\/studio\/threed\?.*from=img-bowls.*edge=to-3d/)
|
||||
const note = page.locator('[data-testid="studio-handoff"]')
|
||||
await expect(note).toHaveAttribute('data-status', 'ready')
|
||||
await expect(note).toContainText('ceramic bowls')
|
||||
@@ -291,7 +291,7 @@ test.describe('Studio composer: hand-off to the workspaces', () => {
|
||||
await pickType(page, key)
|
||||
await pickSource(page, /Welcome to the harbour tour/)
|
||||
await generate(page).click()
|
||||
await expect(page).toHaveURL(new RegExp(`/app/studio/${key}\\?.*edge=${edge}`))
|
||||
await expect(page).toHaveURL(new RegExp(`/app/addons/studio/studio/${key}\\?.*edge=${edge}`))
|
||||
await expect(page.locator('[data-testid="studio-handoff"]')).toHaveAttribute('data-status', 'ready')
|
||||
}
|
||||
// The file reached the Diarization form: its run button is enabled.
|
||||
|
||||
+5
-1
@@ -1,5 +1,7 @@
|
||||
import { expect } from '@playwright/test'
|
||||
// Shared fixtures for the Studio front page specs: the installed models, a
|
||||
import { installStudioAddons } from './studio-addon.js'
|
||||
// Shared fixtures for the Studio front page specs (Studio is an add-on, see
|
||||
// studio-addon.js): the installed models, a
|
||||
// gallery that offers a model per type, a machine, believable stored history,
|
||||
// and stand-ins for the files the history points at.
|
||||
//
|
||||
@@ -68,6 +70,8 @@ export async function mockStudio(page, {
|
||||
installStatus = 200,
|
||||
} = {}) {
|
||||
const state = { types: [...types], installs: [], capabilityCalls: 0 }
|
||||
// Studio is an add-on: install the built bundle, then stub what it reads.
|
||||
await installStudioAddons(page, { ids: ['studio'] })
|
||||
await page.route('**/api/models/capabilities', route => {
|
||||
state.capabilityCalls += 1
|
||||
if (capabilitiesStatus !== 200) return route.fulfill({ status: capabilitiesStatus, json: { error: 'down' } })
|
||||
|
||||
@@ -31,7 +31,7 @@ test.describe('Lineage view', () => {
|
||||
await seedHistory(page, sampleHistory())
|
||||
await page.goto('/app/studio')
|
||||
await page.locator('[data-testid="work-project"] button').first().click()
|
||||
await expect(page).toHaveURL(/\/app\/studio\?work=vid-push/)
|
||||
await expect(page).toHaveURL(/\/app\/addons\/studio\/studio\?work=vid-push/)
|
||||
await expect(node(page, 'vid-push')).toHaveAttribute('data-selected', 'true')
|
||||
await page.keyboard.press('Escape')
|
||||
await expect(page.locator('[data-testid="studio-lineage"]')).toHaveCount(0)
|
||||
@@ -115,7 +115,7 @@ test.describe('Lineage view: new take and branch', () => {
|
||||
test('Run as a new take opens the same workspace with the words, model and size and the result as parent', async ({ page }) => {
|
||||
await open(page, 'img-harbour')
|
||||
await page.locator('[data-testid="studio-take"]').click()
|
||||
await expect(page).toHaveURL(/\/app\/studio\/images\?/)
|
||||
await expect(page).toHaveURL(/\/app\/addons\/studio\/studio\/images\?/)
|
||||
await expect(page).toHaveURL(/from=img-harbour/)
|
||||
await expect(page).toHaveURL(/edge=take/)
|
||||
await expect(page.locator('[data-testid="ws-compose"] textarea').first()).toHaveValue(/fishing harbour at first light/)
|
||||
@@ -132,7 +132,7 @@ test.describe('Lineage view: new take and branch', () => {
|
||||
await expect(words).toHaveValue('Slow push-in, small natural motion')
|
||||
await words.fill('Gulls crossing the frame')
|
||||
await page.locator('[data-testid="studio-draft-open"]').click()
|
||||
await expect(page).toHaveURL(/\/app\/studio\/video\?/)
|
||||
await expect(page).toHaveURL(/\/app\/addons\/studio\/studio\/video\?/)
|
||||
await expect(page).toHaveURL(/from=img-harbour/)
|
||||
await expect(page).toHaveURL(/edge=animate/)
|
||||
await expect(page.locator('[data-testid="ws-compose"] textarea').first()).toHaveValue('Gulls crossing the frame')
|
||||
@@ -184,7 +184,7 @@ test.describe('Lineage view: new take and branch', () => {
|
||||
await expect(page.locator('[data-testid="draft-target-diarize"]')).toBeEnabled()
|
||||
await page.locator('[data-testid="draft-target-diarize"]').click()
|
||||
await page.locator('[data-testid="studio-draft-open"]').click()
|
||||
await expect(page).toHaveURL(/\/app\/studio\/diarization\?.*from=tts-welcome.*edge=diarize/)
|
||||
await expect(page).toHaveURL(/\/app\/addons\/studio\/studio\/diarization\?.*from=tts-welcome.*edge=diarize/)
|
||||
await expect(page.locator('[data-testid="studio-handoff"]')).toHaveAttribute('data-status', 'ready')
|
||||
})
|
||||
})
|
||||
@@ -222,7 +222,7 @@ test.describe('Lineage view: what a page cannot take yet', () => {
|
||||
test('a transform can be repeated from its stored input', async ({ page }) => {
|
||||
await open(page, 'trf-clean')
|
||||
await page.locator('[data-testid="studio-take"]').click()
|
||||
await expect(page).toHaveURL(/\/app\/studio\/transform\?.*from=trf-clean.*edge=take/)
|
||||
await expect(page).toHaveURL(/\/app\/addons\/studio\/studio\/transform\?.*from=trf-clean.*edge=take/)
|
||||
await expect(page.locator('[data-testid="studio-handoff"]')).toHaveAttribute('data-status', 'ready')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { test, expect } from './coverage-fixtures.js'
|
||||
import { composer, pickType } from './studio-fixtures.js'
|
||||
import { installStudioAddons } from './studio-addon.js'
|
||||
|
||||
// Studio overview (src/pages/StudioOverview.jsx).
|
||||
//
|
||||
@@ -26,6 +27,8 @@ const SOME_MODELS = {
|
||||
}
|
||||
|
||||
async function mockCapabilities(page, payload = SOME_MODELS) {
|
||||
// Studio is an add-on: install the built bundle before the page opens.
|
||||
await installStudioAddons(page, { ids: ['studio'] })
|
||||
await page.route('**/api/models/capabilities', route => route.fulfill({ json: payload }))
|
||||
}
|
||||
|
||||
@@ -111,7 +114,7 @@ test.describe('Studio overview', () => {
|
||||
await page.route('**/api/auth/status', route => route.fulfill({
|
||||
json: {
|
||||
authEnabled: true,
|
||||
user: { name: 'someone', role: 'user', permissions: { images: true, video: true, tts: true, sound: true } },
|
||||
user: { name: 'someone', role: 'user', permissions: { addons: true, images: true, video: true, tts: true, sound: true } },
|
||||
},
|
||||
}))
|
||||
await mockCapabilities(page)
|
||||
@@ -123,6 +126,7 @@ test.describe('Studio overview', () => {
|
||||
|
||||
test('asks the capabilities endpoint once, not once per modality', async ({ page }) => {
|
||||
let calls = 0
|
||||
await installStudioAddons(page, { ids: ['studio'] })
|
||||
await page.route('**/api/models/capabilities', route => {
|
||||
calls += 1
|
||||
route.fulfill({ json: SOME_MODELS })
|
||||
@@ -179,7 +183,7 @@ test.describe('Studio overview', () => {
|
||||
// and must land on the canonical URL rather than a second spelling of it.
|
||||
await mockCapabilities(page)
|
||||
await page.goto('/app/studio?tab=images')
|
||||
await expect(page).toHaveURL(/\/app\/studio\/images$/)
|
||||
await expect(page).toHaveURL(/\/app\/addons\/studio\/studio\/images$/)
|
||||
await expect(page.locator('[data-testid="studio-workspace"]')).toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,9 @@
|
||||
import { test, expect } from './coverage-fixtures.js'
|
||||
import { installStudioAddons } from './studio-addon.js'
|
||||
|
||||
test.describe('Studio - Transform', () => {
|
||||
test.beforeEach(async ({ page }) => { await installStudioAddons(page, { ids: ['studio'] }) })
|
||||
|
||||
test('Studio exposes a Transform tab that renders Audio Transform', async ({ page }) => {
|
||||
await page.goto('/app/studio/transform')
|
||||
await expect(page.locator('.studio-tab', { hasText: 'Transform' })).toBeVisible()
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { test, expect } from './coverage-fixtures.js'
|
||||
import { installStudioAddons } from './studio-addon.js'
|
||||
|
||||
// The generator workbenches (mock 5b/5c): the control column and the record of
|
||||
// what the form actually sent.
|
||||
|
||||
test.describe('Studio workbench', () => {
|
||||
test.beforeEach(async ({ page }) => { await installStudioAddons(page, { ids: ['studio'] }) })
|
||||
|
||||
test('the compose card is a flat hairline card, not a shadowed one', async ({ page }) => {
|
||||
await page.goto('/app/studio/images')
|
||||
const card = page.locator('[data-testid="ws-compose"]')
|
||||
|
||||
@@ -26,7 +26,7 @@ test.describe('Workspace frame (Images)', () => {
|
||||
await expect(page.locator('[data-testid="studio-workspace"]')).toHaveAttribute('data-type', 'images')
|
||||
await expect(page.locator('.studio-tab')).toHaveCount(8)
|
||||
await page.locator('.studio-tab[data-tab="tts"]').click()
|
||||
await expect(page).toHaveURL(/\/app\/studio\/tts$/)
|
||||
await expect(page).toHaveURL(/\/app\/addons\/studio\/studio\/tts$/)
|
||||
await expect(page.locator('[data-testid="studio-workspace"]')).toHaveAttribute('data-type', 'tts')
|
||||
})
|
||||
|
||||
@@ -146,7 +146,7 @@ test.describe('Workspace frame (Images)', () => {
|
||||
await expect(page.locator('[data-testid="use-in-to-3d"]')).toBeEnabled()
|
||||
await expect(page.locator('[data-testid="use-in-variation"]')).toBeEnabled()
|
||||
await page.locator('[data-testid="use-in-animate"]').click()
|
||||
await expect(page).toHaveURL(/\/app\/studio\/video\?.*from=img-forest.*edge=animate/)
|
||||
await expect(page).toHaveURL(/\/app\/addons\/studio\/studio\/video\?.*from=img-forest.*edge=animate/)
|
||||
await expect(page.locator('[data-testid="studio-handoff"]')).toHaveAttribute('data-status', 'ready')
|
||||
})
|
||||
|
||||
@@ -175,7 +175,7 @@ test.describe('Workspace frame (Images)', () => {
|
||||
await page.goto('/app/studio/images')
|
||||
await tile(page).filter({ hasText: 'Misty pine forest' }).locator('.ws-tile__open').click()
|
||||
await page.locator('[data-testid="ws-lineage"]').click()
|
||||
await expect(page).toHaveURL(/\/app\/studio\?work=img-forest/)
|
||||
await expect(page).toHaveURL(/\/app\/addons\/studio\/studio\?work=img-forest/)
|
||||
await expect(page.locator('[data-testid="studio-lineage"]')).toBeVisible()
|
||||
})
|
||||
|
||||
@@ -187,7 +187,7 @@ test.describe('Workspace frame (Images)', () => {
|
||||
const from = page.locator('[data-testid="ws-from"]')
|
||||
await expect(from).toContainText('animate')
|
||||
await from.getByRole('link').click()
|
||||
await expect(page).toHaveURL(/\/app\/studio\?work=img-harbour$/)
|
||||
await expect(page).toHaveURL(/\/app\/addons\/studio\/studio\?work=img-harbour$/)
|
||||
})
|
||||
|
||||
test('Re-run with edits loads the take, outlines and lists what changed, and runs the edited values', async ({ page }) => {
|
||||
|
||||
@@ -280,6 +280,10 @@ test.describe('Talk session settings', () => {
|
||||
await page.getByTestId('talk-settings-button').click()
|
||||
await page.getByTestId('talk-pipeline').getByRole('button', { name: 'Edit pipeline' }).click()
|
||||
await expect(page).toHaveURL(/\/app\/model-editor\/voice-assistant/)
|
||||
// The editor's Back button reads the way back from the navigation state: the
|
||||
// add-on page, labelled Talk.
|
||||
const state = await page.evaluate(() => window.history.state?.usr)
|
||||
expect(state).toEqual({ from: '/app/addons/talk/talk', fromLabel: 'Talk' })
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { test, expect } from './coverage-fixtures.js'
|
||||
import { installStudioAddons } from './studio-addon.js'
|
||||
|
||||
const animationOperation = {
|
||||
id: 'animate', endpoint: '/3d/animate', output: 'skeleton_animation',
|
||||
@@ -32,6 +33,8 @@ function animationGlb(change = () => {}) {
|
||||
}
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Studio is an add-on: the page opens inside it.
|
||||
await installStudioAddons(page, { ids: ['studio'] })
|
||||
await page.route('**/api/models/capabilities', route => route.fulfill({ json: { data: [
|
||||
{ id: 'trellis-test', capabilities: ['FLAG_3D'] },
|
||||
{ id: 'kimodo-test', capabilities: ['FLAG_3D_ANIMATION'], three_d_operations: [animationOperation] },
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { test, expect } from './coverage-fixtures.js'
|
||||
import { installStudioAddons } from './studio-addon.js'
|
||||
|
||||
// 3D generation page: mock the capabilities + generation endpoints, feed a
|
||||
// real (tiny) Form-B GLB through the parser/viewer, and exercise the
|
||||
@@ -143,6 +144,8 @@ async function pasteImage(page) {
|
||||
|
||||
test.describe('3D generation', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Studio is an add-on: the page opens inside it.
|
||||
await installStudioAddons(page, { ids: ['studio'] })
|
||||
await mockCapabilities(page)
|
||||
await mockGlbDownload(page)
|
||||
})
|
||||
@@ -353,7 +356,7 @@ test.describe('3D generation', () => {
|
||||
body: JSON.stringify({
|
||||
authEnabled: true,
|
||||
staticApiKeyRequired: false,
|
||||
user: { id: 'restricted-user', role: 'user', permissions: { '3d': false } },
|
||||
user: { id: 'restricted-user', role: 'user', permissions: { addons: true, '3d': false } },
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user