feat(W4): 站点path全链路-portalUrlOf/previewPortalUrl纯函数,站点列/向导地址预览/预览frame sitePath,store携带path,mock嵌套路由,T-W4-16..18 绿51/51
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
// @vitest-environment jsdom
|
||||
/**
|
||||
* W4 向导地址预览联动(方案 §8.3 T-W4-18):
|
||||
* 第 3 步「门户地址预览」随上级与编码输入联动(校级 lockParent 模式预置上级=选上级)。
|
||||
* 挂载 SiteWizard(Modal transfer 到 body),桩掉主题三件套与配置表单。
|
||||
*/
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createApp, defineComponent, h, nextTick, ref } from "vue";
|
||||
import { createPinia } from "pinia";
|
||||
import ViewUIPlus from "view-ui-plus";
|
||||
|
||||
const themesMock = vi.hoisted(() => ({
|
||||
list: vi.fn(async () => [
|
||||
{
|
||||
name: "default",
|
||||
title: "云端默认",
|
||||
screenshotUrl: "/api/theme/default/asset/screenshot.png",
|
||||
params: [],
|
||||
},
|
||||
]),
|
||||
}));
|
||||
|
||||
vi.mock("@/api/module/cms", () => ({
|
||||
themeApi: { list: () => ({ send: themesMock.list }) },
|
||||
siteApi: { save: () => ({ send: async () => ({}) }) },
|
||||
}));
|
||||
|
||||
vi.mock("@/components/theme/ThemePicker.vue", () => ({
|
||||
default: defineComponent({
|
||||
props: ["themes", "modelValue"],
|
||||
emits: ["update:modelValue", "pick"],
|
||||
setup(_, { emit }) {
|
||||
(globalThis as any).__wizardPickTheme = (name: string) => emit("pick", name);
|
||||
return () => h("div", { class: "theme-picker-stub" }, "themes");
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/theme/ThemeParamForm.vue", () => ({
|
||||
default: defineComponent({ props: ["modelValue"], emits: ["update:modelValue"], render: () => null }),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/theme/ThemePreviewFrame.vue", () => ({
|
||||
default: defineComponent({ props: ["url", "tick"], render: () => h("div", "preview-stub") }),
|
||||
}));
|
||||
|
||||
vi.mock("@/views/cms/components/SiteConfigForm.vue", () => ({
|
||||
default: defineComponent({
|
||||
props: ["modelValue"],
|
||||
methods: { validate: async () => true },
|
||||
render: () => null,
|
||||
}),
|
||||
}));
|
||||
|
||||
import SiteWizard from "@/views/cms/SiteWizard.vue";
|
||||
|
||||
let cleanups: (() => void)[] = [];
|
||||
|
||||
async function flush(frames = 6) {
|
||||
for (let i = 0; i < frames; i++) {
|
||||
await nextTick();
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 0));
|
||||
}
|
||||
|
||||
function mountWizard(lockParentId?: number, secondMain = false) {
|
||||
const sites = [
|
||||
{ id: 1, code: "sch-3", path: "sch-3", parentSiteId: 0, schId: 1, schName: "第一中学", name: "第一中学门户", status: 1 },
|
||||
...(secondMain
|
||||
? [{ id: 2, code: "sch-8", path: "sch-8", parentSiteId: 0, schId: 2, schName: "第八中学", name: "第八中学门户", status: 1 }]
|
||||
: []),
|
||||
];
|
||||
const visible = ref(false); // 挂载后翻转以触发 SiteWizard 的 visible watch(真实打开路径)
|
||||
const pinia = createPinia();
|
||||
const el = document.createElement("div");
|
||||
document.body.appendChild(el);
|
||||
const app = createApp(defineComponent({
|
||||
setup() {
|
||||
return () => h(SiteWizard, {
|
||||
visible: visible.value,
|
||||
sites,
|
||||
lockParentId,
|
||||
"onUpdate:visible": (v: boolean) => { visible.value = v; },
|
||||
});
|
||||
},
|
||||
}));
|
||||
app.use(pinia);
|
||||
app.use(ViewUIPlus);
|
||||
// SiteWizard 第 2 步的 FormItem 不在 Form 内(现状如此),jsdom 卸载时 view-ui-plus
|
||||
// 内部 removeField/colon 读取 undefined 崩溃 → 本测试用展示桩覆盖 FormItem
|
||||
app.component("FormItem", defineComponent({
|
||||
props: ["label", "prop", "labelWidth"],
|
||||
setup(props, { slots }) {
|
||||
return () => h("div", { class: "form-item-stub" }, [
|
||||
props.label ? h("label", props.label) : null,
|
||||
slots.default?.(),
|
||||
]);
|
||||
},
|
||||
}));
|
||||
app.mount(el);
|
||||
visible.value = true;
|
||||
cleanups.push(() => {
|
||||
app.unmount();
|
||||
el.remove();
|
||||
document.querySelectorAll(".ivu-modal-wrap").forEach(n => n.remove());
|
||||
});
|
||||
return el;
|
||||
}
|
||||
|
||||
async function gotoStep3() {
|
||||
// step1 → step2 → step3(footer 按钮 transfer 到 body)
|
||||
const clickNext = async () => {
|
||||
await flush();
|
||||
const btn = [...document.querySelectorAll(".ivu-modal-footer button")]
|
||||
.find(b => b.textContent?.includes("下一步"));
|
||||
btn!.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
await flush();
|
||||
};
|
||||
await clickNext();
|
||||
await clickNext();
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
afterEach(() => {
|
||||
cleanups.forEach(fn => fn());
|
||||
cleanups = [];
|
||||
});
|
||||
|
||||
describe("SiteWizard 门户地址预览(T-W4-18)", () => {
|
||||
it("选上级(lockParent 预置 sch-3)+ 输入 jdxy → 预览 /portal/sch-3/jdxy/", async () => {
|
||||
mountWizard(1);
|
||||
await gotoStep3();
|
||||
|
||||
const input = document.querySelector('.ivu-modal input[placeholder="URL 路径用,如 demo"]') as HTMLInputElement;
|
||||
expect(input).toBeTruthy();
|
||||
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")!.set!;
|
||||
setter.call(input, "jdxy");
|
||||
input.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
await flush();
|
||||
|
||||
expect(document.body.textContent).toContain("门户地址预览");
|
||||
expect(document.body.textContent).toContain("/portal/sch-3/jdxy/");
|
||||
});
|
||||
|
||||
it("改上级即变:换到另一主站 sch-8 → 预览 /portal/sch-8/jdxy/", async () => {
|
||||
mountWizard(2, true);
|
||||
await gotoStep3();
|
||||
|
||||
const input = document.querySelector('.ivu-modal input[placeholder="URL 路径用,如 demo"]') as HTMLInputElement;
|
||||
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")!.set!;
|
||||
setter.call(input, "jdxy");
|
||||
input.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
await flush();
|
||||
|
||||
expect(document.body.textContent).toContain("/portal/sch-8/jdxy/");
|
||||
expect(document.body.textContent).not.toContain("/portal/sch-3/jdxy/");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user