Files
Jyotisha/frontend/tests/session-conversation-layout.test.ts
T
Jesse_ChenandClaude Opus 5 62903b7a12
Independent Staging Quality Gate / validate (push) Failing after 9m13s
Independent Staging Quality Gate / publish (push) Skipped
feat(ui): 空状态改成「问候 + 输入框 + 两枚入口 pill」
此前空态是 hero 卡(h1 clamp 到 52px)加两张 min-height 228px 的产品卡,
输入框被压在约 800px 内容之下,主动作不是视觉焦点。

改法:问候拆成 StarterGreeting 留在对话区并底对齐,两个产品入口降级成
StarterEntries 渲染在 composer 之后。composer 本身一行未动——它在空态与
会话态是同一个 DOM 位置,首条消息落地时不重挂载,草稿/焦点/排队卡都不受影响。

顺带修掉:空态 composer 1040px、会话态 760px,发首条消息时输入框会当场
变窄一截。两态统一到 --session-column-width,并新增断言锁住。

校正入口保留三个服务端驱动的文案分支(我第一版收成了两个,是
rectification-agentic-entry 打红才发现)。

DESIGN.md:Product entrypoint card 整节重写为 Starter home;删掉
Suggestion card 死文档(它描述的主题卡样式正是本轮删除的 CSS)。

测试 3350 不变,fail 仍 31 且与基线双向零差异;/ 仍 Static;
CSS gzip 41,096→41,044(−0.13%)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
2026-09-16 05:31:15 +00:00

62 lines
4.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
const readProjectFile = (path: string) => readFileSync(new URL(`../${path}`, import.meta.url), "utf8");
const globalStyles = readProjectFile("src/app/globals.css");
const messageRowSource = readProjectFile("src/components/chat-message-row.tsx");
test("aligns the session transcript and composer to one readable column", () => {
assert.match(globalStyles, /--session-column-width:\s*760px/);
assert.match(globalStyles, /\.conversation:not\(\.is-empty\):not\(\.is-rectification\) \.message-list\s*\{[^}]*width:\s*min\(calc\(var\(--session-column-width\)/);
assert.match(globalStyles, /\.conversation:not\(\.is-empty\):not\(\.is-rectification\) \+ \.composer-wrap \.composer[^\{]*\{[^}]*width:\s*min\(var\(--session-column-width\),\s*100%\)/);
// 原值:`.composer-footer` 与 `.composer` 同宽 / 新值:只剩 `.composer`(上一条已断言)
// / 原因:常驻底栏已删,工具行移进输入框内部,不再有第二个需要对齐的盒子。
assert.doesNotMatch(globalStyles, /\.composer-footer\s*\{/);
});
test("keeps onboarding transcript and intake card on the same session column", () => {
const pageSource = readProjectFile("src/app/page.tsx");
assert.match(pageSource, /\$\{!profileComplete \? " is-onboarding" : ""\}/);
assert.match(globalStyles, /\.conversation\.is-onboarding \.welcome \{[\s\S]*width:\s*min\(calc\(var\(--session-column-width\) \+ \(var\(--session-column-gutter\) \* 2\)\), 100%\)/);
assert.match(globalStyles, /\.conversation\.is-onboarding\.is-empty \.welcome \{[\s\S]*width:\s*min\(var\(--session-column-width\), 100%\)/);
assert.match(globalStyles, /\.conversation\.is-onboarding \.onboarding-card,\s*\n\.conversation\.is-onboarding \.onboarding-card\.birth-time-transition-card \{[\s\S]*width:\s*100%;[\s\S]*max-width:\s*none/);
assert.match(globalStyles, /\.conversation\.is-onboarding-form \.welcome \{[\s\S]*padding:\s*var\(--space-6\) var\(--session-column-gutter\) var\(--space-8\)/);
});
test("centers the starter home even when the conversation grid is not is-empty", () => {
const pageSource = readProjectFile("src/app/page.tsx");
// 原值:空态 `.welcome` 与 `.starter-list` 都是 1040px
// 新值:`.welcome` 收到 `--session-column-width``.starter-list` 已删
// 原因:空态输入框此前 1040px、会话态 760px,首条消息落地时输入框会当场变窄一截。
// 现在两态同宽——这比原断言更强:它顺带锁住了「不跳宽度」。
assert.match(globalStyles, /\.welcome \{\s*width:\s*min\(var\(--session-column-width\), 100%\);\s*margin-inline:\s*auto;/);
assert.match(globalStyles, /\.composer-wrap-starter \.composer \{\s*width:\s*min\(var\(--session-column-width\), 100%\);/);
assert.doesNotMatch(globalStyles, /\.starter-list/);
assert.match(
pageSource,
/!onboardingFormActive && !activeSession\?\.messages\.length && \(Boolean\(activeSession\?\.messagesHydrated\) \|\| activeSession\?\.sessionType !== "consultation"\) \? " is-empty"/,
);
});
test("keeps message motion restrained and honors reduced-motion preferences", () => {
assert.match(messageRowSource, /gsap\.matchMedia\(\)/);
assert.match(messageRowSource, /prefers-reduced-motion:\s*no-preference/);
// Former value: `duration:\s*0\.18`. DESIGN.md §6 gives the message entrance 160ms; 0.18 was
// the GSAP copy drifting from the (now removed) 160ms CSS keyframe that ran alongside it.
assert.match(messageRowSource, /duration:\s*0\.16/);
assert.match(messageRowSource, /ease:\s*"cubic-bezier\(\.22,\s*1,\s*\.36,\s*1\)"/);
assert.match(messageRowSource, /clearProps:\s*"opacity,transform,visibility"/);
});
test("lets model controls fit their labels and keeps the stop action on-brand", () => {
assert.match(globalStyles, /\.model-selector-trigger\s*\{[^}]*width:\s*fit-content/);
assert.match(globalStyles, /\.model-selector-popup\s*\{[^}]*width:\s*max-content[^}]*min-width:\s*180px[^}]*max-width:\s*min\(420px,/);
assert.match(globalStyles, /\.model-selector-copy b\s*\{[^}]*overflow:\s*visible[^}]*white-space:\s*normal/);
assert.doesNotMatch(globalStyles, /\.model-selector-copy b\s*\{[^}]*text-overflow:\s*ellipsis/);
// 原值 `var(--color-action)` / 新值 `var(--color-action-strong)` / 原因:停止键是填充面,
// 走品牌 coral 那一阶;文字阶留给 --color-action。见 BUG-738。
assert.match(globalStyles, /\.composer \.composer-stop\s*\{[^}]*background:\s*var\(--color-action-strong\)/);
assert.match(globalStyles, /\.composer \.composer-stop:not\(:disabled\):hover\s*\{[^}]*background:\s*var\(--color-action-hover\)/);
});