fix(home,chart,ephemeris): drop the unfinished-rectification hint, full chart-type captions, orbit instead of 还没拿到, constellation icon (BUG-1111..1114)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
This commit is contained in:
Jesse_Chen
2026-09-30 01:11:47 +08:00
co-authored by Claude Opus 5.5
parent 82db6373d6
commit 921992a492
14 changed files with 171 additions and 38 deletions
+41
View File
@@ -14942,3 +14942,44 @@
- 防复发:真机复测后更新本条状态;若仍复现需真机录屏与 Safari 版本。
- 相关记录:TASK-report-english-edition-20260929
- 修复版本:分支 `codex/surface-feedback-20260929`
## BUG-1111 | 首页常驻「上次那次校正还没完成,可以在历史对话里接着做。」
- 状态:resolved(代码 + 回归测试)
- 首次发现 / 最近更新:2026-09-30 / 2026-09-30
- 影响面:`components/starter-home.tsx::rectificationEntryHint`。
- 现象:有未完成的校正时首页入口下方一直挂着这句提示;产品要求去掉。
- 修复:`rectificationEntryHint(rectificationForSelf)` 只保留「生时校正暂时只支持本人。」一句;未完成的校正仍在历史列表里。
- 验证:`starter-home-polish-20260926.test.tsx`、`rectification-agentic-entry.test.ts`(断言三栏更新)、`surface-feedback-20260929.test.tsx`。
- 相关记录:BUG-1041、TASK-starter-home-polish-20260926 D2
- 修复版本:分支 `codex/surface-feedback-20260929`
## BUG-1112 | 星盘类型面板里每格的说明文字被截掉下半截 / 显示不全
- 状态:resolved(代码 + 回归测试;iPhone 真机欠)
- 首次发现 / 最近更新:2026-09-30 / 2026-09-30
- 影响面:`globals.css .chart-page-type-cell small`。
- 根因:说明行 `white-space: nowrap; overflow: hidden`,行高沿用按钮的紧行高,iPhone 上中文字形下半被裁;「以月亮为第 1 宫」这类长说明只能省略。
- 修复:说明行行高 1.4、最多两行(line-clamp),格子最小高 56 → 64px。
- 验证:`chart-type-picker.test.tsx`(三栏)、`surface-feedback-20260929.test.tsx`。
- 修复版本:分支 `codex/surface-feedback-20260929`
## BUG-1113 | 星盘、星历加载时写「这一张盘还没拿到。」「这一天的星历还没拿到。」
- 状态:resolved(代码 + 回归测试 + 本机手机视口截图)
- 首次发现 / 最近更新:2026-09-30 / 2026-09-30
- 影响面:新增 `components/orbit-waiting.tsx`;`secondary-page-shell.tsx`、`chart-page-skeleton.tsx`、`chart-dasha-tab.tsx`。
- 现象:几秒内就会加载好的内容,先用一句「还没拿到」占位,读起来像出错。
- 修复:统一用首页加载屏的轨道环(40px)作为次级页短等待动画:星盘空盘中央、星历 / 报告列表首屏、Chara 大运栏;原句只留给读屏器(`sr-only` + `role="status"`)。
- 产品决定:此前 AGENTS §6「揭幕后不得再出现加载动画」只对首页;星盘页 Chara 栏等待改为轨道环属产品本次明确要求,记录在此。
- 验证:`surface-feedback-20260929.test.tsx`;既有「还没拿到」断言因文本仍在读屏节点里而不变。
- 修复版本:分支 `codex/surface-feedback-20260929`
## BUG-1114 | 「那一刻的天空」按钮用的是各平台常见的星芒(AI)图标
- 状态:resolved(代码 + 回归测试)
- 首次发现 / 最近更新:2026-09-30 / 2026-09-30
- 修复:改为自绘「小星座」图标(四颗星以细线相连),与点开后的群星汇聚一致;产品在三个方案中选定。
- 验证:`surface-feedback-20260929.test.tsx`。
- 相关记录:BUG-1102
- 修复版本:分支 `codex/surface-feedback-20260929`
+5 -1
View File
@@ -563,6 +563,10 @@ one skeleton, none of them carrying the sidebar.
more. A page's own reassurance copy (the chart page's cost-and-speed eyebrow,
for one) stays in the body; it is content, not chrome.
### Short waits on secondary pages (2026-09-30, BUG-1113)
The chart page, the ephemeris and the report list wait with one motion: the home loading screen's orbit ring at 40px (`OrbitWaiting`), centred in the empty chart grid or in the page. No sentence is shown; the old 「……还没拿到。」 copy is kept only as the screen-reader status. The chart's breathing empty grid stays (BUG-1016 exception).
### Personal report centre
- **Structure:** inside the app shell, not a page of its own. The name 「我的报告」 sits alone in the 46px header. The body opens with the **generate card** (`.report-center-create`, 2026-09-29, product sketch): a centred `--color-canvas` sheet, `--space-6` below the header (BUG-1103: it used to sit flush under it), with a file icon, the title 「完整本命报告」 (what you get), one line 「星盘、力量、大运、年运、瑜伽共 6 章,中英两版;生成后可离开,完成时下方自动出现。」 and the filled 「生成报告」 button (what it does) — title and button no longer say the same thing — the page's only generate entry; the header button was deleted rather than kept as a second one. Below it, 「过往的报告」 heads the **row list** of reports; with none yet, a single quiet line 「还没有个人报告。」 replaces the old second big empty card. The supporting paragraph (「有填报到分钟的出生时间即可生成……」) and the 「共 N 份 · 已完成 N 份」 overview line above it were removed on 2026-09-28 (TASK-self-edit-avatar-menu-20260928 S3, product: 「这里的提示去掉」); the minute requirement still lives in the 生成 button's hover title.
@@ -1183,7 +1187,7 @@ D40 / D45 / D60 的表格上方,出生时间不是「已校正」时多一句
### 位置一:星盘页顶栏按钮
- 星盘页 `ok` 后立即预取 `/api/birth-sky?subject=<当前人物>` 并在屏外画好 PNG;**画好之后**顶栏右侧才出现一枚描边按钮「那一刻的天空」(`<Button variant="outline" size="sm">`,顶栏里按 `.chat-header-actions button` 收成 32px + 命中区外扩)。2026-09-29 晚改(BUG-1102,产品「按钮不明显、慢一秒才出现」):星盘 `ok` 即显示按钮,不等 PNG;封面还在画时点击被记下,画好立即开播重放,中间不插转圈(重放的汇聚本身就是等待);样式改为 `.birth-sky-entry`:星芒图标 + `--color-action` 12% 浅底 + accent 字色 600 字重(不用 `--color-action-soft` 实块,BUG-1078)。预取失败时按钮消失,资料不全、切人物时都不显示按钮,不报错。切换人物或改出生资料(指纹变)就按新人物重新预取。家人的盘也能生成。
- 星盘页 `ok` 后立即预取 `/api/birth-sky?subject=<当前人物>` 并在屏外画好 PNG;**画好之后**顶栏右侧才出现一枚描边按钮「那一刻的天空」(`<Button variant="outline" size="sm">`,顶栏里按 `.chat-header-actions button` 收成 32px + 命中区外扩)。2026-09-29 晚改(BUG-1102,产品「按钮不明显、慢一秒才出现」):星盘 `ok` 即显示按钮,不等 PNG;封面还在画时点击被记下,画好立即开播重放,中间不插转圈(重放的汇聚本身就是等待);样式改为 `.birth-sky-entry`:自绘小星座图标(四颗星细线相连,2026-09-30 替换星芒,BUG-1114)+ `--color-action` 12% 浅底 + accent 字色 600 字重(不用 `--color-action-soft` 实块,BUG-1078)。预取失败时按钮消失,资料不全、切人物时都不显示按钮,不报错。切换人物或改出生资料(指纹变)就按新人物重新预取。家人的盘也能生成。
- 点按钮**重放群星汇聚并定格**(2026-09-29,TASK-mobile-chart-and-confirmed-edit-20260929 M4,替换第三轮 P1 的静态海报弹层):与首次建盘过场同一个组件 `BirthSkyConverge`、同一条时间轴,`mode="hold"`——汇聚约 3 秒、文字 600ms 淡入,然后**停在定格帧不动**,不淡出、不自动关闭。每次打开都从头重放。定格帧就是导出的海报(画布调用序列逐条相同,且用生成 PNG 时同一个宋体判定 `cover.serif`)。全屏原生 `<dialog>` portal 到 `document.body`,底色 `night` 页底色,画布取视口内最大 3:4、乘 DPR。
- 控件浮在画面上方一行:**左上 ×**(只有图标,读屏名「关闭」)、**右上「分享」**(分享图标 + 字,night 墨色细描边)。播放中也在,都 ≥ 44px,顶部留安全区。点画面(不在控件上)直接跳到定格帧、**不关闭**;按 Esc 或点 × 关闭,焦点回到按钮。`prefers-reduced-motion` 时不汇聚,600ms 淡入到定格帧。
- 「分享」:浏览器能分享文件时(iOS Safari)调 `navigator.share({ files })`,系统面板里有「存储图像」;否则 Blob + `a.download`。分享的就是屏幕上定格的那张 PNG(按钮出现前已画好,分享从不等待)。用户取消不算错误、不提示;其他失败在右上方小字「没能分享,再试一次。」。
+4
View File
@@ -149,6 +149,10 @@ Jyotisha 的可见文案是产品的一部分。正确性红线(真实性、
- 英文版正文的口径:用 BPHS / PL9 通行的英文术语(Exalted、Own Sign、Mooltrikona、Sub lord……),不音译中文、不中英混写;限制说明用固定英文四句(见 `report-public-projection.ts` 的 `ENGLISH_LIMITATION_COPY`)。应用外壳(侧栏、标题、按钮、到底部)始终是中文。
## 等待时不写「还没拿到」(2026-09-30)
星盘、星历、报告列表几秒内就会出现,等待时只放轨道环动画,不在屏幕上写「这一张盘还没拿到。」一类句子;这些句子只给读屏器。首页不再写「上次那次校正还没完成,可以在历史对话里接着做。」。
## 报告里的限制说明
普通报告只留下人能读的正文、结论、行动建议和必要限制。不写字段名、状态码、评分、权重或执行账本。说不到日期就写「这次说不到具体哪一天。」依据没补上就写「有些依据还没补上,相关说法不能当成确定预测。」判断没闭合就写「有些判断还没闭合,不能写成确定结论。」不出现 `technique_truth`、`workflow_route` 这类键。
+17 -7
View File
@@ -2231,6 +2231,11 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
line-height: 1.6;
text-align: center;
}
/* The home screen's orbit ring at 40px: the shared short-wait motion for the
chart, ephemeris and report pages (2026-09-30). */
.orbit-waiting { display: grid; place-items: center; min-height: 64px; }
.orbit-waiting .app-loading-symbol { width: 40px; height: 40px; }
.orbit-waiting .app-loading-mark { width: 22px; height: 22px; }
.chart-page-body,
.ephemeris-body,
.report-center-body,
@@ -5175,7 +5180,7 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
.chart-page-type-cell {
display: flex;
min-width: 0;
min-height: 56px;
min-height: 64px;
flex-direction: column;
align-items: center;
justify-content: center;
@@ -5187,14 +5192,21 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
color: var(--color-ink);
cursor: pointer;
}
.chart-page-type-cell strong { font-size: var(--type-body-md); font-weight: 500; font-variant-numeric: tabular-nums; }
.chart-page-type-cell strong { font-size: var(--type-body-md); font-weight: 500; line-height: 1.3; font-variant-numeric: tabular-nums; }
/* The meaning line wraps to at most two lines (2026-09-30): it used to be one
clipped line whose glyph bottoms were cut off on iPhone (the button's inherited
line-height was tighter than CJK glyphs), and 「以月亮为第 1 宫」 ran out of room. */
.chart-page-type-cell small {
display: -webkit-box;
max-width: 100%;
overflow: hidden;
color: var(--color-ink-tertiary);
font-size: var(--type-caption);
text-overflow: ellipsis;
white-space: nowrap;
line-height: 1.4;
text-align: center;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
line-clamp: 2;
}
.chart-page-type-cell:hover { background: var(--color-canvas-muted); }
.chart-page-type-cell.is-current { border-color: var(--color-ink); background: var(--color-selected); }
@@ -5285,12 +5297,10 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
/* Waiting copy sits over the breathing empty chart, centred, so the waiting
stage and the finished one are the same height. */
.chart-page-waiting-plot { position: relative; }
.chart-page-note.chart-page-skeleton-copy {
.chart-page-skeleton-copy {
position: absolute;
inset: 50% var(--space-4) auto;
margin: 0;
transform: translateY(-50%);
text-align: center;
}
.chart-page-symbol-hit { fill: transparent; stroke: none; }
.chart-page-symbol-target { cursor: help; }
@@ -1,7 +1,6 @@
"use client";
import dynamic from "next/dynamic";
import { Sparkles } from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
@@ -28,10 +27,28 @@ export function BirthSkyEntry({ cover }: { cover: BirthSkyCover | null }) {
return (
<>
<Button type="button" variant="secondary" size="sm" className="birth-sky-entry" onClick={() => setWanted(true)}>
<Sparkles aria-hidden="true" />
<ConstellationIcon />
{BIRTH_SKY_ENTRY_LABEL}
</Button>
{open ? <BirthSkyReplay cover={cover} onClose={() => setWanted(false)} /> : null}
</>
);
}
/**
* A small constellation: four stars joined by thin lines (2026-09-30, product:
* the sparkle icon is on every AI product). It says what the button opens —
* the stars of that moment drawing together — and is drawn here, not taken
* from an icon set.
*/
function ConstellationIcon() {
return (
<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true">
<path d="M2.5 11.5 L6 5 L10.5 7.5 L13.5 2.5" fill="none" stroke="currentColor" strokeWidth="1" strokeLinecap="round" strokeLinejoin="round" opacity=".55" />
<circle cx="2.5" cy="11.5" r="1.3" fill="currentColor" />
<circle cx="6" cy="5" r="1.6" fill="currentColor" />
<circle cx="10.5" cy="7.5" r="1.2" fill="currentColor" />
<circle cx="13.5" cy="2.5" r="1.4" fill="currentColor" />
</svg>
);
}
@@ -1,4 +1,5 @@
import { chartLayerFailureCopy } from "@/lib/chart-view-failure";
import { OrbitWaiting } from "@/components/orbit-waiting";
import type { ChartViewFailureStatus, ChartViewOk } from "@/lib/chart-view-contract";
import { CHARA_ZERO_YEAR_LABEL, CHARA_ZERO_YEAR_NOTE, CHART_VIEW_COPY } from "@/lib/chart-view-labels";
@@ -154,7 +155,9 @@ export function ChartDashaTab({
<Track {...view.dasha.vimshottari} asOf={asOf} />
{charaPending || charaFailure ? (
<section className="chart-page-dasha-track">
<p className="chart-page-note">{charaPending ? CHART_VIEW_COPY.waitingLayer : chartLayerFailureCopy(charaFailure!)}</p>
{charaPending
? <OrbitWaiting label={CHART_VIEW_COPY.waitingLayer} />
: <p className="chart-page-note">{chartLayerFailureCopy(charaFailure!)}</p>}
</section>
) : (
<Track {...view.dasha.chara} asOf={asOf} />
@@ -1,5 +1,6 @@
import { EMPTY_NORTH_INDIAN_CHART, VedicChartSvg } from "@/components/personal-report/vedic-chart-svg";
import { EMPTY_WESTERN_WHEEL, WesternWheelSvg } from "./western-wheel-svg";
import { OrbitWaiting } from "@/components/orbit-waiting";
/** Only chart positions opt in; the shared page shell remains static. */
export function ChartPageSkeleton({ copy, western = false }: { copy: string; western?: boolean }) {
@@ -10,9 +11,10 @@ export function ChartPageSkeleton({ copy, western = false }: { copy: string; wes
{western ? <WesternWheelSvg western={EMPTY_WESTERN_WHEEL} skeleton />
: <VedicChartSvg chart={EMPTY_NORTH_INDIAN_CHART} ariaLabel={copy} skeleton />}
</div>
{/* Over the empty drawing, not under it: the finished chart has no line
below it any more (BUG-1101), so a line here would make it jump. */}
<p className="chart-page-note chart-page-skeleton-copy">{copy}</p>
{/* Over the empty drawing, not under it, so the waiting stage and the
finished one are the same height (BUG-1101). An orbit, not a sentence
(2026-09-30); the sentence is for screen readers. */}
<OrbitWaiting className="chart-page-skeleton-copy" label={copy} />
</div>
</div>
);
+17
View File
@@ -0,0 +1,17 @@
/**
* The one short-wait animation for secondary pages (2026-09-30, product: while
* a chart or a day's ephemeris loads, show motion instead of 「……还没拿到。」).
* It is the home loading screen's orbit ring at a smaller size, so the app has
* one waiting motif. The sentence stays for screen readers only.
*/
export function OrbitWaiting({ label, className = "" }: Readonly<{ label: string; className?: string }>) {
return (
<div className={`orbit-waiting${className ? ` ${className}` : ""}`} role="status">
<span className="app-loading-symbol" aria-hidden="true">
<span className="app-loading-orbit" />
<span className="app-loading-mark" />
</span>
<span className="sr-only">{label}</span>
</div>
);
}
@@ -3,6 +3,7 @@
import type { ReactNode } from "react";
import { CurrentSubjectSwitcher } from "@/components/current-subject-switcher";
import { OrbitWaiting } from "@/components/orbit-waiting";
import { SecondaryHeader } from "@/components/secondary-header";
export type SecondaryPageShellProps = {
@@ -40,7 +41,7 @@ export function SecondaryPageShell({
actions={actions}
/>
<div className="secondary-page">
{waiting ? <p className="secondary-page-waiting">{waiting}</p> : children}
{waiting ? <OrbitWaiting className="secondary-page-waiting" label={waiting} /> : children}
</div>
</>
);
+6 -11
View File
@@ -45,16 +45,11 @@ export function StarterGreeting({ starterGreeting, dailyTrend = null, dailyTrend
}
/* The line under the pills only appears when there is something to do about
it. First runs and finished cases say nothing: the pill already names the
action. The branch reads the entry summary itself, not the derived
start/restart label, and a non-self subject wins because the case summary
belongs to the account owner. */
export function rectificationEntryHint(
rectificationForSelf: boolean,
summary: RectificationEntrySummary | null,
): string | null {
it: a non-self subject cannot start a rectification. A resumable case says
nothing here any more (2026-09-30, product: 「这个提示可以去掉」); the case
is still in the history list. */
export function rectificationEntryHint(rectificationForSelf: boolean): string | null {
if (!rectificationForSelf) return "生时校正暂时只支持本人。";
if (summary?.hasResumableCase) return "上次那次校正还没完成,可以在历史对话里接着做。";
return null;
}
@@ -67,6 +62,7 @@ export type StarterEntriesProps = {
readonly rectificationLoading: boolean;
readonly rectificationMutationPending: boolean;
readonly openRectificationFromHomepage: () => void;
/** Still passed by the page; the hint no longer reads it (2026-09-30). */
readonly rectificationEntrySummary: RectificationEntrySummary | null;
readonly rectificationError: string;
readonly rectificationSurfaceOpen: boolean;
@@ -82,7 +78,6 @@ export function StarterEntries({
rectificationLoading,
rectificationMutationPending,
openRectificationFromHomepage,
rectificationEntrySummary,
rectificationError,
rectificationSurfaceOpen,
rectificationErrorMessage,
@@ -90,7 +85,7 @@ export function StarterEntries({
const subjectId = useCurrentSubject();
const rectificationForSelf = subjectId === "self";
const dailyLabel = natalMinuteAvailable ? "今日星语" : "每日运势";
const rectificationHint = rectificationEntryHint(rectificationForSelf, rectificationEntrySummary);
const rectificationHint = rectificationEntryHint(rectificationForSelf);
return (
<div className="starter-entries">
+3 -1
View File
@@ -69,7 +69,9 @@ test("the strip is one sideways-scrolling line and the sheet is a three-column g
assert.match(strip, /min-width:\s*0/);
assert.match(cssDeclarations(".chart-page-type-grid", globalStyles), /grid-template-columns:\s*repeat\(3, minmax\(0, 1fr\)\)/);
assert.match(cssDeclarations(".chart-page-type-menu", globalStyles), /width:\s*44px/);
assert.match(cssDeclarations(".chart-page-type-cell", globalStyles), /min-height:\s*56px/);
// 原值:min-height 56px;新值:64px;原因:2026-09-30 真机截图说明文字被截底,说明行改为最多两行、行高 1.4,格子加高以容纳
assert.match(cssDeclarations(".chart-page-type-cell", globalStyles), /min-height:\s*64px/);
assert.match(cssDeclarations(".chart-page-type-cell small", globalStyles), /line-clamp:\s*2/);
assert.doesNotMatch(globalStyles, /\.chart-page-varga-chips/);
});
@@ -986,8 +986,11 @@ test("homepage CTA is server-driven from the entry summary, not the session list
// 「restart」分支对应的那句已删除,改锁 hint 读的是 summary 本身、不读会话列表
// 原因:TASK-starter-home-polish-20260926 D2 删掉「上次已经校正完…」一句;BUG-1041 查明
// `=== "restart"` 从未命中(解析器读错键名)。「CTA 与提示由服务端 entry summary 驱动」这条意图不变。
assert.match(page, /rectificationEntryHint\(rectificationForSelf, rectificationEntrySummary\)/);
assert.match(page, /summary\?\.hasResumableCase/);
// 原值:首页小字 rectificationEntryHint(rectificationForSelf, rectificationEntrySummary) 读 summary?.hasResumableCase
// 新值:rectificationEntryHint(rectificationForSelf),不再读 summary(未完成校正的提示句已删)
// 原因:2026-09-30 产品「首页的这个提示可以去掉」;CTA 仍由 entry summary 驱动(下方 resolveRectificationEntryAction 断言不变)
assert.match(page, /rectificationEntryHint\(rectificationForSelf\)/);
assert.doesNotMatch(page, /上次那次校正还没完成/);
assert.doesNotMatch(page, /rectificationCardAction === "restart"/);
assert.match(entryLib, /开始新的生时校正/);
assert.match(entryLib, /再次校正/);
@@ -73,19 +73,21 @@ test("D2: the line under the pills only appears when the user has something to d
assert.doesNotMatch(markup, REMOVED_HINTS);
}
// An unfinished case: one sentence, the resumable one.
// 原值:未完成的校正显示一句「上次那次校正还没完成,可以在历史对话里接着做。」
// 新值:未完成的校正也不显示任何提示
// 原因:2026-09-30 产品「首页的这个提示可以去掉」;校正会话仍在历史列表里
const resumable = renderToStaticMarkup(<StarterEntries {...entriesProps({ rectificationEntrySummary: summaries.resumable })} />);
assert.equal(hintOf(resumable), RESUMABLE_HINT);
assert.equal(hintOf(resumable), null);
assert.doesNotMatch(resumable, new RegExp(RESUMABLE_HINT));
assert.doesNotMatch(resumable, REMOVED_HINTS);
});
test("D2: a non-self subject gets the self-only sentence, even with an unfinished case on record", async () => {
assert.equal(rectificationEntryHint(false, null), NON_SELF_HINT);
assert.equal(rectificationEntryHint(false, summaries.resumable), NON_SELF_HINT);
assert.equal(rectificationEntryHint(true, summaries.resumable), RESUMABLE_HINT);
assert.equal(rectificationEntryHint(true, summaries.rectified), null);
assert.equal(rectificationEntryHint(true, summaries.firstRun), null);
assert.equal(rectificationEntryHint(true, null), null);
// 原值:rectificationEntryHint(rectificationForSelf, summary),本人 + 未完成时返回 RESUMABLE_HINT
// 新值:rectificationEntryHint(rectificationForSelf),本人一律无提示;非本人仍是「生时校正暂时只支持本人。」
// 原因:2026-09-30 产品删除未完成校正提示,入参 summary 不再使用
assert.equal(rectificationEntryHint(false), NON_SELF_HINT);
assert.equal(rectificationEntryHint(true), null);
// Rendered through the real subject store, not just the helper.
const harness = createClientLifecycleHarness();
@@ -194,5 +196,6 @@ test("BUG-1041: the homepage parser reads what the entry-summary route actually
const resumable = entrySummaryFromResponse(resumableWire);
assert.equal(resumable.hasResumableCase, true);
assert.equal(resumable.latestResumable?.lastActivityAt, "2026-09-26T00:00:00.000Z");
assert.equal(rectificationEntryHint(true, resumable), RESUMABLE_HINT);
// 原值:rectificationEntryHint(true, resumable) === RESUMABLE_HINT;新值:解析仍读出 hasResumableCase,但首页不再据此出提示;原因同上
assert.equal(rectificationEntryHint(true), null);
});
@@ -130,3 +130,34 @@ test("switching editions keeps the reader on the same chapter (BUG-1108)", () =>
assert.match(view, /drawn=\{drawnThrough !== null && chapter <= drawnThrough\}/);
assert.match(view, /useState\(section\.eager \|\| drawn\)/);
});
// Product feedback 2026-09-30 01:00.
test("the home no longer says an unfinished rectification is waiting in history", async () => {
const { rectificationEntryHint } = await import("../src/components/starter-home");
assert.equal(rectificationEntryHint(true), null);
assert.doesNotMatch(read("../src/components/starter-home.tsx"), /上次那次校正还没完成/);
});
test("chart-type cells show the whole meaning line (two lines, CJK-safe line height)", () => {
const css = read("../src/app/globals.css");
const small = css.slice(css.indexOf(".chart-page-type-cell small {"), css.indexOf("}", css.indexOf(".chart-page-type-cell small {")));
assert.match(small, /line-height: 1\.4/);
assert.match(small, /-webkit-line-clamp: 2/);
assert.doesNotMatch(small, /white-space: nowrap/);
});
test("chart, ephemeris and report waits show the orbit, the sentence only for screen readers", () => {
const orbit = read("../src/components/orbit-waiting.tsx");
assert.match(orbit, /<span className="sr-only">\{label\}<\/span>/);
assert.match(orbit, /app-loading-orbit/);
assert.match(read("../src/components/secondary-page-shell.tsx"), /<OrbitWaiting className="secondary-page-waiting" label=\{waiting\} \/>/);
assert.match(read("../src/components/chart-page/chart-page-skeleton.tsx"), /<OrbitWaiting className="chart-page-skeleton-copy" label=\{copy\} \/>/);
assert.doesNotMatch(read("../src/components/chart-page/chart-page-skeleton.tsx"), /<p className="chart-page-note/);
});
test("the sky button carries a drawn constellation, not the sparkle", () => {
const entry = read("../src/components/birth-sky/birth-sky-entry.tsx");
assert.doesNotMatch(entry, /Sparkles/);
assert.match(entry, /<ConstellationIcon \/>/);
assert.equal((entry.match(/<circle /g) ?? []).length, 4);
});