新增能力:Web 与 Desktop 支持新建和保存 Markdown;桌面端注册 .md/.markdown 打开程序,支持单实例文件转交、自定义主题目录以及窗口位置、尺寸和最大化状态的延迟原子持久化与屏幕越界修正。 界面与兼容性:保留打开 Markdown 为独立主操作,将新建、保存和主题目录收纳到更多菜单;Web 移除本地素材目录入口并明确默认不支持本地素材;新文档默认使用一级标题或首行合法化生成文件名,可不保存直接导出 PDF。 渲染修复:图片、Mermaid 和 ECharts 按文档顺序串行执行媒体分页回填,每个媒体元素至多重排一次;图片以限宽后的高度作为缩放基准,只缩放媒体主体并保持标题尺寸;ECharts 冻结为 SVG 图片后参与统一分页。 示例与文档:默认示例仅保留一张网络图片,增加有效 Base64 横图、竖图、边界图片及多尺寸 Mermaid/ECharts;更新根 README、Desktop README、部署文档、四个 packages README、PROGRESS 和 AGENTS 发布规范;登记 v0.4.6 超链接导航问题。 部署与验证:构建并运行 yixiong/md-to-pdf:v0.4.5 正式镜像,Compose 健康且 PDF 冒烟无 Mermaid/ECharts 错误;生成 NSIS 安装包和免安装 ZIP,本机已升级到 NSIS v0.4.5 并清理旧 Squirrel 安装;202 项测试、类型检查、生产构建和 git diff --check 全部通过。 发布产物:Setup.exe SHA-256 9CBD6362E15AA745185805E753D5A7749434E0E142E6CE0F66463E6A19468F1D;ZIP SHA-256 97B2D44E72F626A3396E5AB5B330FFB7F9BECAF945A1707808C876940A82E850;当前 Windows 产物未配置代码签名。
166 lines
3.7 KiB
TypeScript
166 lines
3.7 KiB
TypeScript
import { mkdtemp, readdir, rm } from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { describe, expect, it } from "vitest";
|
|
import {
|
|
loadWindowState,
|
|
parseWindowState,
|
|
resolveWindowBounds,
|
|
saveWindowState
|
|
} from "../src/window-state.js";
|
|
|
|
const primaryWorkArea = {
|
|
x: 0,
|
|
y: 0,
|
|
width: 1920,
|
|
height: 1040
|
|
};
|
|
const defaultSize = { width: 1440, height: 960 };
|
|
const minimumSize = { width: 1024, height: 720 };
|
|
|
|
describe("桌面窗口状态", () => {
|
|
it("校验并规范化持久化状态", () => {
|
|
expect(
|
|
parseWindowState({
|
|
version: 1,
|
|
bounds: {
|
|
x: 120.4,
|
|
y: 80.6,
|
|
width: 1280.2,
|
|
height: 800.8
|
|
},
|
|
maximized: true
|
|
})
|
|
).toEqual({
|
|
version: 1,
|
|
bounds: {
|
|
x: 120,
|
|
y: 81,
|
|
width: 1280,
|
|
height: 801
|
|
},
|
|
maximized: true
|
|
});
|
|
});
|
|
|
|
it("拒绝损坏或不受支持的状态", () => {
|
|
expect(parseWindowState(undefined)).toBeUndefined();
|
|
expect(
|
|
parseWindowState({
|
|
version: 2,
|
|
bounds: primaryWorkArea,
|
|
maximized: false
|
|
})
|
|
).toBeUndefined();
|
|
expect(
|
|
parseWindowState({
|
|
version: 1,
|
|
bounds: { x: 0, y: 0, width: -1, height: 720 },
|
|
maximized: false
|
|
})
|
|
).toBeUndefined();
|
|
});
|
|
|
|
it("首次启动时在主显示器居中", () => {
|
|
expect(
|
|
resolveWindowBounds(
|
|
undefined,
|
|
[primaryWorkArea],
|
|
primaryWorkArea,
|
|
defaultSize,
|
|
minimumSize
|
|
)
|
|
).toEqual({
|
|
x: 240,
|
|
y: 40,
|
|
width: 1440,
|
|
height: 960
|
|
});
|
|
});
|
|
|
|
it("将部分溢出的窗口完整收回原显示器", () => {
|
|
expect(
|
|
resolveWindowBounds(
|
|
{ x: 1700, y: 900, width: 1280, height: 800 },
|
|
[primaryWorkArea],
|
|
primaryWorkArea,
|
|
defaultSize,
|
|
minimumSize
|
|
)
|
|
).toEqual({
|
|
x: 640,
|
|
y: 240,
|
|
width: 1280,
|
|
height: 800
|
|
});
|
|
});
|
|
|
|
it("显示器移除后在主显示器居中恢复", () => {
|
|
expect(
|
|
resolveWindowBounds(
|
|
{ x: 3000, y: 120, width: 1200, height: 780 },
|
|
[primaryWorkArea],
|
|
primaryWorkArea,
|
|
defaultSize,
|
|
minimumSize
|
|
)
|
|
).toEqual({
|
|
x: 360,
|
|
y: 130,
|
|
width: 1200,
|
|
height: 780
|
|
});
|
|
});
|
|
|
|
it("窗口大于工作区时缩小到可完整展示", () => {
|
|
const compactWorkArea = {
|
|
x: 1920,
|
|
y: 0,
|
|
width: 1280,
|
|
height: 680
|
|
};
|
|
expect(
|
|
resolveWindowBounds(
|
|
{ x: 2000, y: -100, width: 1800, height: 1000 },
|
|
[primaryWorkArea, compactWorkArea],
|
|
primaryWorkArea,
|
|
defaultSize,
|
|
minimumSize
|
|
)
|
|
).toEqual({
|
|
x: 1920,
|
|
y: 0,
|
|
width: 1280,
|
|
height: 680
|
|
});
|
|
});
|
|
|
|
it("通过临时文件原子保存并可重新载入", async () => {
|
|
const directory = await mkdtemp(
|
|
path.join(os.tmpdir(), "md-to-pdf-window-")
|
|
);
|
|
const filePath = path.join(directory, "window-state.json");
|
|
const state = {
|
|
version: 1 as const,
|
|
bounds: { x: 10, y: 20, width: 1200, height: 800 },
|
|
maximized: false
|
|
};
|
|
try {
|
|
await saveWindowState(filePath, state);
|
|
await expect(loadWindowState(filePath)).resolves.toEqual(state);
|
|
const updatedState = {
|
|
...state,
|
|
bounds: { x: 30, y: 40, width: 1280, height: 900 },
|
|
maximized: true
|
|
};
|
|
await saveWindowState(filePath, updatedState);
|
|
await expect(loadWindowState(filePath)).resolves.toEqual(
|
|
updatedState
|
|
);
|
|
expect(await readdir(directory)).toEqual(["window-state.json"]);
|
|
} finally {
|
|
await rm(directory, { recursive: true, force: true });
|
|
}
|
|
});
|
|
});
|