import { mkdir, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import { closeElectronApp, expect, getStableWindow, test } from './fixtures/electron'; import { billingSummary } from '../fixtures/permanent-points'; async function seedInactiveCodingProjects(resources: { rootDir: string; userDataDir: string }) { const now = '2026-09-06T08:00:00.000Z'; const older = '2026-09-04T08:00:00.000Z'; const projects = [ { id: 'project-moonlight', name: '月光小游戏', path: join(resources.rootDir, '月光小游戏'), createdAt: older, updatedAt: now, lastOpenedAt: now, projectId: '11111111-1111-4111-8111-111111111111', }, { id: 'project-storybook', name: '故事绘本', path: join(resources.rootDir, '故事绘本'), createdAt: older, updatedAt: older, lastOpenedAt: older, projectId: '22222222-2222-4222-8222-222222222222', }, ]; for (const project of projects) { await mkdir(join(project.path, '.makelore'), { recursive: true }); await mkdir(join(project.path, 'knowledge'), { recursive: true }); await writeFile(join(project.path, '.makelore', 'project.json'), JSON.stringify({ schemaVersion: 2, projectType: 'custom', projectId: project.projectId, initialized: true, agents: [], knowledgeDirectory: 'knowledge', createdAt: project.createdAt, updatedAt: project.updatedAt, })); } await writeFile(join(resources.userDataDir, 'makelore-projects.json'), JSON.stringify({ projects: { projects: Object.fromEntries(projects.map(({ projectId: _projectId, ...project }) => [project.id, project])), activeProjectId: null, }, })); } test.describe('Makelore module navigation without setup flow', () => { test('keeps the home account wallet, profile, settings and logout available without creating a payment', async ({ launchElectronApp }, testInfo) => { const app = await launchElectronApp({ skipSetup: true }); try { const page = await getStableWindow(app); await expect(page.getByTestId('ai-module-selection-page')).toBeVisible(); await app.evaluate(({ ipcMain, BrowserWindow }, summary) => { const window = BrowserWindow.getAllWindows()[0]; window.setSize(1440, 900); const state = { loggedIn: true, requests: [] as { method: string; path: string }[] }; Object.assign(globalThis, { homeAccountFixture: state }); const session = () => state.loggedIn ? { accessToken: 'home-account-fixture', tokenType: 'Bearer', expiresAt: Date.now() + 3600000, lastActiveAt: Date.now(), canRefresh: false } : null; // Main pushes its session after each Renderer load. Own that event boundary // together with sync/me so the isolated fixture cannot receive a guest push. const send = window.webContents.send.bind(window.webContents); window.webContents.send = (channel: string, ...args: unknown[]) => send(channel, ...(channel === 'auth:session-changed' ? [session()] : args)); const result = (json: unknown) => ({ ok: true, data: { status: 200, ok: true, json } }); ipcMain.removeHandler('hostapi:fetch'); ipcMain.handle('hostapi:fetch', async (_event, request: { path?: string; method?: string }) => { const path = request.path ?? ''; const method = (request.method ?? 'GET').toUpperCase(); state.requests.push({ method, path }); if (path === '/api/auth/session/sync') return result({ success: true, session: session() }); if (path === '/api/auth/me') return result({ success: true, user: state.loggedIn ? { username: 'child-wallet', userId: 'home-user', tenantId: null, deptId: null, authorities: [] } : null, moduleAccess: { programming: true, design: true, robot: true, cloud_agents: true } }); if (path === '/api/auth/logout' && method === 'POST') { state.loggedIn = false; return result({ success: true }); } if (method === 'GET') { if (['/api/provider-accounts', '/api/provider-accounts/key-info', '/api/provider-vendors', '/api/providers'].includes(path)) return result([]); if (path === '/api/provider-accounts/default') return result({ accountId: null }); if (path === '/api/coding/projects') return result({ projects: [], activeProjectId: null }); if (path === '/api/settings') return result({ classroomSidebarWidth: 171, sidebarCollapsed: false, setupComplete: true }); if (path === '/api/user-sync/bootstrap') return result({ success: true, bootstrap: { preferences: {}, providerAccounts: [], partners: [], usageAggregates: [] } }); } if (path === '/api/works/user/agent-profile' && method === 'GET') return result({ success: true, profile: { display_name: '小麦', age: null, gender: null, avatar_url: null, share_age_with_agents: false, share_gender_with_agents: false, analysis_enabled: true, completed: true, version: 1, updated_at: '2026-10-13T00:00:00Z' } }); if (path === '/api/works/billing/me' && method === 'GET') return result({ success: true, data: summary }); if (path.startsWith('/api/works/billing/recharge/orders?') && method === 'GET') return result({ success: true, data: { items: [] } }); if (path === '/api/works/image-workspace' && method === 'GET') return result({ success: true, data: { capabilities: { conversation: true, generation: false, image: false, video: false, generationOptions: { image: { available: false, supportedAspectRatios: [], supportedDurationSeconds: [], maxOutputCount: 1, requiresFirstFrame: false, supportedReferenceRoles: [] }, video: { available: false, supportedAspectRatios: [], supportedDurationSeconds: [], maxOutputCount: 1, requiresFirstFrame: true, supportedReferenceRoles: [] }, } }, workspaces: [], } }); if (path.startsWith('/api/cloud-agents/') && method === 'GET') { if (path.endsWith('/recovery')) return result({ recent: null, pending: [] }); if (path.endsWith('/agents') || path.endsWith('/bootstrap')) return result({ agents: [], next_cursor: null }); } if (path.startsWith('/api/works/billing/') || path === '/api/works/user/agent-profile') throw new Error('Unexpected home account write ' + method + ' ' + path); return result({ success: true }); }); }, billingSummary()); // The setup-skipping fixture starts as a guest. Hydrate only this isolated // Renderer with a fake persisted session before the auth store is created. await page.addInitScript(() => { const now = Date.now(); localStorage.setItem('niancode-settings', JSON.stringify({ version: 0, state: { classroomSidebarWidth: 171, sidebarCollapsed: false } })); localStorage.setItem('niancode-auth', JSON.stringify({ version: 4, state: { authBase: 'https://biz.nianxx.cn/auth/', clientId: 'app', accessToken: 'home-account-fixture', tokenType: 'Bearer', expiresAt: now + 3600000, lastActiveAt: now, canRefresh: false, legacyRefreshToken: null, user: { username: 'child-wallet', userId: 'home-user', tenantId: null, deptId: null, authorities: [] }, moduleAccess: { programming: true, design: true, robot: true, cloud_agents: true }, } })); // Setup-skipping mode also suppresses the home profile sync. Restore the // isolated account's cached profile in the store's persisted shape. localStorage.setItem('niancode-user-agent-profiles', JSON.stringify({ version: 1, state: { profilesByUserId: { 'home-user': { displayName: '小麦', age: null, gender: 'undisclosed', avatarUrl: null, shareAgeWithAgents: false, shareGenderWithAgents: false, analysisEnabled: true, pendingSync: false, version: 1, updatedAt: '2026-10-13T00:00:00Z' } }, } })); }); await page.reload({ waitUntil: 'domcontentloaded' }); const trigger = page.getByTestId('home-account-menu-trigger'); const menu = page.getByTestId('home-account-menu'); const surface = page.getByTestId('home-account-trigger-surface'); const checkAccountGeometry = async (width: number) => { await expect.poll(async () => (await menu.boundingBox())!.width).toBeCloseTo(288, 1); const visible = (await surface.boundingBox())!; const target = (await trigger.boundingBox())!; const avatar = (await surface.locator('span').first().boundingBox())!; await expect.poll(async () => (await surface.locator('svg').boundingBox())!.width).toBeCloseTo(14, 1); const chevron = (await surface.locator('svg').boundingBox())!; const panel = (await menu.boundingBox())!; expect(visible.width).toBeCloseTo(150, 1); expect(visible.height).toBeCloseTo(42, 1); expect(avatar.width).toBeCloseTo(28, 1); expect(avatar.height).toBeCloseTo(28, 1); await expect(surface).toHaveCSS('font-size', '12px'); expect(chevron.width).toBeCloseTo(14, 1); expect(chevron.height).toBeCloseTo(14, 1); expect(target.width).toBeCloseTo(150, 1); expect(target.height).toBeCloseTo(42, 1); expect(visible.x).toBeCloseTo(10, 1); expect(await page.evaluate(() => innerHeight) - visible.y - visible.height).toBeCloseTo(12, 0); expect(panel.x).toBeCloseTo(visible.x, 1); expect(visible.y - panel.y - panel.height).toBeCloseTo(8, 1); expect(panel.y).toBeGreaterThanOrEqual(0); expect(panel.x + panel.width).toBeLessThanOrEqual(width); }; const readTriggerAppearance = (accountTrigger: typeof trigger) => accountTrigger.evaluate(element => { const face = element.firstElementChild!; const bounds = element.getBoundingClientRect(); const avatar = face.firstElementChild!.getBoundingClientRect(); const arrow = face.querySelector('svg')!.getBoundingClientRect(); const style = getComputedStyle(face); return { width: bounds.width, height: bounds.height, left: bounds.left, bottom: innerHeight - bounds.bottom, avatarWidth: avatar.width, avatarHeight: avatar.height, arrowWidth: arrow.width, arrowHeight: arrow.height, fontSize: style.fontSize, color: style.color, background: style.backgroundColor, border: style.borderTopColor, radius: style.borderRadius, padding: style.paddingLeft, gap: style.gap }; }); const checkSettingsShell = async () => { await expect(page.getByTestId('settings-page')).toBeVisible(); await expect(page.getByTestId('sidebar')).toHaveCount(0); await expect(page.getByTestId('titlebar-sidebar-toggle')).toHaveCount(0); await expect(page.getByTestId('coding-conversation-header')).toHaveCount(0); await expect(page.getByTestId('settings-back-button')).toBeVisible(); }; await expect(trigger).toContainText('小麦'); const homeAppearance = await readTriggerAppearance(trigger); await expect(page.locator('[data-testid^="ai-module-option-"]')).toHaveCount(3); const footer = await trigger.evaluate(element => element.closest('footer')!.getBoundingClientRect().top); const cards = await page.getByTestId('ai-module-options').boundingBox(); expect(footer).toBeGreaterThanOrEqual(cards!.y + cards!.height); await trigger.click(); await expect(menu.getByTestId('sidebar-total-token-points')).toHaveText('75 点'); await expect(menu.getByText('永久有效 · 预占 5 点', { exact: true })).toBeVisible(); await expect(menu.getByRole('menuitem', { name: '词元点数与充值', exact: true })).toBeVisible(); await checkAccountGeometry(1440); await page.screenshot({ path: testInfo.outputPath('home-account-menu-desktop.png') }); await page.keyboard.press('Escape'); await expect(trigger).toHaveAttribute('aria-expanded', 'false'); await expect(trigger).toBeFocused(); await trigger.click(); await page.getByRole('heading', { name: '今天,想创作什么?', exact: true }).click(); await expect(trigger).toHaveAttribute('aria-expanded', 'false'); await trigger.click(); await menu.getByRole('menuitem', { name: '词元点数与充值', exact: true }).click(); const wallet = page.getByTestId('point-wallet-dialog'); await expect(wallet).toBeVisible(); await expect(wallet.getByTestId('point-recharge-recipient')).toHaveText('充值到:小麦(child-wallet)'); await expect(wallet.getByRole('button', { name: /500 点\s*¥10.00/ })).toBeEnabled(); await wallet.getByText('我的可用点数', { exact: true }).click(); await expect(trigger).toHaveAttribute('aria-expanded', 'false'); await expect(wallet).toBeVisible(); await wallet.getByRole('button', { name: '关闭词元点数', exact: true }).click(); await expect(wallet).toHaveCount(0); await trigger.click(); await menu.getByRole('menuitem', { name: '个人资料', exact: true }).click(); const profile = page.getByRole('dialog', { name: '个人资料', exact: true }); await expect(profile.getByLabel(/名字/)).toHaveValue('小麦'); await profile.getByRole('button', { name: '关闭个人资料', exact: true }).click(); await trigger.click(); await menu.getByRole('menuitem', { name: '设置', exact: true }).click(); await expect(page).toHaveURL(/\/settings$/); await checkSettingsShell(); expect(await readTriggerAppearance(trigger)).toEqual(homeAppearance); await page.getByTestId('settings-back-button').click(); await expect(page).toHaveURL(/\/module-select$/); await expect(trigger).toContainText('小麦'); let codeAppearance: typeof homeAppearance | undefined; for (const [label, origin] of [['code', '/chat'], ['agents', '/cloud-agents?view=mine'], ['canvas', '/image-canvas']]) { await page.evaluate(path => { window.location.hash = '#' + path; }, origin); const sidebarTrigger = page.getByTestId('sidebar-member-menu-trigger'); await expect(sidebarTrigger).toBeVisible(); // Route changes may replace the visible sidebar during lazy workspace mounting. await expect.poll(() => readTriggerAppearance(sidebarTrigger)).toEqual(homeAppearance); const appearance = await readTriggerAppearance(sidebarTrigger); if (label === 'code') { codeAppearance = appearance; expect(homeAppearance).toEqual(codeAppearance); } else expect(appearance).toEqual(codeAppearance); await sidebarTrigger.click(); await page.screenshot({ path: testInfo.outputPath(`account-menu-${label}-desktop.png`) }); await page.getByTestId('sidebar-account-menu').getByRole('menuitem', { name: '设置', exact: true }).click(); await expect(page).toHaveURL(/\/settings$/); await checkSettingsShell(); expect(await readTriggerAppearance(trigger)).toEqual(codeAppearance); await page.getByTestId('settings-back-button').click(); await expect.poll(() => page.evaluate(() => window.location.hash)).toBe('#' + origin); await expect(sidebarTrigger).toBeVisible(); } await page.getByTestId('sidebar-module-switcher-trigger').click(); await expect(page).toHaveURL(/\/module-select$/); await app.evaluate(({ BrowserWindow }) => { const window = BrowserWindow.getAllWindows()[0]; window.setMinimumSize(360, 600); window.setSize(375, 800); }); await expect.poll(() => page.evaluate(() => innerWidth)).toBe(375); await trigger.click(); await expect(menu.getByTestId('sidebar-total-token-points')).toHaveText('75 点'); await expect.poll(() => page.evaluate(() => document.documentElement.scrollWidth - innerWidth)).toBeLessThanOrEqual(1); await checkAccountGeometry(375); await page.screenshot({ path: testInfo.outputPath('home-account-menu-375.png') }); await menu.getByRole('menuitem', { name: '设置', exact: true }).click(); await expect(page).toHaveURL(/\/settings$/); await checkSettingsShell(); await trigger.click(); await expect(menu.getByTestId('sidebar-total-token-points')).toHaveText('75 点'); await checkAccountGeometry(375); await page.screenshot({ path: testInfo.outputPath('settings-account-menu-375.png') }); await page.keyboard.press('Escape'); await page.getByTestId('settings-back-button').click(); await expect(page).toHaveURL(/\/module-select$/); await page.evaluate(() => { window.location.hash = '#/image-canvas'; }); await expect(page.getByTestId('standalone-account-dock')).toBeVisible(); await expect.poll(() => readTriggerAppearance(trigger)).toEqual(homeAppearance); await trigger.click(); await checkAccountGeometry(375); await page.screenshot({ path: testInfo.outputPath('canvas-account-menu-375.png') }); await menu.getByRole('menuitem', { name: '设置', exact: true }).click(); await checkSettingsShell(); await page.getByTestId('settings-back-button').click(); await expect(page).toHaveURL(/\/image-canvas$/); await page.evaluate(() => { window.location.hash = '#/module-select'; }); await expect(trigger).toBeVisible(); await trigger.click(); await menu.getByRole('menuitem', { name: '退出登录', exact: true }).click(); await expect(page).toHaveURL(/\/login$/); await expect(trigger).toHaveCount(0); const requests = await app.evaluate(() => (globalThis as typeof globalThis & { homeAccountFixture: { requests: { method: string; path: string }[] } }).homeAccountFixture.requests); const billingReads = requests.filter(request => request.path.startsWith('/api/works/billing/')); expect(billingReads.length).toBeGreaterThan(0); expect(billingReads.every(request => request.method === 'GET')).toBe(true); expect(requests.filter(request => request.path === '/api/works/user/agent-profile').every(request => request.method === 'GET')).toBe(true); expect(requests.filter(request => request.path === '/api/auth/logout' && request.method === 'POST')).toHaveLength(1); } catch (error) { const page = app.windows().at(-1); if (page && !page.isClosed()) await testInfo.attach('account-failure-dom', { body: await page.locator('body').innerText(), contentType: 'text/plain' }); await testInfo.attach('account-fixture-requests', { body: JSON.stringify(await app.evaluate(() => (globalThis as typeof globalThis & { homeAccountFixture?: unknown }).homeAccountFixture)), contentType: 'application/json', }); throw error; } finally { await app.evaluate(({ BrowserWindow }) => { for (const window of BrowserWindow.getAllWindows()) window.destroy(); }).catch(() => undefined); await closeElectronApp(app); } }); test('opens remembered Code projects and keeps the module return entry in chat instead of project settings', async ({ electronTestResources, launchElectronApp, }) => { await seedInactiveCodingProjects(electronTestResources); const app = await launchElectronApp({ skipSetup: true }); try { const page = await getStableWindow(app); await expect(page.getByTestId('ai-module-selection-page')).toBeVisible(); await page.getByTestId('ai-module-option-programming').click(); await expect(page).toHaveURL(/\/chat$/); await expect(page.getByRole('button', { name: '新增项目' })).toBeVisible(); await expect(page.getByText('已有项目')).toHaveCount(0); await expect(page.getByText('选择一个项目继续')).toHaveCount(0); const projectCards = page.getByTestId('coding-existing-projects'); await expect(projectCards).toBeVisible(); await expect(projectCards.getByRole('button')).toHaveCount(2); await expect(projectCards.getByRole('button').nth(0)).toContainText('月光小游戏'); await expect(projectCards.getByRole('button').nth(1)).toContainText('故事绘本'); await projectCards.getByRole('button', { name: '进入项目 故事绘本' }).click(); await expect(page.getByTestId('coding-chat-panel')).toBeVisible(); await expect(page.getByTestId('coding-existing-projects')).toHaveCount(0); await expect(page.getByTestId('sidebar-course-project-project-storybook')).toHaveAttribute('aria-current', 'page'); const sidebar = page.getByTestId('sidebar'); const moduleReturn = page.getByTestId('sidebar-module-switcher-trigger'); await expect(moduleReturn).toBeVisible(); await expect(moduleReturn).toHaveText(''); await expect(moduleReturn).toHaveAttribute('aria-label', '返回首页'); await expect(moduleReturn).toHaveAttribute('title', '返回首页'); await expect(page.getByTestId('sidebar-module-return-icon')).toBeVisible(); const brandBox = await sidebar.locator('.classroom-brand').boundingBox(); const returnBox = await moduleReturn.boundingBox(); const collapseBox = await page.getByRole('button', { name: '折叠侧栏' }).boundingBox(); const sidebarBox = await sidebar.boundingBox(); expect(brandBox).not.toBeNull(); expect(returnBox).not.toBeNull(); expect(collapseBox).not.toBeNull(); expect(returnBox!.y + returnBox!.height).toBeLessThanOrEqual(brandBox!.y); const headerBox = await page.getByTestId('coding-conversation-header').boundingBox(); const tabsBox = await page.getByRole('tablist', { name: '当前工作' }).boundingBox(); expect(Math.abs(returnBox!.y + returnBox!.height / 2 - headerBox!.y - headerBox!.height / 2)).toBeLessThan(1); expect(Math.abs(brandBox!.y + brandBox!.height / 2 - tabsBox!.y - tabsBox!.height / 2)).toBeLessThan(1); expect(brandBox!.y - sidebarBox!.y).toBe(0); expect(brandBox!.height).toBe(tabsBox!.height); expect(returnBox!.width).toBe(32); expect(Math.abs(returnBox!.y + returnBox!.height / 2 - collapseBox!.y - collapseBox!.height / 2)).toBeLessThan(1); expect(returnBox!.x + returnBox!.width).toBeLessThanOrEqual(collapseBox!.x); expect(sidebarBox!.x + sidebarBox!.width - collapseBox!.x - collapseBox!.width).toBeLessThanOrEqual(14); await expect(page.getByTestId('titlebar-sidebar-toggle')).toHaveCount(1); const community = sidebar.getByRole('button', { name: '作品广场', exact: true }); await expect(community).toHaveCount(1); const communityBox = await community.boundingBox(); const createBox = await sidebar.getByTestId('sidebar-create-project').boundingBox(); const headingBox = await sidebar.locator('.classroom-projects-heading').boundingBox(); expect(communityBox!.y).toBeGreaterThanOrEqual(brandBox!.y + brandBox!.height); expect(communityBox!.y - brandBox!.y - brandBox!.height).toBe(10); expect(createBox!.y - communityBox!.y - communityBox!.height).toBe(10); expect(createBox!.y - brandBox!.y - brandBox!.height).toBe(61); expect(communityBox!.height).toBe(createBox!.height); await expect(community).toHaveCSS('background-color', 'rgb(248, 250, 245)'); const buttonStyles = await page.evaluate(() => { const styles = (selector: string) => { const s = getComputedStyle(document.querySelector(selector)!); return [s.borderRadius, s.borderWidth, s.borderColor, s.boxShadow, s.padding, s.fontSize, s.fontWeight]; }; return { community: styles('[data-testid=sidebar] .community-link'), create: styles('[data-testid=sidebar-create-project]') }; }); expect(buttonStyles.community).toEqual(buttonStyles.create); expect(headingBox!.y).toBeGreaterThanOrEqual(createBox!.y + createBox!.height); expect(headingBox!.y - createBox!.y - createBox!.height).toBeLessThanOrEqual(14); await page.screenshot({ path: test.info().outputPath('chat-code-return-entry.png') }); // Exercise native state events without switching the test app into a macOS Space. await app.evaluate(({ BrowserWindow }) => BrowserWindow.getAllWindows()[0].emit('enter-full-screen')); await expect.poll(async () => (await moduleReturn.boundingBox())!.x).toBe(createBox!.x); const fullscreenReturn = await moduleReturn.boundingBox(); expect(fullscreenReturn!.y).toBe(returnBox!.y); expect(await page.getByRole('button', { name: '折叠侧栏' }).boundingBox()).toEqual(collapseBox); expect(await sidebar.locator('.classroom-brand').boundingBox()).toEqual(brandBox); await page.screenshot({ path: test.info().outputPath('chat-fullscreen-return-aligned.png') }); await app.evaluate(({ BrowserWindow }) => BrowserWindow.getAllWindows()[0].emit('leave-full-screen')); await expect.poll(async () => (await moduleReturn.boundingBox())!.x).toBe(returnBox!.x); const sidebarResizer = sidebar.getByRole('separator', { name: '调整左侧功能栏宽度' }); await sidebarResizer.press('Home'); await expect(sidebarResizer).toHaveAttribute('aria-valuenow', '128'); const narrowReturn = await moduleReturn.boundingBox(); const narrowToggle = await page.getByRole('button', { name: '折叠侧栏' }).boundingBox(); expect(narrowReturn!.x + narrowReturn!.width).toBeLessThanOrEqual(narrowToggle!.x); await expect(moduleReturn).toBeVisible(); await expect(page.getByRole('button', { name: '折叠侧栏' })).toBeVisible(); const narrowHeader = await page.getByTestId('coding-conversation-header').boundingBox(); expect(narrowToggle!.x + narrowToggle!.width).toBeLessThanOrEqual(narrowHeader!.x); expect(await moduleReturn.evaluate(el => { const b = el.getBoundingClientRect(); return el.contains(document.elementFromPoint(b.x + b.width / 2, b.y + b.height / 2)); })).toBe(true); await expect(community).toBeVisible(); expect(await community.evaluate(element => element.scrollWidth <= element.clientWidth)).toBe(true); await page.screenshot({ path: test.info().outputPath('chat-narrow-navigation-row.png') }); await sidebarResizer.dblclick(); await expect(sidebarResizer).toHaveAttribute('aria-valuenow', '171'); await sidebar.getByRole('button', { name: '项目操作:故事绘本', exact: true }).click(); await page.getByRole('menuitem', { name: '项目设置', exact: true }).click(); await expect(page).toHaveURL(/\/project-config$/); await expect(page.getByTestId('project-configuration-page')).toBeVisible(); await expect(sidebar.getByTestId('sidebar-module-switcher')).toHaveCount(0); const projectBack = page.getByTestId('project-configuration-back-button'); await expect(projectBack).toBeVisible(); await page.screenshot({ path: test.info().outputPath('project-config-without-module-return.png') }); await projectBack.click(); await expect(page).toHaveURL(/\/chat$/); await expect(moduleReturn).toBeVisible(); await sidebar.getByRole('button', { name: '项目操作:故事绘本', exact: true }).click(); await page.getByRole('menuitem', { name: '项目设置', exact: true }).click(); await page.getByTestId('resource-card-plugins').click(); await expect(page).toHaveURL(/\/project-config\/plugins\?/); const plugins = page.getByTestId('project-plugins-sheet'); await expect(plugins).toBeVisible(); await expect(sidebar.getByTestId('sidebar-module-switcher')).toHaveCount(0); await page.screenshot({ path: test.info().outputPath('project-config-plugins-without-module-return.png') }); await plugins.getByRole('button', { name: '关闭', exact: true }).click(); await expect(page).toHaveURL(/\/project-config$/); await expect(projectBack).toBeVisible(); await sidebar.getByTestId('sidebar-course-project-project-storybook').click(); await expect(page).toHaveURL(/\/chat$/); const sidebarToggle = page.getByTestId('titlebar-sidebar-toggle'); await sidebarToggle.click(); await page.getByTestId('coding-chat-panel').click({ position: { x: 500, y: 200 } }); await expect(sidebar).toHaveAttribute('aria-hidden', 'true'); const collapsedToggleBox = await sidebarToggle.boundingBox(); await expect(moduleReturn).toBeVisible(); await sidebarToggle.hover(); await expect(sidebar).toHaveAttribute('aria-hidden', 'false'); expect(await sidebarToggle.boundingBox()).toEqual(collapsedToggleBox); await expect(moduleReturn).toBeVisible(); await page.screenshot({ path: test.info().outputPath('chat-collapsed-hover-code-return.png') }); await moduleReturn.click(); await expect(page).toHaveURL(/\/module-select$/); await expect(page.getByTestId('ai-module-selection-page')).toBeVisible(); } finally { await closeElectronApp(app); } }); test('aligns the desktop home title with the middle of three stacked horizontal module cards', async ({ launchElectronApp }) => { const app = await launchElectronApp({ skipSetup: true }); try { const page = await getStableWindow(app); await expect(page.getByTestId('ai-module-selection-page')).toBeVisible(); const moduleCards = page.locator('[data-testid^="ai-module-option-"]'); await expect(moduleCards.locator('.module-option-card-title')).toHaveText([ 'Agents 智能体', 'Code 编程', 'Canvas 设计', ]); await expect(page.getByTestId('ai-module-option-robot')).toHaveCount(0); await expect(moduleCards.first().locator('.module-option-card-description')).toHaveText('打造你想象中的AI助手'); await expect(moduleCards.locator('img')).toHaveCount(3); await expect.poll(async () => await moduleCards.locator('img').evaluateAll((images) => images.every((image) => image.complete && image.naturalWidth > 0), )).toBe(true); const readCardMetrics = async () => await moduleCards.evaluateAll((elements) => elements.map((element) => { const bounds = element.getBoundingClientRect(); const image = element.querySelector('.module-option-card-image')?.getBoundingClientRect(); const content = element.querySelector('.module-option-card-content')?.getBoundingClientRect(); return { left: bounds.left, top: bounds.top, width: bounds.width, height: bounds.height, imageWidth: image?.width ?? 0, imageHeight: image?.height ?? 0, imageLeft: image?.left ?? 0, contentLeft: content?.left ?? 0, }; })); await expect(moduleCards.first().locator('.module-option-card-arrow')).toHaveCount(0); await expect(moduleCards.first().locator('.module-option-artwork')).toHaveCount(0); await app.evaluate(({ BrowserWindow }) => { BrowserWindow.getAllWindows()[0]?.setMinimumSize(360, 600); }); for (const width of [1440, 1024, 768, 375]) { await app.evaluate(({ BrowserWindow }, windowWidth) => BrowserWindow.getAllWindows()[0]?.setSize(windowWidth, windowWidth === 375 ? 800 : 900), width); await expect.poll(() => page.evaluate(() => innerWidth)).toBe(width); const cards = await readCardMetrics(); expect(cards).toHaveLength(3); expect(Math.max(...cards.map(card => card.left)) - Math.min(...cards.map(card => card.left))).toBeLessThan(1); expect(Math.max(...cards.map(card => card.width)) - Math.min(...cards.map(card => card.width))).toBeLessThan(1); expect(Math.max(...cards.map(card => card.height)) - Math.min(...cards.map(card => card.height))).toBeLessThan(1); const gaps = cards.slice(1).map((card, index) => card.top - cards[index]!.top - cards[index]!.height); expect(Math.min(...gaps)).toBeGreaterThan(0); expect(Math.abs(gaps[0]! - gaps[1]!)).toBeLessThan(1); for (const card of cards) { expect(card.left).toBeGreaterThanOrEqual(0); expect(card.left + card.width).toBeLessThanOrEqual(width); expect(card.imageWidth).toBeLessThan(card.width / 2); expect(card.imageHeight).toBeGreaterThan(card.height * 0.75); expect(card.contentLeft).toBeGreaterThanOrEqual(card.imageLeft + card.imageWidth - 1); } const title = page.getByRole('heading', { name: '今天,想创作什么?', exact: true }); const heading = await title.boundingBox(); expect(heading).not.toBeNull(); const textWidth = await title.evaluate(element => { const range = document.createRange(); range.selectNodeContents(element); return range.getBoundingClientRect().width; }); expect(Math.abs(heading!.width - textWidth)).toBeLessThan(1); if (width >= 1024) { const shift = await title.evaluate(element => { const intro = element.parentElement!; const original = intro.style.transform; const shiftedLeft = element.getBoundingClientRect().left; try { intro.style.transform = 'none'; return shiftedLeft - element.getBoundingClientRect().left; } finally { intro.style.transform = original; } }); expect(shift).toBeCloseTo(1.5 * 96 / 2.54, 1); expect(heading!.x + heading!.width).toBeLessThanOrEqual(cards[0]!.left); expect(Math.abs(heading!.y + heading!.height / 2 - cards[1]!.top - cards[1]!.height / 2)).toBeLessThan(1); expect(await moduleCards.evaluateAll(elements => elements.every(element => { const bounds = element.getBoundingClientRect(); return [[2, 2], [bounds.width - 2, bounds.height - 2], [bounds.width / 2, bounds.height / 2]].every(([x, y]) => element.contains(document.elementFromPoint(bounds.left + x!, bounds.top + y!)), ); }))).toBe(true); } else { expect(heading!.y + heading!.height).toBeLessThanOrEqual(cards[0]!.top); } expect(await page.evaluate(() => document.documentElement.scrollWidth - innerWidth)).toBeLessThanOrEqual(1); expect(await moduleCards.locator('.module-option-card-title, .module-option-card-description').evaluateAll(elements => elements.every(element => element.scrollWidth <= element.clientWidth), )).toBe(true); await page.screenshot({ path: test.info().outputPath(`module-entry-${width}.png`) }); } await moduleCards.first().hover(); await moduleCards.first().focus(); await expect.poll(async () => await moduleCards.first().evaluate((element) => { const surface = element.querySelector('.module-option-card-surface'); const image = element.querySelector('.module-option-card-image'); const imageMedia = element.querySelector('.module-option-card-image-media'); const content = element.querySelector('.module-option-card-content'); const title = element.querySelector('.module-option-card-title'); return Boolean(surface) && Boolean(image) && Boolean(imageMedia) && Boolean(content) && Boolean(title) && getComputedStyle(surface).transform === 'none' && getComputedStyle(image).transform === 'none' && getComputedStyle(imageMedia).transform === 'none' && getComputedStyle(content).transform === 'none' && getComputedStyle(title).transform === 'none'; })).toBe(true); } finally { await closeElectronApp(app); } }); test('omits Learning from the module chooser and redirects its retired route', async ({ launchElectronApp }) => { const app = await launchElectronApp({ skipSetup: true }); try { const page = await getStableWindow(app); await expect(page.getByTestId('ai-module-selection-page')).toBeVisible(); await expect(page.locator('[data-testid^="ai-module-option-"]')).toHaveCount(3); await expect(page.getByTestId('ai-module-option-learning')).toHaveCount(0); await page.evaluate(() => { window.location.hash = '#/learning'; }); await expect(page).toHaveURL(/\/module-select$/); await expect(page.getByTestId('ai-module-selection-page')).toBeVisible(); } finally { await closeElectronApp(app); } }); test('returns to the module chooser from the sidebar entry', async ({ launchElectronApp }) => { const app = await launchElectronApp({ skipSetup: true }); try { const page = await getStableWindow(app); const minimumSize = await app.evaluate(({ BrowserWindow }) => BrowserWindow.getAllWindows()[0]?.getMinimumSize()); expect(minimumSize).toEqual([1024, 700]); await expect(page.getByTestId('ai-module-selection-page')).toBeVisible(); await page.getByTestId('ai-module-option-programming').click(); await expect(page.getByTestId('main-layout')).toBeVisible(); await expect(page).toHaveURL(/\/chat$/); await expect(page.getByText('请先新建项目')).toHaveCount(0); await expect(page.getByRole('heading', { name: '你想让麦洛和你一起构建什么?' })).toBeVisible(); await expect(page.getByRole('img', { name: '麦洛 M 标识' })).toBeVisible(); await expect(page.getByRole('button', { name: '新增项目' })).toBeVisible(); await expect(page.getByTestId('coding-project-starter')).toHaveCount(0); await expect(page.getByTestId('coding-existing-projects')).toHaveCount(0); await expect(page.getByText('一念成光,万物可创。')).toHaveCount(0); await expect(page.getByRole('img', { name: 'Makelore logo' })).toHaveCount(0); await expect(page.getByTestId('sidebar').locator('.classroom-brand')).toBeVisible(); await expect(page.getByTestId('sidebar-module-switcher-trigger')).toHaveText(''); await expect(page.getByTestId('sidebar-module-return-icon')).toBeVisible(); await expect(page.getByTestId('sidebar-create-project')).toBeVisible(); await expect(page.getByTestId('sidebar-nav-publish')).toHaveCount(0); await expect(page.getByTestId('sidebar-nav-project-config')).toHaveCount(0); await expect(page.getByTestId('sidebar-nav-agent-chat')).toHaveCount(0); await expect(page.getByRole('button', { name: '添加已有项目' })).toHaveCount(0); await expect(page.getByText('成长成就在这里')).toHaveCount(0); await expect(page.getByText('已创建 Phaser 2D 小游戏起步工程')).toHaveCount(0); await expect(page.getByTestId('game-asset-browser')).toHaveCount(0); await expect(page.getByTestId('game-asset-review-summary')).toHaveCount(0); await page.getByTestId('coding-start-project-button').click(); const createProjectDialog = page.getByRole('dialog', { name: '新建项目' }); await expect(createProjectDialog).toBeVisible(); await createProjectDialog.getByRole('button', { name: '取消' }).first().click(); await expect(page.getByRole('dialog', { name: '新建项目' })).toHaveCount(0); await expect(page.getByTestId('sidebar-module-switcher')).toBeVisible(); const moduleHomeTrigger = page.getByTestId('sidebar-module-switcher-trigger'); await expect(moduleHomeTrigger).toHaveAttribute('aria-label', '返回首页'); await expect(page.getByTestId('classroom-titlebar-navigation')).toBeVisible(); await expect(moduleHomeTrigger).toHaveCSS('width', '32px'); await expect(moduleHomeTrigger).toHaveCSS('height', '32px'); await expect(page.getByTestId('sidebar-module-switcher-menu')).toHaveCount(0); await moduleHomeTrigger.click(); await expect(page).toHaveURL(/\/module-select$/); await expect(page.getByTestId('ai-module-selection-page')).toBeVisible(); await expect(page.getByTestId('ai-module-option-robot')).toHaveCount(0); await page.evaluate(() => { window.location.hash = '#/ai-hardware'; }); await expect(page).toHaveURL(/\/ai-hardware$/); await expect(page.getByTestId('ai-hardware-page')).toBeVisible(); await expect(page.getByTestId('sidebar-robot-navigation')).toBeVisible(); await expect(page.getByTestId('sidebar-module-switcher-trigger')).toContainText('Robot 机器'); await page.getByTestId('sidebar-module-switcher-trigger').click(); await expect(page).toHaveURL(/\/module-select$/); await expect(page.getByTestId('ai-module-selection-page')).toBeVisible(); } finally { await closeElectronApp(app); } }); });