Merge branch 'main' of https://git.nianxx.cn/wangxuming/makelore
This commit is contained in:
@@ -24,14 +24,49 @@ test.describe('Makelore module navigation without setup flow', () => {
|
||||
|
||||
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<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.9);
|
||||
expect(initialMetrics[0]?.imageHeight ?? 0).toBeGreaterThan((initialMetrics[0]?.height ?? 0) * 0.75);
|
||||
|
||||
await app.evaluate(({ BrowserWindow }) => {
|
||||
BrowserWindow.getAllWindows()[0]?.setSize(1100, 700);
|
||||
@@ -45,14 +80,76 @@ test.describe('Makelore module navigation without setup flow', () => {
|
||||
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.9);
|
||||
expect(resizedMetrics[0]!.imageHeight).toBeGreaterThan(resizedMetrics[0]!.height * 0.75);
|
||||
|
||||
await page.mouse.move(20, 20);
|
||||
const restingTransforms = 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 {
|
||||
surfaceShadow: surface ? getComputedStyle(surface).boxShadow : 'none',
|
||||
image: image ? getComputedStyle(image).transform : 'none',
|
||||
imageMedia: imageMedia ? getComputedStyle(imageMedia).transform : 'none',
|
||||
content: content ? getComputedStyle(content).transform : 'none',
|
||||
title: title ? getComputedStyle(title).transform : 'none',
|
||||
};
|
||||
});
|
||||
|
||||
await moduleCards.first().hover();
|
||||
await moduleCards.first().focus();
|
||||
await expect.poll(async () => await moduleCards.first().evaluate((element, rest) => {
|
||||
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');
|
||||
const description = element.querySelector<HTMLElement>('.module-option-card-description');
|
||||
const getTranslateZ = (transform: string) => {
|
||||
if (!transform.startsWith('matrix3d(')) return 0;
|
||||
const values = transform.slice(9, -1).split(',').map(Number);
|
||||
return values[14] ?? 0;
|
||||
};
|
||||
const titleTransform = title ? getComputedStyle(title).transform : 'none';
|
||||
const descriptionTransform = description ? getComputedStyle(description).transform : 'none';
|
||||
return Boolean(surface)
|
||||
&& Boolean(image)
|
||||
&& Boolean(imageMedia)
|
||||
&& Boolean(content)
|
||||
&& Boolean(title)
|
||||
&& Boolean(description)
|
||||
&& getComputedStyle(surface).transform !== 'none'
|
||||
&& getComputedStyle(surface).boxShadow !== rest.surfaceShadow
|
||||
&& getComputedStyle(surface).borderTopColor === 'rgb(255, 255, 255)'
|
||||
&& getComputedStyle(image).transform !== 'none'
|
||||
&& getComputedStyle(imageMedia).transform !== 'none'
|
||||
&& getComputedStyle(content).transform !== 'none'
|
||||
&& titleTransform !== 'none'
|
||||
&& descriptionTransform !== 'none'
|
||||
&& getTranslateZ(titleTransform) > getTranslateZ(descriptionTransform) + 20;
|
||||
}, restingTransforms)).toBe(true);
|
||||
|
||||
await expect.poll(async () => await moduleCards.first().evaluate((element, rest) => {
|
||||
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(image && imageMedia && content && title)
|
||||
&& getComputedStyle(image!).transform !== rest.image
|
||||
&& getComputedStyle(imageMedia!).transform !== rest.imageMedia
|
||||
&& getComputedStyle(content!).transform !== rest.content
|
||||
&& getComputedStyle(title!).transform !== rest.title;
|
||||
}, restingTransforms)).toBe(true);
|
||||
} finally {
|
||||
await closeElectronApp(app);
|
||||
}
|
||||
});
|
||||
|
||||
test('opens the module chooser and switches between enabled modules', async ({ launchElectronApp }) => {
|
||||
test('returns to the module chooser from the sidebar entry', async ({ launchElectronApp }) => {
|
||||
const app = await launchElectronApp({ skipSetup: true });
|
||||
|
||||
try {
|
||||
@@ -79,33 +176,24 @@ test.describe('Makelore module navigation without setup flow', () => {
|
||||
await expect(page.getByTestId('game-asset-review-summary')).toHaveCount(0);
|
||||
|
||||
await expect(page.getByTestId('sidebar-module-switcher')).toBeVisible();
|
||||
await expect(page.getByTestId('sidebar-module-switcher-trigger')).toHaveAttribute('aria-expanded', 'false');
|
||||
await page.getByTestId('sidebar-module-switcher-trigger').click();
|
||||
await expect(page.getByTestId('sidebar-module-programming')).toHaveAttribute('aria-current', 'page');
|
||||
await expect(page.getByTestId('sidebar-module-learning')).toBeDisabled();
|
||||
await expect(page.getByTestId('sidebar-module-robot')).toContainText('Robot 机器');
|
||||
await page.getByTestId('sidebar-module-painting').click();
|
||||
await expect(page.getByTestId('image-canvas-page')).toBeVisible();
|
||||
await expect(page.getByTestId('image-workspace-unavailable')).toBeVisible();
|
||||
await expect(page.getByText('AI 设计暂不可用').first()).toBeVisible();
|
||||
await expect(page.getByTestId('sidebar-image-workspace')).toBeVisible();
|
||||
await expect(page.getByTestId('sidebar-create-image-project')).toBeVisible();
|
||||
await expect(page.getByTestId('sidebar-image-projects')).toBeVisible();
|
||||
await expect(page.getByText('本地开发')).toHaveCount(0);
|
||||
await expect(page.getByTestId('sidebar-reset-local-image-workspace')).toHaveCount(0);
|
||||
await expect(page.getByTestId('sidebar-create-project')).toHaveCount(0);
|
||||
await expect(page.getByTestId('sidebar-module-painting')).toHaveAttribute('aria-current', 'page');
|
||||
await page.getByTestId('sidebar-module-switcher-trigger').click();
|
||||
await page.getByTestId('sidebar-module-robot').click();
|
||||
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-create-project')).toHaveCount(0);
|
||||
await expect(page.getByTestId('sidebar-module-robot')).toHaveAttribute('aria-current', 'page');
|
||||
await expect(page.getByTestId('sidebar-module-switcher-trigger')).toContainText('Robot 机器');
|
||||
await page.getByTestId('sidebar-module-switcher-trigger').click();
|
||||
await page.getByTestId('sidebar-module-programming').click();
|
||||
await expect(page.getByText('请先新建项目')).toHaveCount(0);
|
||||
await expect(page.getByText('一念成光,万物可创。')).toBeVisible();
|
||||
await expect(page.getByTestId('chat-operation-page')).toHaveCount(0);
|
||||
await expect(page).toHaveURL(/\/module-select$/);
|
||||
await expect(page.getByTestId('ai-module-selection-page')).toBeVisible();
|
||||
} finally {
|
||||
await closeElectronApp(app);
|
||||
}
|
||||
|
||||
@@ -398,6 +398,15 @@ async function installSlashCommandHost(
|
||||
shareEnabled: true,
|
||||
});
|
||||
}
|
||||
if (path === '/api/opencode/skills') {
|
||||
return respond({
|
||||
skills: [
|
||||
{ name: 'frontend-slides' },
|
||||
{ name: 'grilling' },
|
||||
{ name: 'planning-with-files' },
|
||||
],
|
||||
});
|
||||
}
|
||||
if (
|
||||
path
|
||||
=== '/api/opencode/sessions/ses_slash_e2e/summarize'
|
||||
@@ -571,8 +580,6 @@ test.describe('OpenCode slash commands', () => {
|
||||
await completeSetup(page);
|
||||
await installSlashCommandHost(electronApp);
|
||||
await page.reload();
|
||||
await page.getByTestId('sidebar-module-switcher-trigger').click();
|
||||
await page.getByTestId('sidebar-module-programming').click();
|
||||
await expect(page).toHaveURL(/\/opencode-chat$/);
|
||||
await page.getByTestId('project-agent-chat-game-development').click();
|
||||
const composer = page.getByRole('textbox');
|
||||
@@ -759,8 +766,6 @@ test.describe('OpenCode slash commands', () => {
|
||||
await installSlashCommandHost(electronApp);
|
||||
await page.reload();
|
||||
await page.setViewportSize({ width: 1280, height: 800 });
|
||||
await page.getByTestId('sidebar-module-switcher-trigger').click();
|
||||
await page.getByTestId('sidebar-module-programming').click();
|
||||
await expect(page).toHaveURL(/\/opencode-chat$/);
|
||||
await expect(page.getByTestId('opencode-message-composer')).toBeVisible();
|
||||
|
||||
@@ -771,6 +776,11 @@ test.describe('OpenCode slash commands', () => {
|
||||
const createPartnerDialog = page.getByRole('dialog', { name: '创建项目伙伴' });
|
||||
await expect(createPartnerDialog).toBeVisible();
|
||||
await expect(createPartnerDialog).toHaveCSS('z-index', '110');
|
||||
await expect(createPartnerDialog.getByLabel('绑定技能:项目演示')).not.toBeChecked();
|
||||
await expect(createPartnerDialog.getByLabel('绑定技能:方案质询')).toBeChecked();
|
||||
await expect(createPartnerDialog.getByLabel('绑定技能:项目规划')).toBeChecked();
|
||||
await createPartnerDialog.getByLabel('绑定技能:方案质询').uncheck();
|
||||
await expect(createPartnerDialog.getByLabel('绑定技能:方案质询')).not.toBeChecked();
|
||||
await createPartnerDialog.getByLabel(/伙伴名称/).fill('E2E层级检查');
|
||||
await page.getByRole('button', { name: '取消' }).click();
|
||||
await expect(createPartnerButton).toHaveClass(/text-brand/);
|
||||
|
||||
@@ -57,7 +57,25 @@ test.describe('Project configuration skills', () => {
|
||||
await expect(drawer.getByRole('combobox')).toHaveCount(0);
|
||||
await expect(drawer.locator('[data-testid="project-model-list"], [data-testid="project-model-empty-state"]')).toHaveCount(1);
|
||||
}
|
||||
await page.getByRole('button', { name: '关闭' }).click();
|
||||
const closeButton = drawer.getByRole('button', { name: '关闭' });
|
||||
const closeGeometry = await closeButton.evaluate((button) => {
|
||||
const icon = button.querySelector('svg');
|
||||
if (!icon) return null;
|
||||
const buttonRect = button.getBoundingClientRect();
|
||||
const iconRect = icon.getBoundingClientRect();
|
||||
return {
|
||||
button: { x: buttonRect.x, y: buttonRect.y, width: buttonRect.width, height: buttonRect.height },
|
||||
icon: { x: iconRect.x, y: iconRect.y, width: iconRect.width, height: iconRect.height },
|
||||
};
|
||||
});
|
||||
if (!closeGeometry) {
|
||||
throw new Error(`未能读取 ${title} 抽屉关闭按钮的布局区域`);
|
||||
}
|
||||
expect(closeGeometry.button.width).toBe(40);
|
||||
expect(closeGeometry.button.height).toBe(40);
|
||||
expect(Math.abs((closeGeometry.icon.x + closeGeometry.icon.width / 2) - (closeGeometry.button.x + closeGeometry.button.width / 2))).toBeLessThanOrEqual(0.5);
|
||||
expect(Math.abs((closeGeometry.icon.y + closeGeometry.icon.height / 2) - (closeGeometry.button.y + closeGeometry.button.height / 2))).toBeLessThanOrEqual(0.5);
|
||||
await closeButton.locator('svg').click();
|
||||
await expect(page.getByRole('dialog', { name: title })).toHaveCount(0);
|
||||
}
|
||||
|
||||
@@ -94,7 +112,7 @@ test.describe('Project configuration skills', () => {
|
||||
await expect(page.getByTestId('skill-detail-view')).toBeVisible();
|
||||
await expect(page.getByTestId('skill-structure')).toContainText('references/guide.md');
|
||||
await expect(page.getByTestId('skill-main-file')).toContainText('# Browser debugging');
|
||||
await page.getByRole('button', { name: '返回技能列表' }).click();
|
||||
await page.getByRole('button', { name: '返回技能列表' }).locator('svg').click();
|
||||
await expect(page.getByTestId('skill-card-agent-browser')).toBeVisible();
|
||||
await page.getByRole('button', { name: '关闭' }).click();
|
||||
await expect(page.getByTestId('resource-card-publish')).toHaveCount(0);
|
||||
|
||||
@@ -344,6 +344,40 @@ describe('AgentConversationSidebar', () => {
|
||||
})));
|
||||
});
|
||||
|
||||
it('defaults only the planning Skills and leaves presentation optional', async () => {
|
||||
hostApiFetchMock.mockResolvedValue({
|
||||
skills: [
|
||||
{ name: 'agent-browser' },
|
||||
{ name: 'frontend-slides' },
|
||||
{ name: 'grilling' },
|
||||
{ name: 'planning-with-files' },
|
||||
],
|
||||
});
|
||||
const callbacks = renderSidebar();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '创建伙伴' }));
|
||||
|
||||
const grilling = await screen.findByLabelText('绑定技能:方案质询');
|
||||
const planning = await screen.findByLabelText('绑定技能:项目规划');
|
||||
const slides = await screen.findByLabelText('绑定技能:项目演示');
|
||||
const browser = await screen.findByLabelText('绑定技能:开发浏览器');
|
||||
await waitFor(() => {
|
||||
expect(grilling).toBeChecked();
|
||||
expect(planning).toBeChecked();
|
||||
expect(slides).not.toBeChecked();
|
||||
expect(browser).not.toBeChecked();
|
||||
});
|
||||
fireEvent.click(grilling);
|
||||
expect(grilling).not.toBeChecked();
|
||||
fireEvent.change(screen.getByLabelText(/伙伴名称/), { target: { value: '小李' } });
|
||||
fireEvent.change(screen.getByLabelText(/职责简述/), { target: { value: '负责整理资料。' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '创建并开始对话' }));
|
||||
|
||||
await waitFor(() => expect(callbacks.onCreateAgent).toHaveBeenCalledWith(expect.objectContaining({
|
||||
skillIds: ['planning-with-files'],
|
||||
})));
|
||||
});
|
||||
|
||||
it('shows advanced creation settings directly below the basics', () => {
|
||||
renderSidebar();
|
||||
fireEvent.click(screen.getByRole('button', { name: '创建伙伴' }));
|
||||
|
||||
@@ -27,7 +27,7 @@ describe('AI module navigation', () => {
|
||||
useOpencodeStore.setState({ activeProject: null });
|
||||
});
|
||||
|
||||
it('shows the four module choices and routes enabled modules from the chooser', async () => {
|
||||
it('shows the four module choices and routes Canvas from the chooser', async () => {
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/module-select']}>
|
||||
<Routes>
|
||||
@@ -43,6 +43,9 @@ describe('AI module navigation', () => {
|
||||
expect(screen.getByTestId('ai-module-options')).toHaveClass('module-selection-options');
|
||||
expect(screen.getByText('zhangsan,欢迎回来!')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('ai-module-option-programming')).toHaveClass('module-option-card', 'module-option-card-horizontal');
|
||||
for (const moduleId of ['programming', 'painting', 'learning', 'robot'] as const) {
|
||||
expect(screen.getByTestId(`ai-module-option-${moduleId}`).querySelector('.module-option-artwork')).toBeNull();
|
||||
}
|
||||
expect(screen.queryByText('先选一个入口。进入后,你还可以从左下角随时切换到其他模块。')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('选择创作空间。你可以随时从侧边栏切换。')).not.toBeInTheDocument();
|
||||
expect(screen.getByAltText('Makelore')).toHaveClass('h-8', 'w-40', 'object-cover');
|
||||
@@ -55,20 +58,35 @@ describe('AI module navigation', () => {
|
||||
expect(screen.getByAltText('数位板设计创作')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('ai-module-option-painting')).toHaveTextContent('用灵感描绘色彩缤纷的世界');
|
||||
const learningOption = screen.getByTestId('ai-module-option-learning');
|
||||
expect(learningOption).toBeDisabled();
|
||||
expect(learningOption).toHaveClass('module-option-card-disabled');
|
||||
expect(learningOption).toBeEnabled();
|
||||
expect(learningOption).not.toHaveClass('module-option-card-disabled');
|
||||
expect(learningOption).toHaveTextContent('Learning 学习');
|
||||
expect(learningOption).toHaveTextContent('用顶尖的方法解锁万物规律');
|
||||
expect(learningOption).toHaveTextContent('暂未开放');
|
||||
expect(screen.getByAltText('数学科技宇宙')).toBeInTheDocument();
|
||||
const robotOption = screen.getByTestId('ai-module-option-robot');
|
||||
expect(robotOption).toBeEnabled();
|
||||
expect(robotOption).not.toHaveClass('module-option-card-disabled');
|
||||
expect(robotOption).toHaveTextContent('Robot 机器');
|
||||
expect(robotOption).toHaveTextContent('制作你的第一个机器人小伙伴');
|
||||
expect(robotOption).not.toHaveTextContent('暂未开放');
|
||||
expect(screen.getByAltText('青少年管理机器人硬件与设备绑定')).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(robotOption);
|
||||
fireEvent.click(learningOption);
|
||||
expect(screen.getByTestId('ai-module-selection-page')).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByTestId('ai-module-option-painting'));
|
||||
expect(await screen.findByTestId('location-path')).toHaveTextContent('/image-canvas');
|
||||
});
|
||||
|
||||
it('routes Robot from the chooser to the AI hardware workspace', async () => {
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/module-select']}>
|
||||
<Routes>
|
||||
<Route path="/module-select" element={<ModuleSelection />} />
|
||||
<Route path="*" element={<LocationProbe />} />
|
||||
</Routes>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByTestId('ai-module-option-robot'));
|
||||
expect(await screen.findByTestId('location-path')).toHaveTextContent('/ai-hardware');
|
||||
});
|
||||
|
||||
@@ -112,7 +130,7 @@ describe('AI module navigation', () => {
|
||||
expect(await screen.findByTestId('location-path')).toHaveTextContent('/login');
|
||||
});
|
||||
|
||||
it('keeps the current module active and switches from the Sidebar entry', async () => {
|
||||
it('returns to the module chooser from the Sidebar entry', async () => {
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/image-canvas']}>
|
||||
<Routes>
|
||||
@@ -121,29 +139,16 @@ describe('AI module navigation', () => {
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('sidebar-module-switcher-trigger')).toHaveAttribute('aria-expanded', 'false');
|
||||
expect(screen.getByTestId('sidebar-module-switcher-trigger')).toHaveTextContent('绘画 Canvas');
|
||||
expect(screen.getByTestId('sidebar-module-switcher-menu')).toHaveAttribute('data-state', 'closed');
|
||||
expect(screen.getByTestId('sidebar-module-painting')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('sidebar-module-switcher-trigger')).toHaveAttribute('aria-label', '返回首页');
|
||||
expect(screen.getByTestId('sidebar-module-switcher-trigger')).toHaveClass('p-3');
|
||||
expect(screen.queryByTestId('sidebar-module-switcher-menu')).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByTestId('sidebar-module-switcher-trigger'));
|
||||
expect(screen.getByTestId('sidebar-module-switcher-menu')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('sidebar-module-painting')).toHaveAttribute('aria-current', 'page');
|
||||
expect(screen.getByTestId('sidebar-module-programming')).not.toHaveAttribute('aria-current');
|
||||
expect(screen.getByTestId('sidebar-module-robot')).not.toHaveAttribute('aria-current');
|
||||
const learningModule = screen.getByTestId('sidebar-module-learning');
|
||||
expect(learningModule).toBeDisabled();
|
||||
expect(learningModule).toHaveTextContent('学习 Learning');
|
||||
expect(learningModule).toHaveTextContent('AI 学习');
|
||||
expect(learningModule).not.toHaveAttribute('aria-current');
|
||||
fireEvent.click(learningModule);
|
||||
expect(screen.getByTestId('location-path')).toHaveTextContent('/image-canvas');
|
||||
|
||||
fireEvent.click(screen.getByTestId('sidebar-module-programming'));
|
||||
expect(await screen.findByTestId('location-path')).toHaveTextContent('/opencode-chat');
|
||||
expect(await screen.findByTestId('location-path')).toHaveTextContent('/module-select');
|
||||
});
|
||||
|
||||
it('marks Robot active on the AI hardware route and switches back to Canvas', async () => {
|
||||
it('shows Robot as the active module and returns to the chooser from the AI hardware route', async () => {
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/ai-hardware']}>
|
||||
<Routes>
|
||||
@@ -154,11 +159,7 @@ describe('AI module navigation', () => {
|
||||
|
||||
expect(screen.getByTestId('sidebar-module-switcher-trigger')).toHaveTextContent('Robot 机器');
|
||||
fireEvent.click(screen.getByTestId('sidebar-module-switcher-trigger'));
|
||||
expect(screen.getByTestId('sidebar-module-robot')).toHaveAttribute('aria-current', 'page');
|
||||
expect(screen.getByTestId('sidebar-module-programming')).not.toHaveAttribute('aria-current');
|
||||
|
||||
fireEvent.click(screen.getByTestId('sidebar-module-painting'));
|
||||
expect(await screen.findByTestId('location-path')).toHaveTextContent('/image-canvas');
|
||||
expect(await screen.findByTestId('location-path')).toHaveTextContent('/module-select');
|
||||
});
|
||||
|
||||
it('keeps breathing room around the compact module switcher trigger', () => {
|
||||
@@ -169,7 +170,7 @@ describe('AI module navigation', () => {
|
||||
);
|
||||
|
||||
const trigger = screen.getByTestId('sidebar-module-switcher-trigger');
|
||||
expect(trigger).toHaveClass('inline-flex', 'w-fit', 'px-2');
|
||||
expect(trigger).toHaveClass('inline-flex', 'w-fit', 'p-2');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -183,6 +183,22 @@ describe('project-owned contact configuration', () => {
|
||||
expect(markdown).toContain('"*": deny');
|
||||
});
|
||||
|
||||
it('grants selected presentation and planning Skills to the materialized OpenCode Agent', async () => {
|
||||
const projectPath = await mkdtemp(path.join(tmpdir(), 'makelore-project-config-planning-skills-'));
|
||||
const initial = await createInitialProjectConfig(projectPath);
|
||||
await writeProjectConfig(projectPath, {
|
||||
...initial,
|
||||
initialized: true,
|
||||
agents: [createContact({ skillIds: ['frontend-slides', 'grilling', 'planning-with-files'] })],
|
||||
});
|
||||
|
||||
const markdown = await readFile(path.join(projectPath, '.opencode', 'agent', 'agent-test.md'), 'utf8');
|
||||
expect(markdown).toContain(' "*": deny\n frontend-slides: allow\n grilling: allow\n planning-with-files: allow');
|
||||
expect(markdown).toContain('主动调用 `frontend-slides`');
|
||||
expect(markdown).toContain('主动调用 `grilling`');
|
||||
expect(markdown).toContain('主动调用 `planning-with-files`');
|
||||
});
|
||||
|
||||
it('denies unselected skills when a contact has selected skills', async () => {
|
||||
const projectPath = await mkdtemp(path.join(tmpdir(), 'makelore-project-config-selected-skills-'));
|
||||
const initial = await createInitialProjectConfig(projectPath);
|
||||
|
||||
@@ -150,8 +150,11 @@ describe('ProjectConfiguration', () => {
|
||||
] as const;
|
||||
for (const [resourceId, title] of resourceDrawers) {
|
||||
fireEvent.click(screen.getByTestId(resourceId));
|
||||
expect(await screen.findByRole('dialog', { name: title })).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: '关闭' }));
|
||||
const drawer = await screen.findByRole('dialog', { name: title });
|
||||
expect(drawer).toBeInTheDocument();
|
||||
const closeButton = within(drawer).getByRole('button', { name: '关闭' });
|
||||
expect(closeButton).toHaveClass('h-10', 'w-10', 'shrink-0');
|
||||
fireEvent.click(closeButton);
|
||||
await waitFor(() => expect(screen.queryByRole('dialog', { name: title })).not.toBeInTheDocument());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -321,24 +321,14 @@ describe('Sidebar project initialization flow', () => {
|
||||
expect(screen.queryByText('课程路径')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('伙伴广场')).not.toBeInTheDocument();
|
||||
const moduleTrigger = screen.getByTestId('sidebar-module-switcher-trigger');
|
||||
expect(moduleTrigger).toHaveAttribute('aria-expanded', 'false');
|
||||
expect(moduleTrigger).toHaveAttribute('aria-label', '返回首页');
|
||||
expect(moduleTrigger).toHaveClass('bg-transparent');
|
||||
expect(moduleTrigger).toHaveClass('w-fit', 'justify-start', 'gap-1');
|
||||
expect(moduleTrigger).toHaveClass('w-fit', 'justify-start', 'p-2');
|
||||
expect(moduleTrigger).not.toHaveClass('bg-brand-soft');
|
||||
expect(moduleTrigger).toHaveTextContent('编程 Code');
|
||||
expect(screen.getByTestId('sidebar-module-switcher-menu')).toHaveAttribute('data-state', 'closed');
|
||||
expect(screen.getByTestId('sidebar-module-programming')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('sidebar-module-switcher-menu')).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByTestId('sidebar-module-switcher-trigger'));
|
||||
const programmingModule = screen.getByTestId('sidebar-module-programming');
|
||||
expect(programmingModule).toHaveTextContent('编程 Code');
|
||||
expect(programmingModule).toHaveClass('bg-brand-soft');
|
||||
expect(programmingModule.querySelector('svg')).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('sidebar-module-painting')).toHaveTextContent('绘画 Canvas');
|
||||
expect(screen.getByTestId('sidebar-module-painting')).not.toHaveClass('bg-brand-soft');
|
||||
expect(screen.getByTestId('sidebar-module-programming')).not.toHaveTextContent('AI');
|
||||
expect(screen.getByTestId('sidebar-module-painting')).not.toHaveTextContent('AI');
|
||||
expect(screen.getByTestId('sidebar-module-programming')).not.toHaveTextContent('Makelore');
|
||||
expect(screen.getByTestId('sidebar-module-painting')).not.toHaveTextContent('Makelore');
|
||||
expect(navigateMock).toHaveBeenCalledWith('/module-select');
|
||||
expect(screen.queryByTestId('sidebar-nav-image-canvas')).not.toBeInTheDocument();
|
||||
expect(await screen.findByText(/暂无真实项目/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -7,6 +7,18 @@ describe('skill display metadata', () => {
|
||||
name: '开发浏览器',
|
||||
description: '打开、查看或调试本地及公网网页,读取 Console、Network、DOM 和样式信息。',
|
||||
});
|
||||
expect(getSkillDisplayInfo('frontend-slides')).toEqual({
|
||||
name: '项目演示',
|
||||
description: '把项目内容整理成可播放的 16:9 HTML 幻灯片,不生成 PPTX 或云端发布。',
|
||||
});
|
||||
expect(getSkillDisplayInfo('grilling')).toEqual({
|
||||
name: '方案质询',
|
||||
description: '在重要工作开始前逐项澄清目标、范围和关键取舍,确认后再执行。',
|
||||
});
|
||||
expect(getSkillDisplayInfo('planning-with-files')).toEqual({
|
||||
name: '项目规划',
|
||||
description: '为复杂任务维护可恢复的计划、发现和进展,记录在项目的 .niancode/agent-planning/ 中。',
|
||||
});
|
||||
});
|
||||
|
||||
it('does not expose an unknown external runtime id', () => {
|
||||
|
||||
@@ -2,7 +2,12 @@ import { describe, expect, it } from 'vitest';
|
||||
import { BUNDLED_COURSE_SKILL_IDS } from '@electron/opencode/course-skills';
|
||||
|
||||
describe('bundled Skill allowlist', () => {
|
||||
it('keeps only the explicitly retained Agent Browser Skill', () => {
|
||||
expect([...BUNDLED_COURSE_SKILL_IDS]).toEqual(['agent-browser']);
|
||||
it('keeps only the explicitly retained project Skills', () => {
|
||||
expect([...BUNDLED_COURSE_SKILL_IDS]).toEqual([
|
||||
'agent-browser',
|
||||
'frontend-slides',
|
||||
'grilling',
|
||||
'planning-with-files',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user