diff --git a/apps/ai-game-creator-shell/src/view/project-development/chat/components/DirectProjectConversation/useConversationScroll.test.tsx b/apps/ai-game-creator-shell/src/view/project-development/chat/components/DirectProjectConversation/useConversationScroll.test.tsx index 8599be37f..620ec09f2 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/chat/components/DirectProjectConversation/useConversationScroll.test.tsx +++ b/apps/ai-game-creator-shell/src/view/project-development/chat/components/DirectProjectConversation/useConversationScroll.test.tsx @@ -356,37 +356,6 @@ describe('回到底部胶囊', () => { ); expect(capsule()).toBe('回到底部'); }); - - it('内容塌回一屏以内:没有滚动事件也收起胶囊', () => { - const metrics: ListMetrics = { - scrollTop: 0, - scrollHeight: 1200, - clientHeight: 400, - }; - stubListMetrics(metrics); - vi.stubGlobal('ResizeObserver', FakeResizeObserver); - const view = render( - undefined} - />, - ); - const capsule = () => view.getByTestId('capsule').textContent; - - metrics.scrollTop = 0; - fireEvent.scroll(view.getByTestId('list')); - expect(capsule()).toBe('回到底部'); - - // 回合收口把过程折进收起的 `
`:内容塌回一屏以内,列表不会再发任何滚动事件。 - // 只认 onScroll 里那次判定的 atBottom 会把胶囊永远挂在一个滚不动的列表上。 - metrics.scrollHeight = 400; - FakeResizeObserver.triggerLayoutChange(); - - expect(capsule()).toBe(''); - }); }); describe('加载行', () => { @@ -418,43 +387,6 @@ describe('加载行', () => { vi.useRealTimers(); } }); - - it('连续翻页之间的小空隙不卸载加载行:加载态不再忽隐忽现', () => { - vi.useFakeTimers(); - try { - stubListMetrics({ scrollTop: 0, scrollHeight: 1200, clientHeight: 400 }); - const props = { - turns: [] as DirectChatTurn[], - historyHasMore: true, - historyError: null, - onLoadEarlierHistory: () => undefined, - }; - const view = render(); - act(() => { - vi.advanceTimersByTime(150); - }); - expect(view.queryByTestId('loading')).not.toBeNull(); - - // 第一页落地:加载态转假,但填充视口的判据下一帧又会起一次,两次之间只隔一次 effect 回流。 - view.rerender(); - act(() => { - vi.advanceTimersByTime(50); - }); - view.rerender(); - - // 小于隐藏延迟的空隙不卸载:加载行不会在两次翻页之间消失重现。 - expect(view.queryByTestId('loading')).not.toBeNull(); - - // 真正结束(转假并超过隐藏延迟)才卸载。 - view.rerender(); - act(() => { - vi.advanceTimersByTime(301); - }); - expect(view.queryByTestId('loading')).toBeNull(); - } finally { - vi.useRealTimers(); - } - }); }); describe('点「回到底部」后的程序化滚动', () => { diff --git a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts index aee826828..fc37be1bc 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts @@ -5969,7 +5969,9 @@ export function registerProjectWorkbenchFoundationTests() { expect(composerRule?.[1]).not.toBeUndefined(); expect(composerRule?.[1]).toContain('position: relative;'); expect(composerRule?.[1]).toContain('gap: 8px;'); - expect(composerRule?.[1]).toContain('z-index: 1;'); + // 输入盒必须高于列表里的「回到底部」胶囊(胶囊 z-10),盒内向上展开的模型 / 附件菜单 + // 才能盖住它;大小关系由 chatDialogFrameLayout.test.ts 的层叠用例守卫,这里钉字面值。 + expect(composerRule?.[1]).toContain('z-index: 30;'); // 四边留白统一 16px、盒内内边距四边同为 12px(文字左内缩必须等于上内缩); // 不允许再出现 `8px 12px 10px` / `10px 12px` 这类「左右一个值、上下另一个值」的写法。 expect(composerRule?.[1]).toContain('margin: 16px;'); diff --git a/apps/ai-game-creator-shell/tests/chatDialogFrameLayout.test.ts b/apps/ai-game-creator-shell/tests/chatDialogFrameLayout.test.ts index 379024e85..abf57ad40 100644 --- a/apps/ai-game-creator-shell/tests/chatDialogFrameLayout.test.ts +++ b/apps/ai-game-creator-shell/tests/chatDialogFrameLayout.test.ts @@ -22,6 +22,10 @@ const INPUT_SOURCE_PATH = repoPath( const CHAT_COMPOSER_CONTROLS_PATH = repoPath( 'apps/ai-game-creator-shell/src/view/project-development/chat/components/DirectProjectComposer/ComposerControls.tsx', ); +/** 「回到底部」胶囊:z-index 写在 Tailwind 类上(`z-10`),styles.css 的层叠求值看不到。 */ +const CAPSULE_PATH = repoPath( + 'apps/ai-game-creator-shell/src/view/project-development/chat/components/DirectProjectConversation/DirectProjectScrollToBottomCapsule.tsx', +); function lengthPx(rawValue: string, label: string): number { const value = rawValue.trim(); @@ -329,10 +333,33 @@ describe('陶泥儿对话区:Codex 三段式(顶栏 / 唯一滚动区 / 文 ); expect(declaration(composer, 'border-radius')).toBe('14px'); - // 对话内容左右留白 16px,消息之间 14px。 + // 对话内容左右留白 16px。列表现在是 flex 列容器,`gap` 必须显式归 0:基础规则里那条 + // `gap: 14px` 在块级容器上一直没生效,改成 flex 后若不归零,会和子项的 `margin-top` + // 叠成双倍间距;子项间距仍由 `> * + *` 的 margin-top 提供。 expect(paddingBox(list).left).toBe(16); expect(paddingBox(list).right).toBe(16); - expect(declaration(list, 'gap')).toBe('14px'); + expect(declaration(list, 'gap')).toBe('0'); + }); + + it('「回到底部」胶囊不盖住输入盒弹层:胶囊层叠低于输入盒(宽屏)', () => { + // 胶囊是列表内的 sticky 浮标(Tailwind `z-10`),模型菜单(盒内 z-index: 20)与附件菜单 + // (盒内 z-index: 3)都从输入盒向上展开、会盖到列表底部。输入盒自己是 `position: relative` + // + z-index 的层叠上下文,盒内菜单的 z-index 只在盒内有效,所以要让**盒子**高于胶囊, + // 菜单才能盖住胶囊;否则重叠处命中的会是胶囊(真机 hit-test 实测:盒 z-index:1 时命中胶囊)。 + const capsuleClassName = + /className="([^"]*)"/u.exec(readFileSync(CAPSULE_PATH, 'utf8'))?.[1] ?? + ''; + const capsuleZIndex = Number(/\bz-(\d+)\b/u.exec(capsuleClassName)?.[1]); + const composer = desktopDeclarations(COMPOSER, COMPOSER_PLAIN); + const composerZIndex = Number(declaration(composer, 'z-index')); + expect(Number.isFinite(capsuleZIndex), '胶囊必须带 Tailwind z-* 类').toBe( + true, + ); + expect( + Number.isFinite(composerZIndex), + '输入盒必须声明 z-index 来建立层叠上下文', + ).toBe(true); + expect(composerZIndex).toBeGreaterThan(capsuleZIndex); }); it('消息列表底部不留给浮层的空白,滚动到底不会多出一段空白(宽屏)', () => {