update
This commit is contained in:
65
scripts/_lib/api-stub.mjs
Normal file
65
scripts/_lib/api-stub.mjs
Normal file
@@ -0,0 +1,65 @@
|
||||
// 跨脚本共享:window.api 桩。
|
||||
//
|
||||
// 用途:TaskStore / SettingsStore 等模块在模块加载时读 window.api.*,没有 IPC
|
||||
// preload 环境时直接抛 undefined。之前 4+ 个脚本各自复制一份 6 行 stub,
|
||||
// 容易漏字段或拼写漂移。
|
||||
//
|
||||
// 用法:
|
||||
// import { installApiStub } from './_lib/api-stub.mjs';
|
||||
// installApiStub(); // 默认桩
|
||||
// installApiStub({ // 自定义部分方法
|
||||
// writeFile: async () => ({ ok: true }),
|
||||
// });
|
||||
// // 测试中要临时改某个方法:
|
||||
// globalThis.window.api.readFile = async () => ({ ok: true, content: '...' });
|
||||
|
||||
/**
|
||||
* 默认 stub:所有方法返回「无害成功」值,让 TaskStore / SettingsStore 不抛错。
|
||||
*
|
||||
* 默认实现按「最小可运行」原则写:writeFile/readFile 返回 {ok:true},on* 返回
|
||||
* noop unsubscribe,notify* 返回 undefined。
|
||||
* 需要断言具体调用的脚本可以覆盖。
|
||||
*/
|
||||
export function installApiStub(overrides = {}) {
|
||||
globalThis.window = globalThis.window || {};
|
||||
const api = {
|
||||
// 文件 IO —— TaskStore._autoSave / file-* IPC 用
|
||||
writeFile: async () => ({ ok: true }),
|
||||
readFile: async () => ({ ok: true, content: '' }),
|
||||
fileExists: async () => true,
|
||||
createIfMissing: async () => ({ ok: true }),
|
||||
createSnapshot: async () => ({ ok: true, snapshotPath: '' }),
|
||||
|
||||
// 事件订阅 —— on* 模式返回 unsubscribe
|
||||
onFileExternalChange: () => () => {},
|
||||
onMenuCommand: () => () => {},
|
||||
onFlushPendingSave: () => () => {},
|
||||
onAlwaysOnTopChanged: () => () => {},
|
||||
onMaximizeStateChanged: () => () => {},
|
||||
onDirtyChanged: () => () => {},
|
||||
|
||||
// 单向通知 —— preload 提供给 renderer 主动调
|
||||
notifyDirtyChanged: () => {},
|
||||
notifyFlushDone: () => {},
|
||||
|
||||
// 设置存储
|
||||
getSettings: async () => ({}),
|
||||
saveSettings: async () => ({}),
|
||||
|
||||
// 窗口控制
|
||||
setAlwaysOnTop: async () => true,
|
||||
minimizeWindow: async () => true,
|
||||
toggleMaximizeWindow: async () => true,
|
||||
closeWindow: async () => true,
|
||||
|
||||
// 应用信息
|
||||
getDataDir: async () => '',
|
||||
getDefaultDataDir: async () => '',
|
||||
getVersion: async () => '0.0.0-test',
|
||||
getElectronVersion: async () => null,
|
||||
|
||||
...overrides,
|
||||
};
|
||||
globalThis.window.api = api;
|
||||
return api;
|
||||
}
|
||||
83
scripts/_lib/check.mjs
Normal file
83
scripts/_lib/check.mjs
Normal file
@@ -0,0 +1,83 @@
|
||||
// 跨脚本共享:统一的断言 / 计数 / 退出 helper。
|
||||
//
|
||||
// 之前散落在 ~25 个脚本里的断言模式有 3 种:
|
||||
// 1) check(name, cond, detail='') + pass/fail 计数(大多数)
|
||||
// 2) ok(name) / fail(name, msg) + failed 计数(少数)
|
||||
// 3) 自定义 ok/bad + 自己的计数器(settings / backup / status-meta)
|
||||
//
|
||||
// 这里统一提供:
|
||||
// - check(name, cond, detail?) —— 主流风格:条件断言,detail 是失败时的诊断
|
||||
// - ok(name) / bad(name, msg) —— 风格 2 / 3 的兼容入口(适合「手动构造失败信息」)
|
||||
// - printSummary(label) —— 打印「通过 X / 失败 Y」并按需 exit
|
||||
// - summary 对象 —— 共享计数器,避免每个脚本各定义 pass/fail
|
||||
//
|
||||
// 用法:
|
||||
// import { check, ok, bad, summary, printSummary } from './_lib/check.mjs';
|
||||
// check('某断言', cond, '可选失败详情');
|
||||
// ...
|
||||
// printSummary('check-batch-ops');
|
||||
|
||||
export const summary = { pass: 0, fail: 0 };
|
||||
|
||||
/**
|
||||
* 主流断言:cond 为真则通过,否则打印失败 + 可选详情。
|
||||
* @param {string} name
|
||||
* @param {boolean} cond
|
||||
* @param {string} [detail]
|
||||
*/
|
||||
export function check(name, cond, detail = '') {
|
||||
if (cond) {
|
||||
summary.pass++;
|
||||
console.log(` ✓ ${name}`);
|
||||
} else {
|
||||
summary.fail++;
|
||||
const tail = detail ? ` — ${detail}` : '';
|
||||
console.log(` ✗ ${name}${tail}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显式 ok:用于「不需要断言条件,直接报告通过」的场景。
|
||||
* 兼容旧脚本里的 ok() 风格。
|
||||
* @param {string} name
|
||||
*/
|
||||
export function ok(name) {
|
||||
summary.pass++;
|
||||
console.log(` ✓ ${name}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 显式 bad:用于「手动构造失败信息」的场景(错误细节比条件断言更复杂时)。
|
||||
* 兼容旧脚本里的 fail()/bad() 风格。
|
||||
* @param {string} name
|
||||
* @param {string} [msg]
|
||||
*/
|
||||
export function bad(name, msg = '') {
|
||||
summary.fail++;
|
||||
const tail = msg ? `: ${msg}` : '';
|
||||
console.log(` ✗ ${name}${tail}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 打印「通过 X / 失败 Y」总结行。
|
||||
* exitOnFail=true 时若有失败则以 exit 1 退出(脚本默认应这么用)。
|
||||
*
|
||||
* @param {string} [label] - 总结行前的标签,例如脚本名
|
||||
* @param {boolean} [exitOnFail=true]
|
||||
*/
|
||||
export function printSummary(label = '', exitOnFail = true) {
|
||||
const head = label ? `${label} — ` : '';
|
||||
console.log(`\n${head}通过 ${summary.pass} / 失败 ${summary.fail}`);
|
||||
if (exitOnFail && summary.fail > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置计数器 —— 脚本里想做多轮隔离断言时调用。
|
||||
* 大多数脚本不需要:进程结束就 exit,counter 自然释放。
|
||||
*/
|
||||
export function resetSummary() {
|
||||
summary.pass = 0;
|
||||
summary.fail = 0;
|
||||
}
|
||||
79
scripts/_lib/find-todo-path.mjs
Normal file
79
scripts/_lib/find-todo-path.mjs
Normal file
@@ -0,0 +1,79 @@
|
||||
// 跨脚本共享:找用户的真实 todo.md 路径并读取内容。
|
||||
//
|
||||
// 解决 user memory 里反复提到的硬编码路径问题:
|
||||
// - 之前 `scripts/check-*.mjs` / `verify-*.mjs` 直接写死 `c:/Users/guan/TodoList/todo.md`
|
||||
// 在别人的机器 / CI 必挂;
|
||||
// - 这套查找链按优先级收敛:CLI `--file` → `TODO_LIST_FILE` 环境变量 → 平台约定
|
||||
// (用户主目录下的 ~/TodoList/todo.md,跨平台统一)。
|
||||
//
|
||||
// 用法:
|
||||
// import { findTodoFile } from './_lib/find-todo-path.mjs';
|
||||
// const { path: todoPath, content } = findTodoFile() ?? {};
|
||||
// if (!todoPath) { /* 走内置 SAMPLE 兜底 */ }
|
||||
//
|
||||
// 返回 `null`(找不到任何候选)让调用方决定降级策略 —— 通常是回落到内置 SAMPLE 跑,
|
||||
// 与「脚本不能因为路径在别人机器上不一样就硬挂」的回归策略一致。
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
/**
|
||||
* 找用户的真实 todo.md。
|
||||
* @returns {{ path: string, content: string } | null}
|
||||
*/
|
||||
export function findTodoFile() {
|
||||
// 1) 命令行 --file=xxx / --file xxx 优先(CI / 调试覆盖)
|
||||
const argv = process.argv.slice(2);
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const a = argv[i];
|
||||
if (a === '--file' && argv[i + 1]) {
|
||||
const p = argv[i + 1];
|
||||
try {
|
||||
return { path: p, content: readFileSync(p, 'utf8') };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (a.startsWith('--file=')) {
|
||||
const p = a.slice('--file='.length);
|
||||
try {
|
||||
return { path: p, content: readFileSync(p, 'utf8') };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2) 环境变量(脚本间共享 —— e.g. CI 全局指定一个 todo.md)
|
||||
const envPath = process.env.TODO_LIST_FILE;
|
||||
if (envPath) {
|
||||
try {
|
||||
return { path: envPath, content: readFileSync(envPath, 'utf8') };
|
||||
} catch {
|
||||
/* 文件被删 / 权限不足 —— 继续往下找 */
|
||||
}
|
||||
}
|
||||
|
||||
// 3) 平台约定:用户主目录下的 TodoList(~/TodoList)。
|
||||
//
|
||||
// 注意:不要用 HOMEDRIVE 单拼 —— `HOMEDRIVE=C:` + 不带 HOMEPATH 会得到
|
||||
// `C:\TodoList\todo.md`(C: 盘根目录下的 TodoList),完全不是用户的 home。
|
||||
// 应该走 USERPROFILE / HOME 这类已经合并好 drive + path 的环境变量。
|
||||
const home = process.env.USERPROFILE || process.env.HOME || '';
|
||||
const candidates = [];
|
||||
if (home) {
|
||||
if (process.platform === 'win32') {
|
||||
candidates.push(`${home}\\TodoList\\todo.md`, `${home}/TodoList/todo.md`);
|
||||
} else {
|
||||
candidates.push(`${home}/TodoList/todo.md`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const p of candidates) {
|
||||
try {
|
||||
return { path: p, content: readFileSync(p, 'utf8') };
|
||||
} catch {
|
||||
/* 跳过不存在的候选 */
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
36
scripts/_lib/store-fixture.mjs
Normal file
36
scripts/_lib/store-fixture.mjs
Normal file
@@ -0,0 +1,36 @@
|
||||
// 跨脚本共享:构造 TaskStore 测试 fixture。
|
||||
//
|
||||
// 之前 ~20 个脚本里复制粘贴 makeStore(),三套签名(必填 md / 可选 md / 无参)
|
||||
// 实质等价:建 store → loadDefault 或 loadFromContent → 禁掉 autoSave。
|
||||
//
|
||||
// 统一为 makeStore(markdown = null, options = {}):
|
||||
// - markdown = null/不传 = 走 loadDefault()(默认文档)
|
||||
// - markdown = 字符串 = 走 loadFromContent(markdown, null)(自定义内容)
|
||||
// - options.settingsStore = 一个 settingsStore 实例(桩或真),注入后 TaskStore
|
||||
// 才能读到 *Position 偏好;不传等价于未注入 → TaskStore 走 'front' 默认值。
|
||||
//
|
||||
// autoSave 必须禁掉:脚本里没有真实的 preload/IPC 环境,让 autoSave 跑会触发
|
||||
// 找不到 window.api 而抛错。所有脚本都做了相同的事——集中到 helper 里确保新人
|
||||
// 不用手动记得这一步。
|
||||
|
||||
import { TaskStore } from '../../src/task-store.js';
|
||||
|
||||
/**
|
||||
* 构造一个 TaskStore fixture。
|
||||
*
|
||||
* @param {string|null} [markdown=null] - 自定义 markdown;null = 用默认文档
|
||||
* @param {{ settingsStore?: object }} [options={}] - 注入的 settingsStore(可选)
|
||||
* @returns {TaskStore}
|
||||
*/
|
||||
export function makeStore(markdown = null, options = {}) {
|
||||
const store = new TaskStore(options);
|
||||
if (markdown !== null) {
|
||||
store.loadFromContent(markdown, null);
|
||||
} else {
|
||||
store.loadDefault(null);
|
||||
}
|
||||
// 关掉 autoSave 的副作用(避免污染测试环境 / 触发 IPC)。
|
||||
// 保留 .cancel() / .flush() 接口,调用方代码不需要做 null 检查。
|
||||
store._autoSave = Object.assign(() => {}, { cancel() {}, flush() {} });
|
||||
return store;
|
||||
}
|
||||
48
scripts/_lib/tmp-todo.mjs
Normal file
48
scripts/_lib/tmp-todo.mjs
Normal file
@@ -0,0 +1,48 @@
|
||||
// 跨脚本共享:临时 todo.md 文件的创建 + 清理。
|
||||
//
|
||||
// 用途:polling-fallback / snapshot-creation / write-race 等脚本要在隔离目录
|
||||
// 下模拟真实 todo.md 文件,每次都重复同样的 3 行 mkdtempSync+writeFileSync
|
||||
// boilerplate,且清理端 `fs.rmSync(tmpDir, { recursive, force })` 在 3 个脚本
|
||||
// 各自复制一份 —— 任何一处改成 `fs.rm` 或漏 `{ force: true }` 就会在 CI 上
|
||||
// 留下 tmp 目录。
|
||||
//
|
||||
// 用法:
|
||||
// import { makeTmpTodoFile, cleanupTmpTodo } from './_lib/tmp-todo.mjs';
|
||||
// const { dir, file } = makeTmpTodoFile({ prefix: 'poll-test-' });
|
||||
// // ... 测试 ...
|
||||
// cleanupTmpTodo({ dir });
|
||||
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
// 默认 seed:最小可工作的「全部任务」+「工作」分类 + 一条 a 任务。
|
||||
// 覆盖路径:如果脚本要不同初始内容,传 `content` override(snapshot-creation
|
||||
// 用 `# todo\n- [ ] A\n- [ ] B\n`;write-race 用 `## 工作\n\n- [ ] V0`)。
|
||||
const DEFAULT_SEED = '# 全部任务\n\n## 工作\n\n- [ ] a\n';
|
||||
|
||||
/**
|
||||
* 创建一个临时目录并写入 todo.md,返回 { dir, file }。
|
||||
*
|
||||
* @param {object} [opts]
|
||||
* @param {string} [opts.prefix='todo-test-'] — mkdtempSync 前缀,含连字符
|
||||
* @param {string} [opts.content] — 初始 markdown 内容;不传走 DEFAULT_SEED
|
||||
* @returns {{ dir: string, file: string }}
|
||||
*/
|
||||
export function makeTmpTodoFile({ prefix = 'todo-test-', content = DEFAULT_SEED } = {}) {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
||||
const file = path.join(dir, 'todo.md');
|
||||
fs.writeFileSync(file, content);
|
||||
return { dir, file };
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理临时目录。`force: true` 让目录不存在时也不抛 —— 测试中途崩了也能跑。
|
||||
*
|
||||
* @param {object} opts
|
||||
* @param {string} opts.dir
|
||||
*/
|
||||
export function cleanupTmpTodo({ dir }) {
|
||||
if (!dir) return;
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
Reference in New Issue
Block a user