146 lines
5.8 KiB
TypeScript
146 lines
5.8 KiB
TypeScript
// @vitest-environment jsdom
|
||
/**
|
||
* RichTextEditor 组件行为(审计批次 6:ArticleEdit 复用后,正文上传契约成为两个页面共用底座,
|
||
* 需要独立护栏)。覆盖:
|
||
* ① 上传 FormData 参数:siteId / scene / articleId 按传入拼装,未传的键不出现;
|
||
* ② 上传成功 → emit uploaded(附件 id) 且 insertFn 收到 (url, fileName, url);
|
||
* ③ 上传失败(code≠0)→ 不 emit uploaded;
|
||
* ④ 工具栏配置仍排除视频组与全屏(Notice 趟坑口径)。
|
||
*
|
||
* 无 @vue/test-utils 依赖:沿用项目既有 createApp 手写挂载 + vi.mock 桩范式。
|
||
*/
|
||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||
import { createApp, defineComponent, h } from "vue";
|
||
|
||
const confState = vi.hoisted(() => ({
|
||
conf: { baseHost: "", ssoHost: "", serviceHost: "", isMock: false, isDebug: false },
|
||
}));
|
||
|
||
vi.mock("@/assets/config/host.config", () => ({ default: confState.conf }));
|
||
|
||
/** wangEditor 桩:把传进来的 defaultConfig 挂到全局,测试借此直接驱动 customUpload */
|
||
vi.mock("@wangeditor/editor-for-vue", () => ({
|
||
Toolbar: defineComponent({
|
||
props: ["editor", "defaultConfig", "mode"],
|
||
setup(props) {
|
||
(globalThis as any).__toolbarConfig = props.defaultConfig;
|
||
return () => h("div", { class: "toolbar-stub" });
|
||
},
|
||
}),
|
||
Editor: defineComponent({
|
||
props: ["modelValue", "defaultConfig", "mode"],
|
||
emits: ["onCreated", "update:modelValue"],
|
||
setup(props, { emit }) {
|
||
(globalThis as any).__editorConfig = props.defaultConfig;
|
||
(globalThis as any).__editorEmitUpdate = (v: string) => emit("update:modelValue", v);
|
||
(globalThis as any).__editorCreated = emit;
|
||
return () => h("div", { class: "editor-stub" });
|
||
},
|
||
}),
|
||
}));
|
||
|
||
import RichTextEditor from "@/components/RichTextEditor.vue";
|
||
|
||
let cleanups: (() => void)[] = [];
|
||
|
||
function mountEditor(props: Record<string, unknown>, onUploaded?: (id: number) => void) {
|
||
const el = document.createElement("div");
|
||
document.body.appendChild(el);
|
||
const app = createApp({
|
||
render: () => h(RichTextEditor, { modelValue: "", ...props, onUploaded }),
|
||
});
|
||
app.mount(el);
|
||
cleanups.push(() => {
|
||
app.unmount();
|
||
el.remove();
|
||
});
|
||
return el;
|
||
}
|
||
|
||
/** 直接调用组件内部构造的 customUpload(等同 wangEditor 选中图片后的回调) */
|
||
async function runUpload(file: File): Promise<{ inserted: string[] }> {
|
||
const inserted: string[] = [];
|
||
const cfg = (globalThis as any).__editorConfig;
|
||
await cfg.MENU_CONF.uploadImage.customUpload(file, (url: string) => inserted.push(url));
|
||
return { inserted };
|
||
}
|
||
|
||
afterEach(() => {
|
||
cleanups.forEach(fn => fn());
|
||
cleanups = [];
|
||
vi.unstubAllGlobals();
|
||
delete (globalThis as any).__editorConfig;
|
||
delete (globalThis as any).__toolbarConfig;
|
||
});
|
||
|
||
describe("RichTextEditor - 正文上传与事件契约", () => {
|
||
it("① 上传 FormData 带 siteId/scene/articleId(scene 透传契约)", async () => {
|
||
const seen: FormData[] = [];
|
||
vi.stubGlobal(
|
||
"fetch",
|
||
vi.fn(async (_url: string, init: any) => {
|
||
seen.push(init.body as FormData);
|
||
return { json: async () => ({ code: 0, data: { id: 77, url: "/cms/upload/a.png", fileName: "a.png" } }) };
|
||
})
|
||
);
|
||
mountEditor({ siteId: 5, scene: "svc_item_asset" }, () => {});
|
||
await runUpload(new File(["x"], "a.png", { type: "image/png" }));
|
||
|
||
expect(seen).toHaveLength(1);
|
||
expect(seen[0].get("file")).toBeInstanceOf(File);
|
||
expect(seen[0].get("siteId")).toBe("5");
|
||
expect(seen[0].get("scene")).toBe("svc_item_asset");
|
||
expect(seen[0].get("articleId")).toBeNull();
|
||
});
|
||
|
||
it("①b 文章页口径:传 articleId 时随之提交(编辑已有文章)", async () => {
|
||
const seen: FormData[] = [];
|
||
vi.stubGlobal(
|
||
"fetch",
|
||
vi.fn(async (_url: string, init: any) => {
|
||
seen.push(init.body as FormData);
|
||
return { json: async () => ({ code: 0, data: { id: 78, url: "/cms/upload/b.png", fileName: "b.png" } }) };
|
||
})
|
||
);
|
||
mountEditor({ siteId: 5, articleId: 12 }, () => {});
|
||
await runUpload(new File(["x"], "b.png", { type: "image/png" }));
|
||
|
||
expect(seen[0].get("siteId")).toBe("5");
|
||
expect(seen[0].get("articleId")).toBe("12");
|
||
expect(seen[0].get("scene")).toBeNull();
|
||
});
|
||
|
||
it("② 上传成功:emit uploaded(id) 且 insertFn 收到 (url, fileName, url)", async () => {
|
||
vi.stubGlobal(
|
||
"fetch",
|
||
vi.fn(async () => ({
|
||
json: async () => ({ code: 0, data: { id: 99, url: "/cms/upload/c.png", fileName: "c.png" } }),
|
||
}))
|
||
);
|
||
const ids: number[] = [];
|
||
mountEditor({ siteId: 5, scene: "svc_item_asset" }, id => ids.push(id));
|
||
const { inserted } = await runUpload(new File(["x"], "c.png", { type: "image/png" }));
|
||
|
||
expect(ids).toEqual([99]);
|
||
expect(inserted).toEqual(["/cms/upload/c.png"]);
|
||
});
|
||
|
||
it("③ 上传失败(code≠0):不 emit uploaded、不插入图片", async () => {
|
||
vi.stubGlobal(
|
||
"fetch",
|
||
vi.fn(async () => ({ json: async () => ({ code: 2, msg: "scene 非法" }) }))
|
||
);
|
||
const ids: number[] = [];
|
||
mountEditor({ siteId: 5, scene: "bad_scene" }, id => ids.push(id));
|
||
const { inserted } = await runUpload(new File(["x"], "d.png", { type: "image/png" }));
|
||
|
||
expect(ids).toEqual([]);
|
||
expect(inserted).toEqual([]);
|
||
});
|
||
|
||
it("④ 工具栏仍排除视频组与全屏(Notice 趟坑口径不回退)", () => {
|
||
mountEditor({ siteId: 5 }, () => {});
|
||
expect((globalThis as any).__toolbarConfig.excludeKeys).toEqual(["group-video", "fullScreen"]);
|
||
});
|
||
});
|