fix(home): hide Robot entry and balance three module cards
This commit is contained in:
1 parent
23ddce11da
commit
13336cfe12
6 files changed
+167
-81
No files matched your search
@@ -0,0 +1,67 @@
|
|||||||
|
# Task: Hide Robot home entry and arrange three modules
|
||||||
|
|
||||||
|
## Identity
|
||||||
|
|
||||||
|
- Task ID: 20261013-hide-home-robot-6c1f8a3d
|
||||||
|
- Mode: Feature
|
||||||
|
- Branch: codex/20261013-hide-home-robot-6c1f8a3d-hide-home-robot
|
||||||
|
- Worktree: /Users/chillishark/Makelore 麦洛/.codex-worktrees/makelore/20261013-hide-home-robot-6c1f8a3d
|
||||||
|
- Base commit: 23ddce11da5c4c7aa31af81213a5aafd474cdad0
|
||||||
|
- Owner: codex
|
||||||
|
- Status: Ready for Integration
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Hide Robot only from the home module selection. Arrange Agents, Code and Canvas as three equal desktop cards and single-column compact cards, preserving existing artwork and navigation contracts.
|
||||||
|
|
||||||
|
## Intent And Constraints
|
||||||
|
|
||||||
|
- User explicitly requests Robot home-entry hiding and remaining-entry re-layout. Prior direction still defers temporary-app updates. Preserve global Robot definition, routes, permissions and capabilities; do not restart existing apps, publish, push, merge main or adopt peer changes.
|
||||||
|
|
||||||
|
## Outcome
|
||||||
|
|
||||||
|
- Robot is filtered only in home selection; global module definitions, Robot routes/APIs and policy access are unchanged. Agents, Code and Canvas share an equal-width desktop row beneath the centered welcome heading, with proportional card heights; below 768px they use a single vertical column of horizontal image/text cards. Existing artwork, click hit areas, disabled states and hover/focus behavior are preserved. README and navigation regressions match.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- Module navigation unit tests: 9/9 passed, including hidden Robot with enabled permission, Canvas/Code routing, disabled policy, login/profile gates and Robot sidebar identity.
|
||||||
|
- Pinned pnpm 10.33.4: typecheck, scoped ESLint and production build:vite passed. Build repeated after visual review reduced extra card whitespace at 768px.
|
||||||
|
- Targeted Electron navigation fixtures: 3/3 passed (layout, retired Learning route, sidebar return plus direct Robot route); layout fixture rechecked after final CSS refinement, 1/1 passed.
|
||||||
|
- Layout geometry and screenshots checked at 1440, 1024, 768 and 375px: equal columns/gaps, compact stack, loaded artwork, text/page overflow checks, whole-card click area and stable hover/focus.
|
||||||
|
- git diff --check and task-aware project document drift check passed. Test Electron instances use independent temporary profiles; existing temporary/installed applications are unchanged.
|
||||||
|
- Dependencies reused through an ignored node_modules symlink. No runtime data, dependencies, build/test output or secrets included in commits.
|
||||||
|
|
||||||
|
## Follow-ups
|
||||||
|
|
||||||
|
- Integrate this source commit into the code chosen for the next temporary-app update. No current temporary-app update/restart, main merge, push or publication was performed.
|
||||||
|
|
||||||
|
## Promotion Candidates
|
||||||
|
|
||||||
|
- Target: integrated module-selection/current-state documentation.
|
||||||
|
- Proposal: home offers Agents, Code and Canvas only, with an equal desktop row and compact vertical stack. Robot remains a supported guarded route/capability whose home card is hidden.
|
||||||
|
- Evidence: explicit screenshot request, focused source diff, 9 unit tests, 3 Electron cases and four-size visual/geometry review.
|
||||||
|
- Future impact: later home UI work should preserve three visible entries unless explicitly changed. This supersedes historical four-card home presentation only; module policies and Robot capability remain unchanged. Human confirmation is already supplied; canonical promotion remains serialized integration work.
|
||||||
|
|
||||||
|
## Project Context Loaded
|
||||||
|
|
||||||
|
- Task ID: 20261013-hide-home-robot-6c1f8a3d; feature mode.
|
||||||
|
- Branch: codex/20261013-hide-home-robot-6c1f8a3d-hide-home-robot.
|
||||||
|
- Worktree: /Users/chillishark/Makelore 麦洛/.codex-worktrees/makelore/20261013-hide-home-robot-6c1f8a3d.
|
||||||
|
- Base: 23ddce11da5c4c7aa31af81213a5aafd474cdad0 (latest locally fetched origin/main). start exited 0; status JSON confirms identity, branch, root, mode and reservation.
|
||||||
|
- Read: AGENTS; entry/read-before-planning, memory-index and planning-gate; this task; positioning, current-state, decision-index/system-overview; business-rules and success-criteria; related accepted navigation/permission context. Same-task read-only subagent reviewed required context and every peer Scope/Intent/Promotion.
|
||||||
|
- Project goal: student/creator desktop creation with Main-owned authentication/service access and a single light visual system.
|
||||||
|
- Current integrated focus: combined personal Agent UI and runtime release, separate homepage module display.
|
||||||
|
- Active scope/constraints: only hide one home choice and balance the other three; keep underlying Robot access and module-policy denial semantics.
|
||||||
|
- Other active tasks: Canvas session diagnosis and knowledge workspace; retained teacher/docs tasks, including a legacy child-language task with undefined scope. No concrete homepage dependency or semantic conflict. All other worktrees remain read-only.
|
||||||
|
- Decisions: older four-card vertical home layout is superseded only by this user's explicit hide/re-layout instruction. Robot remains a supported guarded module; missing/denied permissions keep existing handling.
|
||||||
|
- Evidence/commitments: existing module-navigation unit tests and main-navigation Electron fixtures available; no external or timed obligation.
|
||||||
|
- Modules: ModuleSelection/index.tsx, scoped module card CSS in globals.css, navigation tests and README.
|
||||||
|
- Unknown/stale context: older AGENTS/module counts and integrated memory include historical three/four-module summaries; feature mode records promotion candidates instead of rewriting accepted docs.
|
||||||
|
- Gate result: Passed.
|
||||||
|
|
||||||
|
## Plan
|
||||||
|
|
||||||
|
- Filter Robot only in home rendering; center the greeting above a three-column desktop grid and use horizontal single-column cards below 768px.
|
||||||
|
- Preserve artwork, hover/focus stability, policy-disabled states, login/profile gates and navigation destinations.
|
||||||
|
- Adapt focused unit/Electron regressions and check actual layout at wide/narrow windows, typecheck, lint and production build.
|
||||||
|
- Update README and this task record; commit a tested source handoff without changing current app runtime.
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
Code 会话保存的模型被下架后,会提示重新选择可用模型;用户可在原会话中切换,恢复时使用新选择并保留会话与历史消息。切换不会自动重发消息或自动选择替代模型。
|
Code 会话保存的模型被下架后,会提示重新选择可用模型;用户可在原会话中切换,恢复时使用新选择并保留会话与历史消息。切换不会自动重发消息或自动选择替代模型。
|
||||||
|
|
||||||
Makelore 是一个面向软件、视觉创作、智能机器人与个人云智能体的 AI 桌面工作台。当前版本为 `2.0.0`,源码提供四个模块入口;云智能体需配套配置 WS/Yuxi 服务。模块入口页按 Agents、Code、Canvas、Robot 排列,统一采用横向插画卡片;Agents 标语为“打造你想象中的AI助手”。工作区左上角入口点击后返回模块入口页:
|
Makelore 是一个面向软件、视觉创作、智能机器人与个人云智能体的 AI 桌面工作台。当前版本为 `2.0.0`,首页展示 Agents、Code、Canvas 三个模块入口,Robot 入口隐藏;云智能体需配套配置 WS/Yuxi 服务。三个入口在桌面等宽并排、插画在上文字在下,小窗口改为纵向排列的横向插画卡片;Agents 标语为“打造你想象中的AI助手”。工作区左上角入口点击后返回模块入口页:
|
||||||
|
|
||||||
- `Makelore Code|AI 编程`:管理本地项目、项目智能体、对话、文件上下文、代码变更和运行时。
|
- `Makelore Code|AI 编程`:管理本地项目、项目智能体、对话、文件上下文、代码变更和运行时。
|
||||||
会话工作台采用浅蓝项目栏、彩色通用手柄图标与卡片选中态;中间对话/作品使用圆角标签,作品预览的后退、前进、刷新与网址输入统一放在标签右侧;只在作品页显示,窄窗口自动换行。右侧智能体咨询使用奶油色标题区。沿用真实项目和会话入口、侧栏伸缩与账户菜单,样式只作用于 Code 会话工作台。可编辑的空对话在输入区上方的可用区域中央显示童趣欢迎语;「去作品广场逛逛」入口紧贴输入框上方,两者一起位于底部。老师介绍以固定宽度浮在右上角,不占用中间布局;欢迎语和底部输入区始终相对项目栏右侧的整个内容区域居中;小窗口的欢迎内容独立滚动;发送首条消息后收起欢迎区。侧栏保留「作品广场」入口,只有点击后才在系统浏览器打开公共广场,不请求、摘取或内嵌展示广场作品。打开广场保留当前项目、对话、草稿和附件,不自动发送消息;老师求助按钮保留临时应用的最新文案「帮我整体看🧠」。
|
会话工作台采用浅蓝项目栏、彩色通用手柄图标与卡片选中态;中间对话/作品使用圆角标签,作品预览的后退、前进、刷新与网址输入统一放在标签右侧;只在作品页显示,窄窗口自动换行。右侧智能体咨询使用奶油色标题区。沿用真实项目和会话入口、侧栏伸缩与账户菜单,样式只作用于 Code 会话工作台。可编辑的空对话在输入区上方的可用区域中央显示童趣欢迎语;「去作品广场逛逛」入口紧贴输入框上方,两者一起位于底部。老师介绍以固定宽度浮在右上角,不占用中间布局;欢迎语和底部输入区始终相对项目栏右侧的整个内容区域居中;小窗口的欢迎内容独立滚动;发送首条消息后收起欢迎区。侧栏保留「作品广场」入口,只有点击后才在系统浏览器打开公共广场,不请求、摘取或内嵌展示广场作品。打开广场保留当前项目、对话、草稿和附件,不自动发送消息;老师求助按钮保留临时应用的最新文案「帮我整体看🧠」。
|
||||||
|
|||||||
@@ -57,8 +57,8 @@ export function ModuleSelection({ authRequired = true }: { authRequired?: boolea
|
|||||||
<img src={logoWordmarkSource} alt="Makelore" className="h-8 w-40 object-cover object-center" draggable="false" />
|
<img src={logoWordmarkSource} alt="Makelore" className="h-8 w-40 object-cover object-center" draggable="false" />
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="grid flex-1 items-center gap-8 py-12 lg:grid-cols-[minmax(0,1fr)_minmax(0,1.35fr)] lg:gap-12">
|
<div className="flex flex-1 flex-col justify-center gap-8 py-12 sm:gap-10">
|
||||||
<section className="min-w-0">
|
<section className="min-w-0 text-center">
|
||||||
<p className="text-sm font-semibold tracking-[0.08em] text-muted-foreground">
|
<p className="text-sm font-semibold tracking-[0.08em] text-muted-foreground">
|
||||||
{userDisplayName ? `${userDisplayName},欢迎回来!` : '欢迎回来!'}
|
{userDisplayName ? `${userDisplayName},欢迎回来!` : '欢迎回来!'}
|
||||||
</p>
|
</p>
|
||||||
@@ -70,7 +70,7 @@ export function ModuleSelection({ authRequired = true }: { authRequired?: boolea
|
|||||||
aria-label="AI 模块列表"
|
aria-label="AI 模块列表"
|
||||||
className="module-selection-options"
|
className="module-selection-options"
|
||||||
>
|
>
|
||||||
{aiModules.map((module) => {
|
{aiModules.filter((module) => module.id !== 'robot').map((module) => {
|
||||||
const content = moduleSelectionContent[module.id];
|
const content = moduleSelectionContent[module.id];
|
||||||
const enabledByPolicy = isAiModuleAllowed(module.id, moduleAccess);
|
const enabledByPolicy = isAiModuleAllowed(module.id, moduleAccess);
|
||||||
const enabled = module.enabled && enabledByPolicy;
|
const enabled = module.enabled && enabledByPolicy;
|
||||||
|
|||||||
+35
-2
@@ -423,7 +423,8 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr);
|
grid-template-columns: minmax(0, 1fr);
|
||||||
gap: var(--module-option-gap);
|
gap: var(--module-option-gap);
|
||||||
width: min(100%, 43rem);
|
width: min(100%, 69rem);
|
||||||
|
margin-inline: auto;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
justify-self: center;
|
justify-self: center;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
@@ -497,7 +498,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: clamp(0.75rem, 1.6vw, 1.25rem);
|
padding: clamp(0.75rem, 1.6vw, 1.25rem);
|
||||||
padding-right: clamp(2.75rem, 5vw, 4rem);
|
padding-right: clamp(0.75rem, 1.6vw, 1.25rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-option-card-title {
|
.module-option-card-title {
|
||||||
@@ -517,6 +518,38 @@
|
|||||||
line-height: 1.25;
|
line-height: 1.25;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.module-selection-options {
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-selection-options .module-option-card {
|
||||||
|
height: auto;
|
||||||
|
min-height: clamp(17.5rem, 24vw, 21rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-selection-options .module-option-card-surface {
|
||||||
|
position: relative;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-selection-options .module-option-card-image {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: calc(100% - 0.125rem);
|
||||||
|
height: auto;
|
||||||
|
aspect-ratio: 16 / 10;
|
||||||
|
margin: 0.0625rem 0.0625rem 0;
|
||||||
|
border-radius: 0.75rem 0.75rem 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-selection-options .module-option-card-content {
|
||||||
|
justify-content: flex-start;
|
||||||
|
padding: 1.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.module-option-card.module-option-card-disabled::before,
|
.module-option-card.module-option-card-disabled::before,
|
||||||
.module-option-card.module-option-card-disabled::after {
|
.module-option-card.module-option-card-disabled::after {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ test.describe('Makelore module navigation without setup flow', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('keeps the four module entries in a proportional vertical stack while resizing the window', async ({ launchElectronApp }) => {
|
test('lays out three module cards in a desktop row and a compact mobile stack', async ({ launchElectronApp }) => {
|
||||||
const app = await launchElectronApp({ skipSetup: true });
|
const app = await launchElectronApp({ skipSetup: true });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -209,17 +209,18 @@ test.describe('Makelore module navigation without setup flow', () => {
|
|||||||
|
|
||||||
const moduleCards = page.locator('[data-testid^="ai-module-option-"]');
|
const moduleCards = page.locator('[data-testid^="ai-module-option-"]');
|
||||||
await expect(moduleCards.locator('.module-option-card-title')).toHaveText([
|
await expect(moduleCards.locator('.module-option-card-title')).toHaveText([
|
||||||
'Agents 智能体', 'Code 编程', 'Canvas 设计', 'Robot 机器',
|
'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.first().locator('.module-option-card-description')).toHaveText('打造你想象中的AI助手');
|
||||||
await expect(moduleCards.locator('img')).toHaveCount(4);
|
await expect(moduleCards.locator('img')).toHaveCount(3);
|
||||||
await expect.poll(async () => await moduleCards.locator('img').evaluateAll((images) =>
|
await expect.poll(async () => await moduleCards.locator('img').evaluateAll((images) =>
|
||||||
images.every((image) => image.complete && image.naturalWidth > 0),
|
images.every((image) => image.complete && image.naturalWidth > 0),
|
||||||
)).toBe(true);
|
)).toBe(true);
|
||||||
await page.screenshot({ path: test.info().outputPath('module-entry-artwork.png') });
|
|
||||||
const readCardMetrics = async () => await moduleCards.evaluateAll((elements) => elements.map((element) => {
|
const readCardMetrics = async () => await moduleCards.evaluateAll((elements) => elements.map((element) => {
|
||||||
const bounds = element.getBoundingClientRect();
|
const bounds = element.getBoundingClientRect();
|
||||||
const image = element.querySelector('.module-option-card-image')?.getBoundingClientRect();
|
const image = element.querySelector('.module-option-card-image')?.getBoundingClientRect();
|
||||||
|
const content = element.querySelector('.module-option-card-content')?.getBoundingClientRect();
|
||||||
return {
|
return {
|
||||||
left: bounds.left,
|
left: bounds.left,
|
||||||
top: bounds.top,
|
top: bounds.top,
|
||||||
@@ -227,70 +228,59 @@ test.describe('Makelore module navigation without setup flow', () => {
|
|||||||
height: bounds.height,
|
height: bounds.height,
|
||||||
imageWidth: image?.width ?? 0,
|
imageWidth: image?.width ?? 0,
|
||||||
imageHeight: image?.height ?? 0,
|
imageHeight: image?.height ?? 0,
|
||||||
|
imageLeft: image?.left ?? 0,
|
||||||
|
imageTop: image?.top ?? 0,
|
||||||
|
contentLeft: content?.left ?? 0,
|
||||||
|
contentTop: content?.top ?? 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-card-arrow')).toHaveCount(0);
|
||||||
await expect(moduleCards.first().locator('.module-option-artwork')).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 }) => {
|
await app.evaluate(({ BrowserWindow }) => {
|
||||||
BrowserWindow.getAllWindows()[0]?.setSize(1100, 700);
|
BrowserWindow.getAllWindows()[0]?.setMinimumSize(360, 600);
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect.poll(async () => (await readCardMetrics())[0]?.width ?? 0).toBeLessThan(initialWidth - 1);
|
for (const width of [1440, 1024, 768]) {
|
||||||
|
await app.evaluate(({ BrowserWindow }, windowWidth) => BrowserWindow.getAllWindows()[0]?.setSize(windowWidth, 900), width);
|
||||||
const resizedMetrics = await readCardMetrics();
|
await expect.poll(() => page.evaluate(() => innerWidth)).toBe(width);
|
||||||
expect(resizedMetrics).toHaveLength(4);
|
const cards = await readCardMetrics();
|
||||||
expect(Math.max(...resizedMetrics.map((card) => card.left)) - Math.min(...resizedMetrics.map((card) => card.left))).toBeLessThan(1);
|
expect(cards).toHaveLength(3);
|
||||||
expect(resizedMetrics.map((card) => card.top)).toEqual(
|
expect(Math.max(...cards.map(card => card.top)) - Math.min(...cards.map(card => card.top))).toBeLessThan(1);
|
||||||
[...resizedMetrics].sort((top, bottom) => top.top - bottom.top).map((card) => card.top),
|
expect(Math.max(...cards.map(card => card.width)) - Math.min(...cards.map(card => card.width))).toBeLessThan(1);
|
||||||
|
const gaps = cards.slice(1).map((card, index) => card.left - cards[index]!.left - cards[index]!.width);
|
||||||
|
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.contentTop).toBeGreaterThanOrEqual(card.imageTop + card.imageHeight - 1);
|
||||||
|
}
|
||||||
|
await expect(page.getByRole('heading', { name: '今天,想创作什么?' })).toHaveCSS('text-align', 'center');
|
||||||
|
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!)),
|
||||||
);
|
);
|
||||||
expect(resizedMetrics[0]!.imageWidth).toBeGreaterThan(resizedMetrics[0]!.width * 0.24);
|
}))).toBe(true);
|
||||||
expect(resizedMetrics[0]!.imageWidth).toBeLessThan(resizedMetrics[0]!.width / 2);
|
await page.screenshot({ path: test.info().outputPath(`module-entry-${width}.png`) });
|
||||||
expect(resizedMetrics[0]!.imageHeight).toBeGreaterThan(resizedMetrics[0]!.height * 0.75);
|
}
|
||||||
|
|
||||||
|
await app.evaluate(({ BrowserWindow }) => BrowserWindow.getAllWindows()[0]?.setSize(375, 800));
|
||||||
|
await expect.poll(() => page.evaluate(() => innerWidth)).toBe(375);
|
||||||
|
const compactCards = await readCardMetrics();
|
||||||
|
expect(Math.max(...compactCards.map(card => card.left)) - Math.min(...compactCards.map(card => card.left))).toBeLessThan(1);
|
||||||
|
for (const [index, card] of compactCards.entries()) {
|
||||||
|
if (index > 0) expect(card.top).toBeGreaterThan(compactCards[index - 1]!.top + compactCards[index - 1]!.height);
|
||||||
|
expect(card.imageWidth).toBeLessThan(card.width / 2);
|
||||||
|
expect(card.imageHeight).toBeGreaterThan(card.height * 0.75);
|
||||||
|
expect(card.contentLeft).toBeGreaterThanOrEqual(card.imageLeft + card.imageWidth - 1);
|
||||||
|
}
|
||||||
|
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-375.png') });
|
||||||
|
|
||||||
await moduleCards.first().hover();
|
await moduleCards.first().hover();
|
||||||
await moduleCards.first().focus();
|
await moduleCards.first().focus();
|
||||||
@@ -323,7 +313,7 @@ test.describe('Makelore module navigation without setup flow', () => {
|
|||||||
const page = await getStableWindow(app);
|
const page = await getStableWindow(app);
|
||||||
await expect(page.getByTestId('ai-module-selection-page')).toBeVisible();
|
await expect(page.getByTestId('ai-module-selection-page')).toBeVisible();
|
||||||
|
|
||||||
await expect(page.locator('[data-testid^="ai-module-option-"]')).toHaveCount(4);
|
await expect(page.locator('[data-testid^="ai-module-option-"]')).toHaveCount(3);
|
||||||
await expect(page.getByTestId('ai-module-option-learning')).toHaveCount(0);
|
await expect(page.getByTestId('ai-module-option-learning')).toHaveCount(0);
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
window.location.hash = '#/learning';
|
window.location.hash = '#/learning';
|
||||||
@@ -385,7 +375,8 @@ test.describe('Makelore module navigation without setup flow', () => {
|
|||||||
await moduleHomeTrigger.click();
|
await moduleHomeTrigger.click();
|
||||||
await expect(page).toHaveURL(/\/module-select$/);
|
await expect(page).toHaveURL(/\/module-select$/);
|
||||||
await expect(page.getByTestId('ai-module-selection-page')).toBeVisible();
|
await expect(page.getByTestId('ai-module-selection-page')).toBeVisible();
|
||||||
await page.getByTestId('ai-module-option-robot').click();
|
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).toHaveURL(/\/ai-hardware$/);
|
||||||
await expect(page.getByTestId('ai-hardware-page')).toBeVisible();
|
await expect(page.getByTestId('ai-hardware-page')).toBeVisible();
|
||||||
await expect(page.getByTestId('sidebar-robot-navigation')).toBeVisible();
|
await expect(page.getByTestId('sidebar-robot-navigation')).toBeVisible();
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ describe('AI module navigation', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows the four supported module choices and routes Canvas from the chooser', async () => {
|
it('shows Agents, Code and Canvas on the home page and routes Canvas from the chooser', async () => {
|
||||||
render(
|
render(
|
||||||
<MemoryRouter initialEntries={['/module-select']}>
|
<MemoryRouter initialEntries={['/module-select']}>
|
||||||
<Routes>
|
<Routes>
|
||||||
@@ -64,18 +64,16 @@ describe('AI module navigation', () => {
|
|||||||
|
|
||||||
expect(screen.getByTestId('ai-module-selection-page')).toBeInTheDocument();
|
expect(screen.getByTestId('ai-module-selection-page')).toBeInTheDocument();
|
||||||
expect(screen.getByRole('heading', { name: '今天,想创作什么?' })).toBeInTheDocument();
|
expect(screen.getByRole('heading', { name: '今天,想创作什么?' })).toBeInTheDocument();
|
||||||
expect(screen.getByRole('heading', { name: '今天,想创作什么?' })).toHaveClass('whitespace-nowrap');
|
|
||||||
expect(screen.getByTestId('ai-module-options')).toHaveClass('module-selection-options');
|
expect(screen.getByTestId('ai-module-options')).toHaveClass('module-selection-options');
|
||||||
expect(await screen.findByText('小明,欢迎回来!')).toBeInTheDocument();
|
expect(await screen.findByText('小明,欢迎回来!')).toBeInTheDocument();
|
||||||
expect(screen.getByTestId('ai-module-option-programming')).toHaveClass('module-option-card', 'module-option-card-horizontal');
|
expect(screen.getByTestId('ai-module-option-programming')).toHaveClass('module-option-card');
|
||||||
for (const moduleId of ['programming', 'painting', 'robot', 'cloud_agents'] as const) {
|
for (const moduleId of ['programming', 'painting', 'cloud_agents'] as const) {
|
||||||
expect(screen.getByTestId(`ai-module-option-${moduleId}`).querySelector('.module-option-artwork')).toBeNull();
|
expect(screen.getByTestId(`ai-module-option-${moduleId}`).querySelector('.module-option-artwork')).toBeNull();
|
||||||
}
|
}
|
||||||
expect(screen.getAllByTestId(/^ai-module-option-/u).map((option) => option.dataset.testid)).toEqual([
|
expect(screen.getAllByTestId(/^ai-module-option-/u).map((option) => option.dataset.testid)).toEqual([
|
||||||
'ai-module-option-cloud_agents',
|
'ai-module-option-cloud_agents',
|
||||||
'ai-module-option-programming',
|
'ai-module-option-programming',
|
||||||
'ai-module-option-painting',
|
'ai-module-option-painting',
|
||||||
'ai-module-option-robot',
|
|
||||||
]);
|
]);
|
||||||
expect(screen.queryByTestId('ai-module-option-learning')).not.toBeInTheDocument();
|
expect(screen.queryByTestId('ai-module-option-learning')).not.toBeInTheDocument();
|
||||||
expect(screen.queryByText('先选一个入口。进入后,你还可以从左下角随时切换到其他模块。')).not.toBeInTheDocument();
|
expect(screen.queryByText('先选一个入口。进入后,你还可以从左下角随时切换到其他模块。')).not.toBeInTheDocument();
|
||||||
@@ -89,12 +87,7 @@ describe('AI module navigation', () => {
|
|||||||
expect(screen.getByText('Canvas 设计')).toBeInTheDocument();
|
expect(screen.getByText('Canvas 设计')).toBeInTheDocument();
|
||||||
expect(screen.getByAltText('数位板设计创作')).toBeInTheDocument();
|
expect(screen.getByAltText('数位板设计创作')).toBeInTheDocument();
|
||||||
expect(screen.getByTestId('ai-module-option-painting')).toHaveTextContent('用灵感描绘色彩缤纷的世界');
|
expect(screen.getByTestId('ai-module-option-painting')).toHaveTextContent('用灵感描绘色彩缤纷的世界');
|
||||||
const robotOption = screen.getByTestId('ai-module-option-robot');
|
expect(screen.queryByTestId('ai-module-option-robot')).not.toBeInTheDocument();
|
||||||
expect(robotOption).toBeEnabled();
|
|
||||||
expect(robotOption).not.toHaveClass('module-option-card-disabled');
|
|
||||||
expect(robotOption).toHaveTextContent('Robot 机器');
|
|
||||||
expect(robotOption).toHaveTextContent('制作你的第一个机器人小伙伴');
|
|
||||||
expect(screen.getByAltText('青少年管理机器人硬件与设备绑定')).toBeInTheDocument();
|
|
||||||
expect(screen.getByAltText('个人云智能体')).toHaveAttribute('src', expect.stringContaining('module-agents.webp'));
|
expect(screen.getByAltText('个人云智能体')).toHaveAttribute('src', expect.stringContaining('module-agents.webp'));
|
||||||
expect(screen.getByAltText('个人云智能体')).toHaveClass('module-option-card-image-media', 'object-cover');
|
expect(screen.getByAltText('个人云智能体')).toHaveClass('module-option-card-image-media', 'object-cover');
|
||||||
|
|
||||||
@@ -102,7 +95,8 @@ describe('AI module navigation', () => {
|
|||||||
expect(await screen.findByTestId('location-path')).toHaveTextContent('/image-canvas');
|
expect(await screen.findByTestId('location-path')).toHaveTextContent('/image-canvas');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('routes Robot from the chooser to the AI hardware workspace', async () => {
|
it('hides Robot from the chooser even when its module permission is enabled', () => {
|
||||||
|
useAuthStore.setState({ moduleAccess: { programming: true, design: true, robot: true, cloud_agents: true } });
|
||||||
render(
|
render(
|
||||||
<MemoryRouter initialEntries={['/module-select']}>
|
<MemoryRouter initialEntries={['/module-select']}>
|
||||||
<Routes>
|
<Routes>
|
||||||
@@ -112,8 +106,9 @@ describe('AI module navigation', () => {
|
|||||||
</MemoryRouter>,
|
</MemoryRouter>,
|
||||||
);
|
);
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId('ai-module-option-robot'));
|
expect(screen.queryByTestId('ai-module-option-robot')).not.toBeInTheDocument();
|
||||||
expect(await screen.findByTestId('location-path')).toHaveTextContent('/ai-hardware');
|
expect(screen.queryByText('Robot 机器')).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByAltText('青少年管理机器人硬件与设备绑定')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('greys out a disabled module and does not navigate when it is clicked', async () => {
|
it('greys out a disabled module and does not navigate when it is clicked', async () => {
|
||||||
|
|||||||
Reference in new issue
Block a user