mirror of
https://github.com/hanzoai/desktop.git
synced 2026-10-30 14:58:42 +00:00
361 lines
15 KiB
TypeScript
361 lines
15 KiB
TypeScript
// Brand-agnostic deep E2E flows for the @hanzo/ai desktop SDK. Each register*()
|
|
// takes the per-app test object (makeApp result); the brand metadata (name,
|
|
// engine label, foreign brands) rides on the `app` fixture, so these flows are
|
|
// byte-identical across zoo / lux / hanzo — only support/app.ts differs.
|
|
//
|
|
// Assertions are on VISIBLE text / element state (never HTTP status). Flaky,
|
|
// async-mounted UI is wrapped in expect() auto-retry / waitFor.
|
|
import type { AppTest } from './app-fixture';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// onboarding: gate without seed, Home with seed
|
|
// ---------------------------------------------------------------------------
|
|
export function registerOnboarding({ test, expect }: AppTest) {
|
|
test.describe('onboarding', () => {
|
|
test('no seed -> welcome gate with terms checkbox', async ({ page, app }) => {
|
|
await app.gotoFresh('/');
|
|
await expect(page.getByText(`Welcome to ${app.brand}`, { exact: false })).toBeVisible();
|
|
await expect(
|
|
page.getByText('I agree to the Terms of Service', { exact: false }),
|
|
).toBeVisible();
|
|
|
|
const checkbox = page.locator('[role="checkbox"], input[type="checkbox"]').first();
|
|
await expect(checkbox).toBeVisible();
|
|
// Drive the gate: toggle the terms checkbox and assert it registers.
|
|
await checkbox.click();
|
|
await expect
|
|
.poll(async () =>
|
|
(await checkbox.getAttribute('aria-checked')) === 'true' ||
|
|
(await checkbox.isChecked().catch(() => false)),
|
|
)
|
|
.toBeTruthy();
|
|
|
|
// The composer/Home must NOT be present on the gate.
|
|
await expect(page.getByText('How can I help you today?')).toHaveCount(0);
|
|
});
|
|
|
|
test('with seed -> boots past onboarding to Home', async ({ page, app }) => {
|
|
await app.gotoDeep('/');
|
|
await expect(page.getByText('How can I help you today?')).toBeVisible();
|
|
await expect(
|
|
page.getByText('I agree to the Terms of Service', { exact: false }),
|
|
).toHaveCount(0);
|
|
});
|
|
});
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// chat: composer renders, accepts input, toolbar controls work
|
|
// ---------------------------------------------------------------------------
|
|
export function registerChat({ test, expect }: AppTest) {
|
|
test.describe('chat', () => {
|
|
test('home composer renders and accepts a message', async ({ page, app }) => {
|
|
await app.gotoDeep('/');
|
|
await expect(page.getByText('How can I help you today?')).toBeVisible();
|
|
|
|
const composer = page.locator('textarea').first();
|
|
await expect(composer).toBeVisible();
|
|
await expect(composer).toHaveAttribute('placeholder', /Send a message/i);
|
|
|
|
const msg = 'Hello from the deep E2E suite';
|
|
await composer.click();
|
|
await composer.fill(msg);
|
|
await expect(composer).toHaveValue(msg);
|
|
|
|
// Suggestion chips (prompt starters) are part of the empty-state Home.
|
|
await expect(
|
|
page.getByText('Negotiate My Salary Like a Pro', { exact: false }),
|
|
).toBeVisible();
|
|
});
|
|
|
|
test('composer toolbar exposes Tools and Add controls', async ({ page, app }) => {
|
|
await app.gotoDeep('/');
|
|
const composer = page.locator('textarea').first();
|
|
await expect(composer).toBeVisible();
|
|
|
|
const toolsBtn = page.getByRole('button', { name: /^Tools$/ }).first();
|
|
await expect(toolsBtn).toBeVisible();
|
|
await toolsBtn.click();
|
|
await page.waitForTimeout(300);
|
|
// Interaction must not crash the app into the error boundary.
|
|
await expect(page.getByText('Something went wrong')).toHaveCount(0);
|
|
await page.keyboard.press('Escape').catch(() => {});
|
|
|
|
// Composer survives the interaction and remains usable.
|
|
await expect(composer).toBeVisible();
|
|
});
|
|
});
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// add-ai: model-provider select (incl. native <Brand> Engine), form fields
|
|
// ---------------------------------------------------------------------------
|
|
export function registerAddAi({ test, expect }: AppTest) {
|
|
test.describe('add-ai', () => {
|
|
test('model provider select lists the native engine + cloud providers', async ({
|
|
page,
|
|
app,
|
|
}) => {
|
|
await app.gotoDeep('/add-ai');
|
|
await expect(page.getByText('Add AI', { exact: false }).first()).toBeVisible();
|
|
await expect(page.getByText('Model Provider', { exact: false })).toBeVisible();
|
|
|
|
const select = page.locator('select').first();
|
|
await expect(select).toBeVisible();
|
|
const options = await select.locator('option').allInnerTexts();
|
|
// Native local engine is brand-specific (e.g. "Zoo Engine (Local)").
|
|
expect(options.some((o) => o.includes(`${app.engineLabel}`))).toBeTruthy();
|
|
// Cloud providers are brand-agnostic.
|
|
for (const provider of ['OpenAI', 'Claude', 'Gemini']) {
|
|
expect(options.some((o) => o.includes(provider))).toBeTruthy();
|
|
}
|
|
});
|
|
|
|
test('selecting OpenAI reveals URL/API-key fields and form accepts input', async ({
|
|
page,
|
|
app,
|
|
}) => {
|
|
await app.gotoDeep('/add-ai');
|
|
const select = page.locator('select').first();
|
|
await expect(select).toBeVisible();
|
|
|
|
await select.selectOption({ label: 'OpenAI' });
|
|
await expect(select).toHaveValue(/.+/); // a concrete provider is now chosen
|
|
|
|
const name = page.getByLabel('AI Name', { exact: false }).first();
|
|
await expect(name).toBeVisible();
|
|
await name.fill('My E2E Model');
|
|
await expect(name).toHaveValue('My E2E Model');
|
|
|
|
const url = page.getByLabel('External URL', { exact: false }).first();
|
|
await expect(url).toBeVisible();
|
|
await url.fill('http://127.0.0.1:11434');
|
|
await expect(url).toHaveValue('http://127.0.0.1:11434');
|
|
|
|
await expect(page.getByText('API Key', { exact: false }).first()).toBeVisible();
|
|
});
|
|
});
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// agents: My Agents / Explore tabs, Import + Add Agent actions
|
|
// ---------------------------------------------------------------------------
|
|
export function registerAgents({ test, expect }: AppTest) {
|
|
test.describe('agents', () => {
|
|
test('tabs switch between My Agents and Explore', async ({ page, app }) => {
|
|
await app.gotoDeep('/agents');
|
|
await expect(page.getByText('Agents', { exact: false }).first()).toBeVisible();
|
|
|
|
const myAgents = page.getByRole('tab', { name: /My Agents/i });
|
|
const explore = page.getByRole('tab', { name: /Explore/i });
|
|
await expect(myAgents).toBeVisible();
|
|
await expect(explore).toBeVisible();
|
|
|
|
// My Agents empty-state.
|
|
await expect(page.getByText('No available agents', { exact: false })).toBeVisible();
|
|
|
|
await explore.click();
|
|
await expect(explore).toHaveAttribute('aria-selected', 'true');
|
|
|
|
await myAgents.click();
|
|
await expect(myAgents).toHaveAttribute('aria-selected', 'true');
|
|
});
|
|
|
|
test('Add Agent navigates to the agent builder', async ({ page, app }) => {
|
|
await app.gotoDeep('/agents');
|
|
const addAgent = page.getByRole('button', { name: /Add Agent/i }).first();
|
|
await expect(addAgent).toBeVisible();
|
|
await expect(page.getByRole('button', { name: /Import/i }).first()).toBeVisible();
|
|
|
|
await addAgent.click();
|
|
// Lands on the multi-step agent builder.
|
|
await expect
|
|
.poll(async () => {
|
|
const url = page.url();
|
|
const txt = await app.bodyText();
|
|
return /add-agent/.test(url) || /Persona|Create New Agent/i.test(txt);
|
|
}, { timeout: 15_000 })
|
|
.toBeTruthy();
|
|
});
|
|
});
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// tools: list, category filters, Import Tool / New Tool, AI Store link
|
|
// ---------------------------------------------------------------------------
|
|
export function registerTools({ test, expect }: AppTest) {
|
|
test.describe('tools', () => {
|
|
test('tools page lists controls and category filters', async ({ page, app }) => {
|
|
await app.gotoDeep('/tools');
|
|
await expect(page.getByText('Tools', { exact: false }).first()).toBeVisible();
|
|
await expect(
|
|
page.getByText('Manage, customize, and expand your AI tools', { exact: false }),
|
|
).toBeVisible();
|
|
|
|
await expect(page.getByRole('button', { name: /Import Tool/i }).first()).toBeVisible();
|
|
await expect(page.getByRole('button', { name: /New Tool/i }).first()).toBeVisible();
|
|
await expect(page.getByText('Explore AI Store', { exact: false }).first()).toBeVisible();
|
|
|
|
for (const cat of ['All', 'Default Tools', 'My Tools', 'Downloaded']) {
|
|
await expect(page.getByText(cat, { exact: false }).first()).toBeVisible();
|
|
}
|
|
});
|
|
|
|
test('New Tool navigates to the tool creator', async ({ page, app }) => {
|
|
await app.gotoDeep('/tools');
|
|
const newTool = page.getByRole('button', { name: /New Tool/i }).first();
|
|
await expect(newTool).toBeVisible();
|
|
await newTool.click();
|
|
await expect
|
|
.poll(async () => /tools\/create/.test(page.url()) ||
|
|
/Something went wrong/.test(await app.bodyText()) === false &&
|
|
page.url() !== `${app.baseURL}/tools`,
|
|
{ timeout: 10_000 })
|
|
.toBeTruthy();
|
|
// Did not crash into the error boundary.
|
|
await expect(page.getByText('Something went wrong')).toHaveCount(0);
|
|
});
|
|
});
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// settings: each working sub-route heading + nav, with in-app navigation
|
|
// ---------------------------------------------------------------------------
|
|
export function registerSettings({ test, expect }: AppTest) {
|
|
const routes: Array<{ path: string; heading: RegExp; marker: RegExp }> = [
|
|
{ path: '/settings/appearance', heading: /Appearance/, marker: /Chat font/i },
|
|
{ path: '/settings/shortcuts', heading: /Shortcuts/, marker: /keyboard shortcuts/i },
|
|
{ path: '/settings/remote-access', heading: /Remote Access/, marker: /ngrok/i },
|
|
{ path: '/settings/analytics-settings', heading: /Analytics/, marker: /Help us improve/i },
|
|
{ path: '/settings/export-connection', heading: /Export Connection/, marker: /Passphrase/i },
|
|
{ path: '/settings/public-keys', heading: /Public Keys/, marker: /Node Public Keys/i },
|
|
{ path: '/settings/prompt-library', heading: /Prompt Library/, marker: /New Prompt/i },
|
|
];
|
|
|
|
test.describe('settings', () => {
|
|
for (const { path, heading, marker } of routes) {
|
|
test(`renders ${path}`, async ({ page, app }) => {
|
|
await app.gotoDeep(path);
|
|
await expect(page.getByText(heading).first()).toBeVisible();
|
|
await expect(page.getByText(marker).first()).toBeVisible();
|
|
// Settings nav rail is present on every sub-route.
|
|
for (const tab of ['General', 'Appearance', 'Shortcuts', 'Remote Access', 'Analytics']) {
|
|
await expect(page.getByText(tab, { exact: false }).first()).toBeVisible();
|
|
}
|
|
});
|
|
}
|
|
|
|
test('settings nav navigates between sub-routes', async ({ page, app }) => {
|
|
await app.gotoDeep('/settings/appearance');
|
|
await expect(page.getByText('Chat font', { exact: false })).toBeVisible();
|
|
|
|
await page.getByRole('link', { name: /^Shortcuts$/ }).first().click();
|
|
await expect
|
|
.poll(() => page.url(), { timeout: 10_000 })
|
|
.toMatch(/\/settings\/shortcuts/);
|
|
await expect(page.getByText('keyboard shortcuts', { exact: false })).toBeVisible();
|
|
});
|
|
});
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// files (vector-fs): AI Files Explorer renders, Add New control
|
|
// ---------------------------------------------------------------------------
|
|
export function registerFiles({ test, expect }: AppTest) {
|
|
test.describe('files', () => {
|
|
test('AI Files Explorer renders with empty-state and Add New', async ({ page, app }) => {
|
|
await app.gotoDeep('/vector-fs');
|
|
await expect(page.getByText('AI Files Explorer', { exact: false })).toBeVisible();
|
|
await expect(
|
|
page.getByText('This will be the home for all your files', { exact: false }),
|
|
).toBeVisible();
|
|
await expect(page.getByText('No files found', { exact: false })).toBeVisible();
|
|
|
|
const addNew = page.getByRole('button', { name: /Add New/i }).first();
|
|
await expect(addNew).toBeVisible();
|
|
await addNew.click();
|
|
await page.waitForTimeout(300);
|
|
await expect(page.getByText('Something went wrong')).toHaveCount(0);
|
|
});
|
|
});
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ais: Manage AIs page renders with empty-state + Add AI
|
|
// ---------------------------------------------------------------------------
|
|
export function registerAis({ test, expect }: AppTest) {
|
|
test.describe('ais', () => {
|
|
test('Manage AIs renders empty-state and Add AI', async ({ page, app }) => {
|
|
await app.gotoDeep('/ais');
|
|
await expect(page.getByText('Manage AIs', { exact: false })).toBeVisible();
|
|
await expect(page.getByText('No available AIs', { exact: false })).toBeVisible();
|
|
await expect(page.getByRole('button', { name: /Add AI/i }).first()).toBeVisible();
|
|
});
|
|
});
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// brand: own brand present, foreign brands absent in visible text
|
|
// ---------------------------------------------------------------------------
|
|
export function registerBrand({ test, expect }: AppTest) {
|
|
test.describe('brand', () => {
|
|
test('document title and brand nav reflect this brand', async ({ page, app }) => {
|
|
await app.gotoDeep('/');
|
|
await expect(page).toHaveTitle(app.titleRe);
|
|
await expect(page.getByText(`Get Started with ${app.brand}`, { exact: false })).toBeVisible();
|
|
});
|
|
|
|
test('no foreign brand leaks into visible UI text', async ({ app }) => {
|
|
if (app.foreign.length === 0) {
|
|
test.skip(true, 'this brand has no foreign-brand constraint');
|
|
return;
|
|
}
|
|
const routes = ['/', '/ais', '/add-ai', '/tools', '/settings/appearance'];
|
|
for (const route of routes) {
|
|
await app.gotoDeep(route);
|
|
const text = await app.bodyText();
|
|
for (const brand of app.foreign) {
|
|
expect(
|
|
text.includes(brand),
|
|
`route ${route} visible text must not contain foreign brand "${brand}"`,
|
|
).toBeFalsy();
|
|
}
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// regressions: routes blocked by pre-existing bundle bugs in the current dist
|
|
// (documented, not asserted-as-good). Unskip once the bundle is fixed.
|
|
// ---------------------------------------------------------------------------
|
|
export function registerRegressions({ test }: AppTest) {
|
|
test.describe('regressions (known-blocked)', () => {
|
|
test.fixme(
|
|
'/inboxes — chat-list query maps over undefined in current bundle',
|
|
async ({ app }) => {
|
|
await app.gotoDeep('/inboxes');
|
|
},
|
|
);
|
|
test.fixme(
|
|
'/settings (General index) — TDZ "Cannot access before initialization" in current bundle',
|
|
async ({ app }) => {
|
|
await app.gotoDeep('/settings');
|
|
},
|
|
);
|
|
});
|
|
}
|
|
|
|
export function registerAll(appTest: AppTest) {
|
|
registerOnboarding(appTest);
|
|
registerChat(appTest);
|
|
registerAddAi(appTest);
|
|
registerAgents(appTest);
|
|
registerTools(appTest);
|
|
registerSettings(appTest);
|
|
registerFiles(appTest);
|
|
registerAis(appTest);
|
|
registerBrand(appTest);
|
|
registerRegressions(appTest);
|
|
}
|