122 lines
7.1 KiB
TypeScript
122 lines
7.1 KiB
TypeScript
import { test, expect, type Page } from '@playwright/test';
|
|
import { createServer, type ViteDevServer } from 'vite';
|
|
import { resolve } from 'node:path';
|
|
|
|
let server: ViteDevServer;
|
|
let baseUrl: string;
|
|
test.use({ channel: process.env.MAKELORE_LAYOUT_BROWSER_CHANNEL, video: 'off' });
|
|
test.beforeAll(async () => {
|
|
server = await createServer({
|
|
configFile: false,
|
|
cacheDir: 'node_modules/.vite-preview-layout',
|
|
resolve: { alias: { '@': resolve('src') } },
|
|
esbuild: { jsx: 'automatic' },
|
|
server: { host: '127.0.0.1', port: 0 },
|
|
plugins: [{
|
|
name: 'isolated-preview-layout', enforce: 'pre',
|
|
load(id) {
|
|
if (id.endsWith('/src/lib/host-events.ts')) return 'export function subscribeHostEvent() { return () => {}; }';
|
|
if (id.endsWith('/src/lib/host-api.ts')) return `
|
|
const browser = { browserId: 'fixture', projectId: 'layout-fixture', state: 'attached', generation: 1, url: 'http://127.0.0.1:4173/', visible: true, bounds: null, eventCursor: 0 };
|
|
export async function hostApiFetch(path) { return path.includes('ensure-work') ? { status: 'ready', browser } : { success: true, browser }; }
|
|
`;
|
|
},
|
|
configureServer(vite) {
|
|
vite.middlewares.use((req, res, next) => {
|
|
if (req.url !== '/' && !req.url?.startsWith('/?')) return next();
|
|
res.setHeader('Content-Type', 'text/html');
|
|
res.end('<html><head><meta charset="utf-8"></head><body><div id="root"></div><script type="module" src="/tests/e2e/fixtures/teacher-preview-layout.tsx"></script></body></html>');
|
|
});
|
|
},
|
|
}],
|
|
});
|
|
await server.listen();
|
|
baseUrl = server.resolvedUrls!.local[0];
|
|
});
|
|
test.afterAll(async () => { await server?.close(); });
|
|
|
|
for (const width of [520, 1000]) {
|
|
test(`distributed agent tabs scroll inside the title bar at ${width}px`, async ({ page }) => {
|
|
await page.setViewportSize({ width, height: 800 });
|
|
await page.goto(baseUrl + '?agents=8');
|
|
const tabs = page.getByRole('navigation', { name: '已下发智能体' });
|
|
await expect(tabs.getByRole('button')).toHaveCount(8);
|
|
await expect(page.getByRole('combobox', { name: '新话题使用的智能体' })).toHaveCount(0);
|
|
const last = tabs.getByRole('button', { name: '与智能体8聊天' });
|
|
await last.focus();
|
|
await last.press('Enter');
|
|
await expect(last).toHaveAttribute('aria-pressed', 'true');
|
|
const bounds = await page.evaluate(() => {
|
|
const strip = document.querySelector('.consultation-agent-tabs')!;
|
|
const controls = document.querySelector('[data-testid="window-controls"]')!;
|
|
return { right: strip.getBoundingClientRect().right, controlsLeft: controls.getBoundingClientRect().left,
|
|
scroll: strip.scrollLeft, documentWidth: document.documentElement.scrollWidth };
|
|
});
|
|
expect(bounds.right).toBeLessThanOrEqual(bounds.controlsLeft);
|
|
expect(bounds.documentWidth).toBeLessThanOrEqual(width);
|
|
expect(bounds.scroll).toBeGreaterThan(0);
|
|
await page.screenshot({ path: test.info().outputPath('distributed-agents-titlebar.png') });
|
|
});
|
|
}
|
|
|
|
async function geometry(page: Page) {
|
|
return page.evaluate(() => {
|
|
const box = (selector: string) => document.querySelector(selector)!.getBoundingClientRect();
|
|
const bubble = box('[data-testid="teacher-presence"]');
|
|
const viewport = box('[data-testid="agent-browser-viewport"]');
|
|
const panel = box('[data-testid="agent-browser-panel"]');
|
|
const work = box('[data-testid="work-view"]');
|
|
return { gap: panel.top - work.top, clearance: Number.parseFloat((document.querySelector('[data-testid="agent-browser-panel"]') as HTMLElement).style.marginTop) || 0, nativeGap: viewport.top - bubble.bottom, sideBySide: bubble.left >= viewport.right || bubble.right <= viewport.left };
|
|
});
|
|
}
|
|
|
|
test('distributed icons load over HTTP and broken images fall back in the titlebar', async ({ page }) => {
|
|
await page.route('https://yuxi.example.test/api/storage/public/images/agent-0.png', route => route.fulfill({
|
|
contentType: 'image/svg+xml',
|
|
body: '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32"><circle cx="16" cy="16" r="15" fill="blue"/></svg>',
|
|
}));
|
|
await page.route('https://yuxi.example.test/api/storage/public/images/agent-1.png', route => route.fulfill({ status: 404 }));
|
|
await page.goto(`${baseUrl}?agents=2&icons=1`);
|
|
const custom = page.getByRole('button', { name: '与智能体1聊天' }).locator('img');
|
|
const fallback = page.getByRole('button', { name: '与智能体2聊天' }).locator('img');
|
|
const speaker = page.getByRole('button', { name: '打开这条智能体消息' }).locator('img');
|
|
await expect(custom).toHaveAttribute('src', 'https://yuxi.example.test/api/storage/public/images/agent-0.png');
|
|
await expect(custom).toHaveCSS('image-rendering', 'auto');
|
|
await expect(speaker).toHaveAttribute('src', 'https://yuxi.example.test/api/storage/public/images/agent-0.png');
|
|
await expect(fallback).toHaveAttribute('src', /avatar-06\.png/);
|
|
for (const img of [custom, fallback, speaker]) {
|
|
await expect.poll(() => img.evaluate(node => (node as HTMLImageElement).naturalWidth)).toBeGreaterThan(0);
|
|
}
|
|
await page.screenshot({ path: test.info().outputPath('distributed-icons.png') });
|
|
});
|
|
|
|
for (const size of [{ width: 1440, height: 900 }, { width: 1180, height: 720 }, { width: 1000, height: 640 }]) {
|
|
test(`preview only clears overlapping speech at ${size.width}x${size.height}`, async ({ page }) => {
|
|
await page.setViewportSize(size);
|
|
const errors: string[] = [];
|
|
page.on('pageerror', (error) => errors.push(error.message));
|
|
await page.goto(baseUrl);
|
|
await expect(page.getByTestId('teacher-invitation')).toBeVisible();
|
|
await expect.poll(async () => (await geometry(page)).nativeGap).toBeGreaterThanOrEqual(8);
|
|
expect((await geometry(page)).gap).toBeLessThan(65);
|
|
await page.getByRole('button', { name: '智能体', exact: true }).click();
|
|
await expect.poll(async () => (await geometry(page)).sideBySide).toBe(true);
|
|
await expect.poll(async () => (await geometry(page)).gap).toBe(20);
|
|
// A narrow dock can overlap again; increasing its width must give the
|
|
// preview all its space back without hiding the unread message.
|
|
await page.getByRole('slider', { name: '智能体栏宽度' }).press('Home');
|
|
await expect.poll(async () => { const g = await geometry(page); return g.sideBySide || g.nativeGap >= 8; }).toBe(true);
|
|
await page.getByRole('slider', { name: '智能体栏宽度' }).press('End');
|
|
await expect.poll(async () => (await geometry(page)).gap).toBe(20);
|
|
await page.screenshot({ path: test.info().outputPath('work-with-teacher-no-gap.png') });
|
|
await page.getByRole('button', { name: '关闭智能体栏' }).click();
|
|
await expect.poll(async () => (await geometry(page)).nativeGap).toBeGreaterThanOrEqual(8);
|
|
await page.getByRole('button', { name: '切换消息长度' }).click();
|
|
await expect.poll(async () => (await geometry(page)).gap).toBe(20);
|
|
await page.getByRole('button', { name: '等会儿聊' }).click();
|
|
await expect(page.getByTestId('teacher-invitation')).toHaveCount(0);
|
|
await expect(page.getByTestId('agent-browser-panel')).toHaveCSS('margin-top', '0px');
|
|
expect(errors).toEqual([]);
|
|
});
|
|
}
|