mirror of
https://github.com/hectorqin/reader.git
synced 2026-11-12 14:01:00 +08:00
两个分支各自演进:一侧引入格式注册表、H5 客户端与 Android 外壳,另一侧 引入漫画目录书与逐页取图。冲突集中在扫描器、metadata、library 路由和 web 客户端,逐项按「两边能力都要保留」的原则合并。 合并后修复了 4 个被冲突掩盖的真实缺陷: - scanner 同时保留了两套目录书实现,类型检查与测试均无法通过;改为统一 走格式注册表,目录书在文件遍历之前发现,其内容不再重复入库。 - /manifest 的 files 对漫画目录书返回的是文件夹路径,客户端无法逐页取图; 改为由 manifest 派生真实页路径。 - 单个文件端点只认 book_files 行,漫画目录书的每一页都 404;改为目录书 额外认领其目录下的路径,并修正路径归一化使穿越尝试仍被拒绝。 - 目录书判定只看图片数量,会把夹着一本书的目录整体吞掉;改为注册表驱动 的判定,含书籍文件的目录始终是书架。 同时修复了 web 端两套测试运行器互相无法执行对方用例的问题(vitest 与 node:test 混跑会静默跳过),以及 web 侧 5 处类型错误。 Ref: #9
60 lines
2.2 KiB
TypeScript
60 lines
2.2 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { naturalCompare, sortByName } from '../src/formats/natural-sort.ts';
|
|
|
|
/**
|
|
* Getting page order wrong silently scrambles a comic, so these cases are
|
|
* regression tests for real naming conventions rather than synthetic examples.
|
|
*/
|
|
|
|
describe('naturalCompare', () => {
|
|
it('sorts numeric runs by value, not lexicographically', () => {
|
|
const names = ['page10.jpg', 'page2.jpg', 'page1.jpg', 'page100.jpg'];
|
|
expect(sortByName(names, (name) => name)).toEqual([
|
|
'page1.jpg',
|
|
'page2.jpg',
|
|
'page10.jpg',
|
|
'page100.jpg',
|
|
]);
|
|
});
|
|
|
|
it('sorts CJK chapter names with numeric suffixes correctly', () => {
|
|
const names = ['第10话', '第2话', '第1话'];
|
|
expect(sortByName(names, (name) => name)).toEqual(['第1话', '第2话', '第10话']);
|
|
});
|
|
|
|
it('orders names case-insensitively first, then deterministically', () => {
|
|
// `localeCompare` with base sensitivity reports these as equal, so the
|
|
// tie-break decides. All that matters is that the result is *stable*: an
|
|
// unstable comparator would let the browser shuffle an archive's pages
|
|
// between loads, which is the bug this whole module exists to prevent.
|
|
const first = naturalCompare('chapter', 'Chapter');
|
|
const second = naturalCompare('chapter', 'Chapter');
|
|
expect(first).toBe(second);
|
|
expect(naturalCompare('apple', 'banana')).toBeLessThan(0);
|
|
});
|
|
|
|
it('does not lose precision on a long zero-padded run', () => {
|
|
// Parsing `000000000000000001` as a float would give 1e-18 and break the
|
|
// comparison; it must be treated as the integer it is.
|
|
expect(naturalCompare('000000000000000001.jpg', '000000000000000002.jpg')).toBeLessThan(0);
|
|
});
|
|
|
|
it('orders a prefix before the longer name that starts with it', () => {
|
|
expect(naturalCompare('a', 'a1')).toBeLessThan(0);
|
|
});
|
|
|
|
it('treats equal names as equal so sort stays stable', () => {
|
|
expect(naturalCompare('x1', 'x1')).toBe(0);
|
|
});
|
|
|
|
it('handles mixed separators used by scan tools', () => {
|
|
const names = ['c_001.png', 'c_002.png', 'c_010.png', 'c_003.png'];
|
|
expect(sortByName(names, (name) => name)).toEqual([
|
|
'c_001.png',
|
|
'c_002.png',
|
|
'c_003.png',
|
|
'c_010.png',
|
|
]);
|
|
});
|
|
});
|