桌面侧栏可在 200 到 360 像素之间拖宽,拖到最左收起,宽度记在本机。手机从左边滑出。会话在同一组里拖动排序,顺序写入可空的 sidebar_order,收藏仍置顶。账户菜单、设置、反馈和退出确认改用组件库。顶栏点数只显示图标和数字。启程清单还在。菜单、拖宽、拖动排序和翻页在首屏之后再载入。
自测:tsc 退出码 0;npm run lint 0 error、121 warning;改到的前端测试 205 项全过;npm run build 后首页仍是 static;首屏 gzip 630350,相对 8f7c0f7d 的 621226 为 +1.47%。test:db 92 项里 79 过、13 败,失败名与 Windows 已知清单一致,排序列在通过的 ok 37。没有跑全量 npm test,没有跑 Python 快速门。没有登录态,没有在浏览器里拖侧栏。
Co-Authored-By: Grok (agent loop)
94 lines
4.0 KiB
TypeScript
94 lines
4.0 KiB
TypeScript
import assert from "node:assert/strict";
|
||
import test from "node:test";
|
||
import { readFileSync } from "node:fs";
|
||
import { createElement } from "react";
|
||
import { renderToStaticMarkup } from "react-dom/server";
|
||
import { PathnameContext } from "next/dist/shared/lib/hooks-client-context.shared-runtime";
|
||
|
||
import { AppSidebar } from "../src/components/app-sidebar.tsx";
|
||
import { SidebarProvider } from "../src/components/ui/sidebar.tsx";
|
||
import { reportSurfaceRedirect } from "../src/lib/synastry-report-href.ts";
|
||
|
||
function renderSidebar(pathname: string) {
|
||
return renderToStaticMarkup(
|
||
createElement(
|
||
PathnameContext.Provider,
|
||
{ value: pathname },
|
||
createElement(
|
||
SidebarProvider,
|
||
null,
|
||
createElement(AppSidebar, {
|
||
sessions: [],
|
||
activeSessionId: null,
|
||
compatibilityEnabled: true,
|
||
account: {
|
||
name: "示例甲",
|
||
email: "fictional@example.invalid",
|
||
credits: 12,
|
||
initial: "示",
|
||
avatar: null,
|
||
},
|
||
}),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
function navAnchor(markup: string, label: string) {
|
||
// 原值:锚点类名 `report-nav-button`
|
||
// 新值:锚点类名 `sidebar-nav-link`
|
||
// 原因:K1 侧栏导航共用一个类,合盘入口仍是这个链接
|
||
const anchors = markup.match(/<a\b[^>]*\bsidebar-nav-link\b[^>]*>[\s\S]*?<\/a>/g) ?? [];
|
||
const hit = anchors.find((anchor) => anchor.includes(`>${label}<`));
|
||
assert.ok(hit, `sidebar nav link missing: ${label}`);
|
||
return hit;
|
||
}
|
||
|
||
const read = (path: string) => readFileSync(new URL(path, import.meta.url), "utf8");
|
||
|
||
test("an old synastry url moves to the synastry route and keeps the language", () => {
|
||
assert.equal(
|
||
reportSurfaceRedirect({ surface: "personal", reportType: "synastry_full", reportId: "abc", language: "en" }),
|
||
"/synastry/abc?lang=en",
|
||
);
|
||
assert.equal(
|
||
reportSurfaceRedirect({ surface: "synastry", reportType: "personal_full", reportId: "abc", language: "zh" }),
|
||
"/reports/abc",
|
||
);
|
||
assert.equal(
|
||
reportSurfaceRedirect({ surface: "synastry", reportType: "synastry_full", reportId: "abc", language: "zh" }),
|
||
null,
|
||
);
|
||
assert.equal(
|
||
reportSurfaceRedirect({ surface: "personal", reportType: "personal_full", reportId: "abc", language: "en" }),
|
||
null,
|
||
);
|
||
});
|
||
|
||
test("the synastry route reuses the reader and the sidebar stays on 婚恋合盘", () => {
|
||
const page = read("../src/app/(app)/synastry/[reportId]/page.tsx");
|
||
const reader = read("../src/components/personal-report/personal-report-page.tsx");
|
||
const sidebar = read("../src/components/app-sidebar.tsx");
|
||
const picker = read("../src/components/synastry/synastry-picker.tsx");
|
||
const center = read("../src/components/personal-report/personal-report-center.tsx");
|
||
assert.match(page, /title: "婚恋合盘 · Jyotisha"/);
|
||
assert.match(page, /readReportLanguage\(lang\)/);
|
||
assert.match(page, /surface="synastry"/);
|
||
assert.match(reader, /window\.location\.replace\(redirectTo\)/);
|
||
assert.match(sidebar, /\{ href: "\/synastry", label: "婚恋合盘"/);
|
||
assert.match(sidebar, /pathname === href \|\| pathname\.startsWith\(`\$\{href\}\/`\)/);
|
||
// 原值: assert.equal("/synastry/abc".startsWith("/synastry/"), true)
|
||
// 新值: pathname 为 /synastry/<id> 时渲染侧栏,当前项(data-active="true")是「婚恋合盘」,不是「我的报告」
|
||
// 原因: 原断言永远成立,没有渲染侧栏(issue #85 G3)
|
||
const markup = renderSidebar("/synastry/abc");
|
||
const synastry = navAnchor(markup, "婚恋合盘");
|
||
const reports = navAnchor(markup, "我的报告");
|
||
assert.match(synastry, /\bdata-active="true"/);
|
||
assert.doesNotMatch(reports, /\bdata-active="true"/);
|
||
assert.doesNotMatch(reports, /aria-current="page"/);
|
||
assert.match(picker, /window\.location\.assign\(`\/synastry\/\$\{payload\.report\.id\}`\)/);
|
||
assert.match(picker, /href=\{`\/synastry\/\$\{report\.id\}`\}/);
|
||
assert.doesNotMatch(picker, /\/reports\/\$\{/);
|
||
assert.match(center, /reportType === "synastry_full" \? `\/synastry\/\$\{id\}` : `\/reports\/\$\{id\}`/);
|
||
});
|