Files
90CNServer/pvf/frontend/tests/gui-launcher.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

65 lines
3.8 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import { guiProviders, getGUIProvider } from "../src/gui/registry";
import { guiPathMatches, newGUIFilePath, searchGUIFiles } from "../src/gui/launcher";
import type { SearchHit, SearchResult } from "../bindings/pvfine/services/models";
const provider = (id: string) => guiProviders.find(item => item.id === id)!;
const hit = (path: string, fileIndex = 1) => ({ path, fileIndex, name: "测试" }) as SearchHit;
const result = (hits: SearchHit[], nextCursor = -1) => ({ hits, nextCursor }) as SearchResult;
describe("文件 GUI 工具入口", () => {
it("每个 GUI 都提供可识别的新建路径和类型搜索入口", () => {
expect(guiProviders.map(item => item.id)).toEqual(["shop", "world-drop", "skill", "quest", "upgrade", "forge", "item", "independent-drop", "vending-machine", "crane-minigame", "cera-shop"]);
for (const item of guiProviders) {
expect(item.launcher.queries.length).toBeGreaterThan(0);
for (const path of item.launcher.paths) {
expect(newGUIFilePath(item, path)).toBe(path);
expect(getGUIProvider({ index: 0, path, text: "", editable: true })?.id).toBe(item.id);
}
}
});
it("校验完整路径和准确文件类型,支持大小写及 Windows 分隔符", () => {
expect(newGUIFilePath(provider("shop"), " /NPC\\Shop\\NEW.SHP/ ")).toBe("NPC/Shop/NEW.SHP");
for (const path of ["", "npc//a.shp", "npc/../a.shp", "./a.shp", "npc/a.shp.txt", "npc/a.skl"]) {
expect(() => newGUIFilePath(provider("shop"), path)).toThrow();
}
expect(() => newGUIFilePath(provider("forge"), "etc/new.etc")).toThrow("支持的文件类型或路径");
expect(guiPathMatches(provider("world-drop"), "etc/otherworlddrop.etc")).toBe(false);
});
it("关键词结果跨过无关分页,并去重、保留后续游标", async () => {
const search = vi.fn()
.mockResolvedValueOnce(result([hit("npc/a.npc")], 100))
.mockResolvedValueOnce(result([hit("npc/a.shp"), hit("npc/a.shp")], 200));
const page = await searchGUIFiles(provider("shop"), "测试", { queryIndex: 0, offset: 0 }, search, () => true);
expect(page.hits.map(item => item.path)).toEqual(["npc/a.shp"]);
expect(page.next).toEqual({ queryIndex: 0, offset: 200 });
expect(search).toHaveBeenNthCalledWith(2, "测试", 100, 100);
});
it("空关键词依次搜索装备和消耗品,未注册文件也可按扩展名打开", async () => {
const search = vi.fn().mockResolvedValueOnce(result([hit("equipment/a.equ")])).mockResolvedValueOnce(result([hit("stackable/a.stk", 2)]));
const first = await searchGUIFiles(provider("item"), "", { queryIndex: 0, offset: 0 }, search, () => true);
expect(first.next).toEqual({ queryIndex: 1, offset: 0 });
const second = await searchGUIFiles(provider("item"), "", first.next!, search, () => true);
expect(second.hits[0]?.path).toBe("stackable/a.stk");
expect(second.next).toBeNull();
expect(search.mock.calls.map(call => call[0])).toEqual(["*.equ", "*.stk"]);
});
it("限定一次扫描量,保留继续搜索入口", async () => {
const search = vi.fn().mockImplementation(async (_query, cursor) => result([hit("other.txt")], cursor + 100));
const page = await searchGUIFiles(provider("quest"), "name", { queryIndex: 0, offset: 0 }, search, () => true);
expect(search).toHaveBeenCalledTimes(10);
expect(page.hits).toEqual([]);
expect(page.next).toEqual({ queryIndex: 0, offset: 1000 });
});
it("归档或查询变化时丢弃旧搜索结果并停止翻页", async () => {
const search = vi.fn().mockResolvedValue(result([hit("npc/a.shp")], 100));
expect(await searchGUIFiles(provider("shop"), "", { queryIndex: 0, offset: 0 }, search, () => false)).toEqual({ hits: [], next: null });
expect(search).toHaveBeenCalledTimes(1);
});
});