生产 v0.3.8~v0.3.13 均由 8013196 加工作区修改构建部署,此前未提交;本提交补齐源码,
并含未部署的 v0.3.14 原地入库。中间各版本源码状态未保存,无法逐版拆分,详见
docs/plan/17-full-upgrade.md 与 docs/plan/18-organize-release.md。
- v0.3.8 全面升级:Deno workspace 依赖、Router 8 / Vite 8、camofox 1.18.0 / Node 24、
PostgreSQL 18.6、备份客户端 18.6、演示 qB 5.2.4
- v0.3.9 媒体整理:分类规则、整理来源与目录快照(0059)、文件管理整理入口
- v0.3.10 手动下载与入库:独立手动下载、详情页资源下载、TMDB / Local 元数据兜底
(metadata.resolve_organize)、正常多版本 / 一次性洗版(0060)
- v0.3.11 搜索详情与桌面黑边修复
- v0.3.12 列表分页、搜索历史、首页概况
- v0.3.13 统一整理弹窗与队列
- v0.3.14 原地入库(0061,未部署)
同批审计修复:
- 刮削入队:确认原记录存在后才删重放产生的新记录;写回 run 关联失败时保持 queued,
由处理器补关联(对齐 MP 先登记、只在入队失败时记失败)
- 识别升级链只在结果更确定时采用,不再用 no_match 顶替原有歧义候选
- bluray_dir 资产以原盘目录本身为刮削根(MP 同做法)
- metadata.resolve_organize 登记到入库排障 Skill 与能力地图
- db/src/types.ts 按迁移重新生成;contracts 整理动作枚举顺序与数据库对齐
- 测试:原地入库用例改为直接查记录表;domain 测试模板补 version
- 前端:通知铃铛弹层与用户菜单统一样式;站点卡片共用卡头、底栏贴底、分页间距
依赖升级证据只提交摘要文件,registry-snapshot.json 移至 .local/(SHA-256 记于文档)。
Co-Authored-By: Claude Opus 5.5 <[email protected]>
91 lines
3.3 KiB
TypeScript
91 lines
3.3 KiB
TypeScript
import { createElement } from "react";
|
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
import { describe, expect, it } from "vitest";
|
|
import { DataTable } from "./data-table.tsx";
|
|
import { groupRows, readCollapsed, toggleCollapsed, writeCollapsed } from "./data-table-groups.ts";
|
|
|
|
type Row = { id: string; status: string; name: string };
|
|
const rows: Row[] = [
|
|
{ id: "1", status: "未识别", name: "a.mkv" },
|
|
{ id: "2", status: "已登记", name: "b.mkv" },
|
|
{ id: "3", status: "未识别", name: "c.mkv" },
|
|
];
|
|
|
|
describe("DataTable groupBy:分组逻辑", () => {
|
|
it("按首次出现顺序分组,组内保持原顺序", () => {
|
|
expect(groupRows(rows, (r) => r.status).map((g) => [g.key, g.rows.map((r) => r.id)])).toEqual([
|
|
["未识别", ["1", "3"]],
|
|
["已登记", ["2"]],
|
|
]);
|
|
});
|
|
it("给 order 时按 order 排组", () => {
|
|
const g = groupRows(
|
|
rows,
|
|
(r) => r.status,
|
|
(a, b) => a.localeCompare(b, "zh-CN"),
|
|
);
|
|
expect(g.map((x) => x.key)).toEqual(["未识别", "已登记"].sort((a, b) => a.localeCompare(b, "zh-CN")));
|
|
});
|
|
it("折叠切换返回新集合", () => {
|
|
const a = new Set<string>();
|
|
const b = toggleCollapsed(a, "x");
|
|
expect([...b]).toEqual(["x"]);
|
|
expect(a.size).toBe(0);
|
|
expect([...toggleCollapsed(b, "x")]).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("DataTable groupBy:记住折叠态", () => {
|
|
it("写入后能读回;没有 storageKey、内容损坏或存储不可用时为 null", () => {
|
|
const map = new Map<string, string>();
|
|
const store = () => ({
|
|
getItem: (k: string) => map.get(k) ?? null,
|
|
setItem: (k: string, v: string) => void map.set(k, v),
|
|
});
|
|
writeCollapsed("t", new Set(["已登记"]), store);
|
|
expect([...(readCollapsed("t", store) ?? [])]).toEqual(["已登记"]);
|
|
expect(readCollapsed(undefined, store)).toBeNull();
|
|
map.set("umt.table-groups:bad", "{oops");
|
|
expect(readCollapsed("bad", store)).toBeNull();
|
|
const blocked = () => ({
|
|
getItem: (): string | null => {
|
|
throw new Error("blocked");
|
|
},
|
|
setItem: () => {
|
|
throw new Error("blocked");
|
|
},
|
|
});
|
|
expect(readCollapsed("t", blocked)).toBeNull();
|
|
expect(() => writeCollapsed("t", new Set(), blocked)).not.toThrow();
|
|
expect(readCollapsed("t", () => undefined)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("DataTable groupBy:渲染", () => {
|
|
it("每组一个可折叠组头,带计数;首屏全部展开", () => {
|
|
const html = renderToStaticMarkup(
|
|
createElement(DataTable<Row>, {
|
|
columns: [{ key: "name", header: "名称", cell: (r: Row) => r.name }],
|
|
rows,
|
|
rowKey: (r: Row) => r.id,
|
|
groupBy: { key: (r: Row) => r.status },
|
|
}),
|
|
);
|
|
expect(html.match(/aria-expanded="true"/g)).toHaveLength(2);
|
|
expect(html).toContain("未识别");
|
|
expect(html).toMatch(/未识别<\/span><span[^>]*>2<\/span>/);
|
|
expect(html).toMatch(/已登记<\/span><span[^>]*>1<\/span>/);
|
|
for (const n of ["a.mkv", "b.mkv", "c.mkv"]) expect(html).toContain(n);
|
|
});
|
|
it("不给 groupBy 时没有组头", () => {
|
|
const html = renderToStaticMarkup(
|
|
createElement(DataTable<Row>, {
|
|
columns: [{ key: "name", header: "名称", cell: (r: Row) => r.name }],
|
|
rows,
|
|
rowKey: (r: Row) => r.id,
|
|
}),
|
|
);
|
|
expect(html).not.toContain('aria-expanded="true"');
|
|
});
|
|
});
|