Files
Jyotisha/frontend/tests/onboarding-presentation.test.ts
T
Jesse_ChenandClaude Fable 5.1 ff0427cf08
Independent Staging Quality Gate / validate (push) Failing after 6m23s
Independent Staging Quality Gate / publish (push) Skipped
feat(home): 首页开场语改成一行问候,占位符改「想聊什么都可以」
开场语从「称呼行 + 追问句」收成 claude.ai 式的单行问候:时段池、
星期池、回访池按顺序拼成一个数组,按 variantSelection 确定性取一条;
名字为空时跳过带称呼的条目。副标题行与 .starter-salutation 下线,
h1 降到 clamp(24px, 2.8vw, 30px) / line-height 1.25。追问改由输入框
占位符承担。

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JUei7K13cYxLHE3Axe4A45
2026-09-17 15:54:20 +00:00

179 lines
9.3 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 test from "node:test";
import { readFileSync } from "node:fs";
import {
createOnboardingFallbackGreeting,
createStartGreeting,
createStartGreetingParts,
} from "../src/lib/starter-greeting.ts";
import { homeSurface } from "./home-surface.ts";
// 2026-07-19 是周日,20 周一,22 周三(星期池为空),24 周五,25 周六。
const SUNDAY = (hour: number) => new Date(2026, 6, 19, hour);
const MONDAY = (hour: number) => new Date(2026, 6, 20, hour);
const WEDNESDAY = (hour: number) => new Date(2026, 6, 22, hour);
const FRIDAY = (hour: number) => new Date(2026, 6, 24, hour);
test("terminal and fallback greetings follow the current profile name", () => {
// 原值: 同时锁 onboardingRequestIdentity / isCurrentOnboardingRequest,避免建议问题串号。
// 新值: 只保留问候语随当前称呼变化。
// 原因: 建议问题请求已删除,身份去重不再有消费点。
//
// 原值 `createStartGreeting("周宁", …, 0)` + `"周宁,从你此刻最关心的问题开始吧。"`
// / 新值 `createStartGreeting("周宁", …, 0.25)` + `"欢迎,周宁"`
// / 原因:时段池现在第一条是不带称呼的「早上好」,要取带称呼的条目得换下标;
// 兜底问候按产品口径改成单行「欢迎,{名字}」,不再追问。断言强度没变:
// 两条都仍然必须带当前称呼、不得带旧称呼。
const terminalGreeting = createStartGreeting("周宁", SUNDAY(8), 0.25);
const fallbackGreeting = createOnboardingFallbackGreeting("周宁");
for (const greeting of [terminalGreeting, fallbackGreeting]) {
assert.match(greeting, /周宁/);
assert.doesNotMatch(greeting, /林遥/);
}
assert.equal(terminalGreeting, "早上好,周宁");
assert.equal(fallbackGreeting, "欢迎,周宁");
});
test("the starter greeting is one line drawn from daypart, weekday and returning pools", () => {
// 原值:`parts.salutation` = "中午好,周宁。"、`parts.question` = "现在最想理清哪件事?",
// 并断言两半拼起来等于整行、每个时段至少 12 条不同问句。
// 新值:`parts.text` 就是整行问候;断言时段池按下标取到「中午好」/「中午好,周宁」。
// 原因:产品把开场语改成 claude.ai 式的单行问候,追问句移到输入框占位符,
// 「招呼语 + 问句」两半这个形状本身被取消,问句池不再存在。
const noon = WEDNESDAY(12);
assert.equal(createStartGreetingParts("周宁", noon, 0).text, "中午好");
assert.equal(createStartGreetingParts("周宁", noon, 0.5).text, "中午好,周宁");
assert.equal(createStartGreeting("周宁", noon, 0.5), createStartGreetingParts("周宁", noon, 0.5).text);
// 边界不越界:1 与 0.999999 都落在池内最后一条。
assert.equal(createStartGreetingParts("周宁", noon, 1).text, "中午好,周宁");
assert.equal(createStartGreetingParts("周宁", noon, 0.999999).text, "中午好,周宁");
});
test("late night has its own three lines and none of them asks a follow-up", () => {
const lateNight = WEDNESDAY(2);
assert.equal(createStartGreetingParts("周宁", lateNight, 0).text, "夜猫子,晚上好");
assert.equal(createStartGreetingParts("周宁", lateNight, 1 / 3).text, "还没睡呀,周宁");
assert.equal(createStartGreetingParts("周宁", lateNight, 0.7).text, "晚安前再聊一会");
// 23 点与 4 点同属深夜。
assert.equal(createStartGreetingParts("周宁", WEDNESDAY(23), 1 / 3).text, "还没睡呀,周宁");
assert.equal(createStartGreetingParts("周宁", WEDNESDAY(4), 1 / 3).text, "还没睡呀,周宁");
});
test("a blank name leaves only the lines that carry no name", () => {
const lateNight = WEDNESDAY(2);
assert.equal(createStartGreetingParts("", lateNight, 0).text, "夜猫子,晚上好");
assert.equal(createStartGreetingParts(" ", lateNight, 0.9).text, "晚安前再聊一会");
// 周一的星期条目带称呼,名字为空时整条不可达;回访池同理。
for (let step = 0; step < 100; step += 1) {
for (const hasSessions of [false, true]) {
const text = createStartGreetingParts("", MONDAY(9), step / 100, hasSessions).text;
assert.doesNotMatch(text, /周一了/);
assert.doesNotMatch(text, /又见面了|嗨,|最近还好吗/);
assert.ok(text.length > 0);
assert.doesNotMatch(text, /\{名字\}/);
assert.doesNotMatch(text, /$|^/);
}
}
});
test("the weekday pool only opens on its own day", () => {
const friday = FRIDAY(9);
const fridayLines = new Set(Array.from({ length: 100 }, (_, step) =>
createStartGreetingParts("周宁", friday, step / 100).text));
assert.ok(fridayLines.has("周五快乐"));
const wednesdayLines = new Set(Array.from({ length: 100 }, (_, step) =>
createStartGreetingParts("周宁", WEDNESDAY(9), step / 100).text));
assert.ok(!wednesdayLines.has("周五快乐"));
assert.equal(wednesdayLines.size, 3);
assert.ok(new Set(Array.from({ length: 100 }, (_, step) =>
createStartGreetingParts("周宁", MONDAY(9), step / 100).text)).has("周一了,周宁"));
assert.ok(new Set(Array.from({ length: 100 }, (_, step) =>
createStartGreetingParts("周宁", SUNDAY(9), step / 100).text)).has("周日,慢一点"));
});
test("the returning pool needs at least one session behind the account", () => {
const evening = WEDNESDAY(20);
const returning = ["又见面了,周宁", "欢迎回来", "嗨,周宁", "最近还好吗,周宁?"];
const firstVisit = new Set(Array.from({ length: 100 }, (_, step) =>
createStartGreetingParts("周宁", evening, step / 100, false).text));
for (const line of returning) assert.ok(!firstVisit.has(line), `first visit must not reach ${line}`);
assert.equal(firstVisit.size, 3);
const returnVisit = new Set(Array.from({ length: 100 }, (_, step) =>
createStartGreetingParts("周宁", evening, step / 100, true).text));
for (const line of returning) assert.ok(returnVisit.has(line), `return visit must reach ${line}`);
assert.equal(returnVisit.size, 7);
});
test("the same inputs always produce the same line", () => {
for (const hour of [2, 8, 12, 16, 20]) {
for (const hasSessions of [false, true]) {
for (const step of [0, 17, 42, 99]) {
const args = [hour, step / 100, hasSessions] as const;
const first = createStartGreeting("周宁", WEDNESDAY(args[0]), args[1], args[2]);
const second = createStartGreeting("周宁", WEDNESDAY(args[0]), args[1], args[2]);
assert.equal(first, second);
}
}
}
});
test("no greeting line uses a metaphor or trails off into a sentence", () => {
for (let day = 19; day <= 25; day += 1) {
for (let hour = 0; hour < 24; hour += 1) {
for (const name of ["周宁", ""]) {
for (const hasSessions of [false, true]) {
for (let step = 0; step < 100; step += 1) {
const text = createStartGreetingParts(
name,
new Date(2026, 6, day, hour),
step / 100,
hasSessions,
).text;
assert.doesNotMatch(text, /[星月陪]/, `metaphor in ${text}`);
assert.doesNotMatch(text, /。$/, `sentence period in ${text}`);
assert.ok(text.length > 0);
assert.ok(text.length <= 12, `${text} is longer than one short line`);
}
}
}
}
}
});
test("the starter hero is one heading and nothing else", () => {
const source = homeSurface;
// 原值 `className="starter-greeting"` / 新值 `className="starter-salutation"`
// / 原因:`.starter-greeting` 这个类名此前既指招呼语那一行、又被读成整块问候区,
// 拆成 `.starter-greeting-block`(容器)与 `.starter-salutation`(招呼语)后不再歧义。
//
// 原值:断言 `<p className="starter-salutation">` 与 `<h1>{starterGreeting.question}</h1>` 同时存在
// / 新值:断言只剩 `<h1 id="starter-heading">{starterGreeting.text}</h1>`,且 `starter-salutation` 在源码与样式里都消失
// / 原因:开场语改成单行问候,副标题那一行连同它的样式一起下线;断言从「两行」改成「一行且不得有第二行」,强度不降反升。
assert.match(source, /<h1 id="starter-heading">\{starterGreeting\.text\}<\/h1>/);
assert.doesNotMatch(source, /starter-salutation/);
assert.doesNotMatch(source, /starterGreeting\.(salutation|question)/);
// Then: no third line repeats the welcome the heading already carries.
assert.doesNotMatch(source, /starter-hero-note/);
const styles = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8");
assert.doesNotMatch(styles, /starter-hero-note/);
assert.doesNotMatch(styles, /starter-salutation/);
// And: the separate static heading pool is gone for good.
assert.doesNotMatch(source, /starterPrompt|createStarterPrompt|greetingForHour/);
assert.doesNotMatch(source, /greeting: createStartGreeting\(/);
});
test("the greeting knows whether the account already has sessions", () => {
// hasSessions 必须来自已有的会话列表变量,不得新开一份状态。
assert.match(homeSurface, /const starterHasPriorSessions = sessions\.some\(/);
assert.match(homeSurface, /createStartGreetingParts\(profile\.name, new Date\(\), starterGreetingSelection, starterHasPriorSessions\)/);
});