import { mkdir, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import { closeElectronApp, expect, getStableWindow, test } from './fixtures/electron'; 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('offers a create CTA and horizontal cards for remembered inactive Code projects', 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'); } finally { await closeElectronApp(app); } }); test('keeps the four module entries in a proportional vertical stack while resizing the window', 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 设计', 'Robot 机器', ]); await expect(moduleCards.first().locator('.module-option-card-description')).toHaveText('打造你想象中的AI助手'); await expect(moduleCards.locator('img')).toHaveCount(4); await expect.poll(async () => await moduleCards.locator('img').evaluateAll((images) => images.every((image) => image.complete && image.naturalWidth > 0), )).toBe(true); await page.screenshot({ path: test.info().outputPath('module-entry-artwork.png') }); const readCardMetrics = async () => await moduleCards.evaluateAll((elements) => elements.map((element) => { const bounds = element.getBoundingClientRect(); const image = element.querySelector('.module-option-card-image')?.getBoundingClientRect(); return { left: bounds.left, top: bounds.top, width: bounds.width, height: bounds.height, imageWidth: image?.width ?? 0, imageHeight: image?.height ?? 0, }; })); const initialMetrics = await readCardMetrics(); expect(initialMetrics).toHaveLength(4); await expect(moduleCards.first().locator('.module-option-card-arrow')).toHaveCount(0); await expect(moduleCards.first().locator('.module-option-artwork')).toHaveCount(0); await expect.poll(async () => await moduleCards.first().evaluate((element) => { const surface = element.querySelector('.module-option-card-surface'); const frame = element.querySelector('.module-option-card-frame'); if (!surface || !frame) return false; const surfaceStyle = getComputedStyle(surface); const frameStyle = getComputedStyle(frame); const surfaceInsets = [surfaceStyle.top, surfaceStyle.right, surfaceStyle.bottom, surfaceStyle.left] .map((value) => Number.parseFloat(value)); return frameStyle.display === 'none' && surfaceInsets.every((value) => value === 0) && surfaceStyle.transform === 'none'; })).toBe(true); const initialImageStyles = await moduleCards.first().evaluate((element) => { const image = element.querySelector('.module-option-card-image'); const cardHeight = element.getBoundingClientRect().height; if (!image) return null; const style = getComputedStyle(image); return { height: Number.parseFloat(style.height), cardHeight, marginTop: Number.parseFloat(style.marginTop), marginBottom: Number.parseFloat(style.marginBottom), marginLeft: Number.parseFloat(style.marginLeft), }; }); expect(initialImageStyles).not.toBeNull(); expect(initialImageStyles!.height).toBeLessThan(initialImageStyles!.cardHeight); expect(initialImageStyles!.marginTop).toBe(1); expect(initialImageStyles!.marginBottom).toBe(1); expect(initialImageStyles!.marginTop).toBe(initialImageStyles!.marginBottom); expect(initialImageStyles!.marginLeft).toBeGreaterThan(0); expect(initialImageStyles!.marginLeft).toBe(initialImageStyles!.marginTop); expect(Math.max(...initialMetrics.map((card) => card.left)) - Math.min(...initialMetrics.map((card) => card.left))).toBeLessThan(1); expect(initialMetrics.map((card) => card.top)).toEqual( [...initialMetrics].sort((top, bottom) => top.top - bottom.top).map((card) => card.top), ); const initialWidth = initialMetrics[0]?.width ?? 0; expect(initialMetrics[0]?.imageWidth ?? 0).toBeGreaterThan(initialWidth * 0.24); expect(initialMetrics[0]?.imageWidth ?? 0).toBeLessThan(initialWidth / 2); expect(initialMetrics[0]?.imageHeight ?? 0).toBeGreaterThan((initialMetrics[0]?.height ?? 0) * 0.75); await app.evaluate(({ BrowserWindow }) => { BrowserWindow.getAllWindows()[0]?.setSize(1100, 700); }); await expect.poll(async () => (await readCardMetrics())[0]?.width ?? 0).toBeLessThan(initialWidth - 1); const resizedMetrics = await readCardMetrics(); expect(resizedMetrics).toHaveLength(4); expect(Math.max(...resizedMetrics.map((card) => card.left)) - Math.min(...resizedMetrics.map((card) => card.left))).toBeLessThan(1); expect(resizedMetrics.map((card) => card.top)).toEqual( [...resizedMetrics].sort((top, bottom) => top.top - bottom.top).map((card) => card.top), ); expect(resizedMetrics[0]!.imageWidth).toBeGreaterThan(resizedMetrics[0]!.width * 0.24); expect(resizedMetrics[0]!.imageWidth).toBeLessThan(resizedMetrics[0]!.width / 2); expect(resizedMetrics[0]!.imageHeight).toBeGreaterThan(resizedMetrics[0]!.height * 0.75); 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(4); 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' })).toBeVisible(); await expect(page.getByTestId('sidebar-module-switcher-trigger')).toContainText('编程 Code'); 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(moduleHomeTrigger).toHaveCSS('padding-top', '8px'); await expect(moduleHomeTrigger).toHaveCSS('padding-right', '8px'); await expect(moduleHomeTrigger).toHaveCSS('padding-bottom', '8px'); await expect(moduleHomeTrigger).toHaveCSS('padding-left', '8px'); 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 page.getByTestId('ai-module-option-robot').click(); 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); } }); });