273 lines
14 KiB
TypeScript
273 lines
14 KiB
TypeScript
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 three 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-"]');
|
|
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(3);
|
|
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<HTMLElement>('.module-option-card-surface');
|
|
const frame = element.querySelector<HTMLElement>('.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<HTMLElement>('.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(3);
|
|
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<HTMLElement>('.module-option-card-surface');
|
|
const image = element.querySelector<HTMLElement>('.module-option-card-image');
|
|
const imageMedia = element.querySelector<HTMLElement>('.module-option-card-image-media');
|
|
const content = element.querySelector<HTMLElement>('.module-option-card-content');
|
|
const title = element.querySelector<HTMLElement>('.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' })).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);
|
|
}
|
|
});
|
|
});
|