Files
90CNServer/pvf/frontend/tests/autosave.test.ts
T
hanfuit 645b64fe21 DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
2026-11-04 14:31:55 +08:00

252 lines
8.1 KiB
TypeScript

import { beforeEach, expect, test, vi } from "vitest";
import { createPinia, setActivePinia } from "pinia";
const api = vi.hoisted(() => ({
CreateSnapshot: vi.fn(),
Status: vi.fn(),
PendingRecovery: vi.fn(),
Restore: vi.fn(),
Discard: vi.fn(),
DropForWorkspace: vi.fn(),
ChooseCachePath: vi.fn(),
saveAllDirty: vi.fn(),
}));
const state = vi.hoisted(() => ({
archive: { open: true, loading: false, modifiedCount: 0, info: null as unknown },
editor: { dirtyCount: 0, saving: false },
version: { status: { loading: false, changedFiles: 0, needsSave: false } },
settings: { autosaveEnabled: true, autosaveIntervalSeconds: 300, autosavePath: "" },
fileSets: { dirty: false },
script: { workspaceDetached: false, dirty: false },
}));
vi.mock("@wailsio/runtime", () => ({ Events: { On: vi.fn() } }));
vi.mock("../bindings/pvfine/services", () => ({ AutosaveService: api }));
vi.mock("../src/stores/archive", () => ({ useArchiveStore: () => state.archive }));
vi.mock("../src/stores/editor", () => ({ useEditorStore: () => Object.assign(state.editor, { saveAllDirty: api.saveAllDirty }) }));
vi.mock("../src/stores/version", () => ({ useVersionStore: () => state.version }));
vi.mock("../src/stores/fileSets", () => ({ useFileSetStore: () => state.fileSets }));
vi.mock("../src/stores/script", () => ({ useScriptStore: () => state.script }));
vi.mock("../src/stores/settings", () => ({
useSettingsStore: () => state.settings,
minAutosaveIntervalMinutes: 1,
maxAutosaveIntervalMinutes: 120,
defaultAutosaveIntervalSeconds: 300,
}));
import { useAutosaveStore } from "../src/stores/autosave";
import { setDialogBusy } from "../src/dialogBusy";
function deferred<T>() {
let resolve!: (value: T) => void;
let reject!: (reason: Error) => void;
const promise = new Promise<T>((yes, no) => {
resolve = yes;
reject = no;
});
return { promise, resolve, reject };
}
const statusPayload = {
enabled: true,
path: "/tmp/autosave.pvf",
defaultPath: "/cache/pvfine/autosave.pvf",
exists: false,
cachedAt: 0,
sourcePath: "",
sizeBytes: 0,
running: false,
lastError: "",
};
beforeEach(() => {
setActivePinia(createPinia());
vi.clearAllMocks();
// 惰性转发:假定时器替换 globalThis 后 window 上的实现同步生效。
vi.stubGlobal("window", {
setInterval: (...args: Parameters<typeof setInterval>) => globalThis.setInterval(...args),
clearInterval: (handle: number) => globalThis.clearInterval(handle),
});
state.archive.open = true;
state.archive.loading = false;
state.archive.modifiedCount = 0;
state.archive.info = null;
state.editor.dirtyCount = 0;
state.editor.saving = false;
state.version.status.loading = false;
state.settings.autosaveEnabled = true;
state.settings.autosaveIntervalSeconds = 300;
api.Status.mockResolvedValue({ ...statusPayload });
api.PendingRecovery.mockResolvedValue(null);
api.CreateSnapshot.mockResolvedValue(undefined);
api.Discard.mockResolvedValue(true);
api.DropForWorkspace.mockResolvedValue(undefined);
api.saveAllDirty.mockResolvedValue(1);
});
test("干净工作区与关闭开关都不写缓存", async () => {
const store = useAutosaveStore();
await store.tick();
expect(api.CreateSnapshot).not.toHaveBeenCalled();
state.settings.autosaveEnabled = false;
state.archive.modifiedCount = 3;
await store.tick();
expect(api.CreateSnapshot).not.toHaveBeenCalled();
});
test("有改动时先提交编辑器草稿再快照", async () => {
const store = useAutosaveStore();
state.editor.dirtyCount = 2;
await store.tick();
expect(api.saveAllDirty).toHaveBeenCalledTimes(1);
expect(api.CreateSnapshot).toHaveBeenCalledTimes(1);
expect(store.lastSnapshotAt).toBeGreaterThan(0);
expect(store.lastError).toBe("");
});
test("保存中、版本加载中或没有归档时跳过", async () => {
const store = useAutosaveStore();
state.archive.modifiedCount = 1;
state.editor.saving = true;
await store.tick();
state.editor.saving = false;
state.version.status.loading = true;
await store.tick();
state.version.status.loading = false;
state.archive.open = false;
await store.tick();
expect(api.CreateSnapshot).not.toHaveBeenCalled();
});
test("快照失败只记录错误,不抛出", async () => {
const store = useAutosaveStore();
state.archive.modifiedCount = 1;
api.CreateSnapshot.mockRejectedValue(new Error("磁盘已满"));
await store.tick();
expect(store.lastError).toBe("磁盘已满");
expect(store.running).toBe(false);
});
test("定时器按设置的间隔触发快照", async () => {
vi.useFakeTimers();
try {
const store = useAutosaveStore();
state.settings.autosaveIntervalSeconds = 60;
state.archive.modifiedCount = 1;
store.restartTimer();
await vi.advanceTimersByTimeAsync(59_000);
expect(api.CreateSnapshot).not.toHaveBeenCalled();
await vi.advanceTimersByTimeAsync(1_000);
expect(api.CreateSnapshot).toHaveBeenCalledTimes(1);
store.stopTimer();
} finally {
vi.useRealTimers();
}
});
test("启动检查待恢复备份并可用恢复/丢弃处理", async () => {
const store = useAutosaveStore();
const pending = {
sourcePath: "/game/Script.pvf",
sourceName: "Script.pvf",
cachedAt: 100,
sizeBytes: 2048,
pendingFiles: 2,
sourceExists: true,
sourceChanged: false,
};
api.PendingRecovery.mockResolvedValue(pending);
expect(await store.checkRecovery()).toEqual(pending);
const restored = { path: "/game/Script.pvf", modifiedCount: 2 };
api.Restore.mockResolvedValue(restored);
expect(await store.restore()).toBe(true);
expect(store.pendingRecovery).toBeNull();
expect(state.archive.info).toEqual(restored);
expect(await store.discard()).toBe(true);
await store.discardForWorkspace();
expect(api.DropForWorkspace).toHaveBeenCalled();
});
test("恢复失败时错误向上抛出并保留待恢复信息", async () => {
const store = useAutosaveStore();
store.pendingRecovery = {
sourcePath: "/game/Script.pvf",
sourceName: "Script.pvf",
cachedAt: 100,
sizeBytes: 2048,
pendingFiles: 1,
sourceExists: false,
sourceChanged: false,
};
api.Restore.mockRejectedValue(new Error("原文件不存在,请先找回文件再恢复备份: /game/Script.pvf"));
await expect(store.restore()).rejects.toThrow("原文件不存在");
expect(store.pendingRecovery).not.toBeNull();
expect(store.lastError).toContain("原文件不存在");
});
test("恢复期间暴露 restoring 状态,重复调用直接忽略", async () => {
const store = useAutosaveStore();
const pending = deferred<unknown>();
api.Restore.mockReturnValue(pending.promise);
const first = store.restore();
expect(store.restoring).toBe(true);
expect(await store.restore()).toBe(false);
expect(api.Restore).toHaveBeenCalledTimes(1);
pending.resolve({ path: "/game/Script.pvf", modifiedCount: 1 });
expect(await first).toBe(true);
expect(store.restoring).toBe(false);
});
test("恢复弹窗忙碌态锁定关闭入口并切换按钮文案", () => {
const instance: any = {
loading: false,
positiveText: "恢复",
negativeButtonProps: {},
closable: true,
maskClosable: true,
closeOnEsc: true,
};
setDialogBusy(instance, true, "恢复", "正在恢复…");
expect(instance).toMatchObject({
loading: true,
positiveText: "正在恢复…",
negativeButtonProps: { disabled: true },
closable: false,
maskClosable: false,
closeOnEsc: false,
});
setDialogBusy(instance, false, "恢复");
expect(instance).toMatchObject({
loading: false,
positiveText: "恢复",
negativeButtonProps: { disabled: false },
closable: true,
maskClosable: true,
closeOnEsc: true,
});
});
test("initialize 只检查一次恢复并启动定时器", async () => {
vi.useFakeTimers();
try {
const store = useAutosaveStore();
state.archive.modifiedCount = 1;
api.PendingRecovery.mockResolvedValue(null);
await store.initialize();
await store.initialize();
expect(api.PendingRecovery).toHaveBeenCalledTimes(1);
await vi.advanceTimersByTimeAsync(300_000);
expect(api.CreateSnapshot).toHaveBeenCalled();
store.stopTimer();
} finally {
vi.useRealTimers();
}
});