Files
Todo-List/scripts/check-external-change-path.mjs
2026-09-12 13:55:57 +08:00

218 lines
9.7 KiB
JavaScript
Raw Permalink 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.
// 测试 pathsEqual —— 外部修改检测路径比较的健壮性
//
// 背景:原实现 `changedPath !== store.filePath` 在 Windows 上永远失败:
// - store.filePath 由 joinPath 拼出forward slash `C:/Users/<user>/TodoList/todo.md`
// - IPC 发来的 changedPath 走 path.resolvebackslash`C:\\Users\\<user>\\TodoList\\todo.md`
// 结果是 resolveExternalChangeConflict 永远提前 return外部修改的 toast / reload
// 全部被静默吞掉 —— 用户看到「改了文件,软件没反应」。
//
// 这个测试覆盖各类路径等价场景,确保比较逻辑不会再退化。
//
// 注:测试 fixture 里出现的 `D:/TodoList` / `D:\\TodoList\\todo.md` 仅作 Windows
// 路径示例(验证 slash 方向、大小写不敏感、UNC 等行为),不指代实际默认目录。
import { pathsEqual } from '../src/utils/path.js';
import { check, ok, bad, summary, printSummary } from './_lib/check.mjs';
import { makeStore } from './_lib/store-fixture.mjs';
// ============================================================
// [1] Windows核心 bug —— slash 方向不同
// ============================================================
console.log('\n[1] Windows: 核心 bug —— slash 方向不同');
{
// 模拟 store.filePath (joinPath 输出)
const fwd = 'D:/TodoList/todo.md';
// 模拟主进程 IPC 发来的 changedPath (path.resolve 输出)
const bwd = 'D:\\TodoList\\todo.md';
check('forward slash vs backslash → 等价', pathsEqual(fwd, bwd),
`fwd=${fwd} bwd=${bwd}`);
check('反方向也一样', pathsEqual(bwd, fwd));
}
// ============================================================
// [2] Windows完全相同
// ============================================================
console.log('\n[2] Windows: 完全相同');
{
const a = 'D:/TodoList/todo.md';
check('同 forward slash → 等价', pathsEqual(a, a));
const b = 'D:\\TodoList\\todo.md';
check('同 backslash → 等价', pathsEqual(b, b));
}
// ============================================================
// [3] Windows大小写差异NTFS 默认大小写不敏感)
// ============================================================
console.log('\n[3] Windows: 大小写差异');
{
check('D:/TodoList vs d:/todolist → 等价',
pathsEqual('D:/TodoList/todo.md', 'd:/todolist/todo.md'));
check('混合大小写 vs 全大写 → 等价',
pathsEqual('C:/Users/Admin/Todo.md', 'C:/USERS/admin/TODO.MD'));
}
// ============================================================
// [4] Windows不同路径 → 不等价
// ============================================================
console.log('\n[4] Windows: 不同路径');
{
check('不同文件名 → 不等价',
!pathsEqual('D:/TodoList/foo.md', 'D:/TodoList/bar.md'));
check('不同目录 → 不等价',
!pathsEqual('D:/TodoA/foo.md', 'D:/TodoB/foo.md'));
check('不同盘符 → 不等价',
!pathsEqual('C:/TodoList/foo.md', 'D:/TodoList/foo.md'));
}
// ============================================================
// [5] POSIX完全相同
// ============================================================
console.log('\n[5] POSIX: 完全相同');
{
check('同路径 → 等价',
pathsEqual('/home/user/todo.md', '/home/user/todo.md'));
check('同路径反方向调用 → 等价',
pathsEqual('/home/user/todo.md', '/home/user/todo.md'));
}
// ============================================================
// [6] POSIX大小写不同 → 不等价POSIX 默认大小写敏感)
// ============================================================
console.log('\n[6] POSIX: 大小写敏感(保守策略)');
{
// 设计取舍:让 macOS / Linux 上的 TODO.md 和 todo.md 在用户角度确实可能是同一文件
// HFS+ / APFS 默认不敏感),但保守起见这里只把「带盘符」的 Windows 路径放宽,
// POSIX 路径严格区分大小写。误报比漏报代价小。
check('POSIX 路径大小写不同 → 不等价(保守)',
!pathsEqual('/home/user/Todo.md', '/home/user/todo.md'));
}
// ============================================================
// [7] null / undefined 处理
// ============================================================
console.log('\n[7] null / undefined');
{
// 实现选择null / undefined 走严格比较(`a === b`),不互通。
// 实际场景两边都是字符串joinPath / path.resolve 的产物),这条只是
// 「实现别在 null 上抛错」级别的健壮性保险,不需要语义层面的兼容性。
check('两个 null → 等价', pathsEqual(null, null));
check('两个 undefined → 等价', pathsEqual(undefined, undefined));
check('null vs undefined → 不等价(严格模式)', !pathsEqual(null, undefined));
check('null vs 字符串 → 不等价', !pathsEqual(null, 'D:/TodoList/todo.md'));
check('字符串 vs null → 不等价', !pathsEqual('D:/TodoList/todo.md', null));
check('undefined vs 字符串 → 不等价',
!pathsEqual(undefined, 'D:/TodoList/todo.md'));
}
// ============================================================
// [8] 空字符串
// ============================================================
console.log('\n[8] 空字符串');
{
check('两个空串 → 等价', pathsEqual('', ''));
check('空串 vs 字符串 → 不等价', !pathsEqual('', 'D:/TodoList/todo.md'));
check('字符串 vs 空串 → 不等价', !pathsEqual('D:/TodoList/todo.md', ''));
}
// ============================================================
// [9] UNC 路径Windows 网络共享):斜杠归一化后等价
// ============================================================
console.log('\n[9] UNC 路径');
{
// \\server\share\file.md 归一化后是 //server/share/file.md
// 两个写法backslash / forward归一化后一致 → 应等价
check('UNC 双反斜杠 vs 双正斜杠 → 等价',
pathsEqual('\\\\server\\share\\file.md', '\\\\server/share/file.md'));
}
// ============================================================
// [10] 核心回归:直接模拟 resolveExternalChangeConflict 的入参
// ============================================================
console.log('\n[10] 模拟 resolveExternalChangeConflict 的入参');
{
// 真实数据流app.js bootstrap 里 joinPath 拼出 filePath正斜杠
// 然后通过 readFile IPC 走到主进程,主进程 path.resolve 后回填到 currentFilePath反斜杠
// 之后 fs.watch / polling 检测到变化,发 'file:external-change' IPC 把 currentFilePath
// (反斜杠形态)送回渲染端。
const joinPathOut = 'D:/TodoList/todo.md';
const ipcPayload = 'D:\\TodoList\\todo.md';
// 旧实现的判断 `changedPath !== store.filePath`
// 两个字符串字符不同 → 不等 → 触发提前 return → bug 触发
const oldCheckSaysNotEqual = joinPathOut !== ipcPayload;
check('旧实现 !== 在 Windows 上判定为不等bug 触发)',
oldCheckSaysNotEqual === true,
`旧实现会让 resolveExternalChangeConflict 提前 return`);
// 新实现pathsEqual 把两条路径判定为相等
check('新实现 pathsEqual 判定为相等(不触发提前 return',
pathsEqual(joinPathOut, ipcPayload) === true);
// 验证 bug 已修复的核心对比
check('核心 bug 已修复slash 不同但路径相同能被识别',
pathsEqual('D:/TodoList/todo.md', 'D:\\TodoList\\todo.md'));
}
// ============================================================
// [11] macOS大小写不敏感APFS / HFS+ 默认)
// ============================================================
//
// 真实场景:用户在 macOS 上手敲 ~/TodoList/todo.md外部编辑器保存时
// 拼出 /users/admin/todolist/todo.md —— APFS / HFS+ 默认大小写不敏感,
// 两条路径指向同一文件。pathsEqual 必须能识别等价,否则外部修改会被吞。
console.log('\n[11] macOS: 大小写不敏感(通过 navigator.platform 模拟)');
{
// 在单测里塞 navigator.platform 模拟 macOS 环境
// Node 18+ 的 globalThis.navigator 是只读 getter需用 defineProperty。
const savedNav = globalThis.navigator;
Object.defineProperty(globalThis, 'navigator', {
value: { platform: 'MacIntel', userAgent: 'Mozilla/5.0 (Macintosh)' },
configurable: true,
writable: true
});
try {
check('macOS 下 /Users/Foo/Todo.md vs /users/foo/todo.md → 等价',
pathsEqual('/Users/Foo/Todo.md', '/users/foo/todo.md'));
check('macOS 下混合大小写也等价',
pathsEqual('/Users/Admin/Todo.md', '/USERS/admin/TODO.MD'));
check('macOS 下完全不同路径 → 不等价',
!pathsEqual('/Users/Foo/Todo.md', '/Users/Bar/Todo.md'));
} finally {
Object.defineProperty(globalThis, 'navigator', {
value: savedNav,
configurable: true,
writable: true
});
}
}
// ============================================================
// [12] macOS 探测:非 macOS 平台不会触发大小写不敏感
// ============================================================
console.log('\n[12] 非 macOS 平台POSIX 仍严格大小写敏感');
{
const savedNav = globalThis.navigator;
Object.defineProperty(globalThis, 'navigator', {
value: { platform: 'Linux x86_64', userAgent: 'Mozilla/5.0 (X11; Linux)' },
configurable: true,
writable: true
});
try {
check('Linux 下大小写不同 → 不等价',
!pathsEqual('/home/user/Todo.md', '/home/user/todo.md'));
check('Linux 下完全相同 → 等价',
pathsEqual('/home/user/todo.md', '/home/user/todo.md'));
} finally {
Object.defineProperty(globalThis, 'navigator', {
value: savedNav,
configurable: true,
writable: true
});
}
}
console.log(`\n通过 ${summary.pass} / 失败 ${summary.fail}`);
process.exit(summary.fail === 0 ? 0 : 1);