538 lines
32 KiB
TypeScript
538 lines
32 KiB
TypeScript
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();
|
||
const applicationUrl = page.url();
|
||
await page.goto('about:blank');
|
||
await app.evaluate(({ ipcMain, BrowserWindow }, summary) => {
|
||
BrowserWindow.getAllWindows()[0].setSize(1440, 900);
|
||
const state = { loggedIn: true, requests: [] as { method: string; path: string }[] };
|
||
Object.assign(globalThis, { homeAccountFixture: state });
|
||
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: state.loggedIn ? { accessToken: 'home-account-fixture', tokenType: 'Bearer', expiresAt: Date.now() + 3600000, lastActiveAt: Date.now(), canRefresh: false } : null });
|
||
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 (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.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-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.goto(applicationUrl);
|
||
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())!;
|
||
const panel = (await menu.boundingBox())!;
|
||
expect(visible.width).toBeCloseTo(128, 1);
|
||
expect(visible.height).toBeCloseTo(25, 1);
|
||
expect(avatar.width).toBeCloseTo(16, 1);
|
||
expect(avatar.height).toBeCloseTo(16, 1);
|
||
expect(target.width).toBeCloseTo(128, 1);
|
||
expect(target.height).toBeCloseTo(50, 1);
|
||
expect(visible.x).toBeGreaterThanOrEqual(16);
|
||
expect(visible.x).toBeCloseTo(width === 1440 ? 80 - 1.5 * 96 / 2.54 : 16, 1);
|
||
expect(await page.evaluate(() => innerHeight) - visible.y - visible.height).toBeCloseTo(16, 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);
|
||
};
|
||
await expect(trigger).toContainText('小麦');
|
||
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 page.evaluate(() => { window.location.hash = '#/module-select'; });
|
||
await expect(trigger).toContainText('小麦');
|
||
|
||
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(/\/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);
|
||
} 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 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.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<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' })).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);
|
||
}
|
||
});
|
||
});
|