feat(theme): S5 学院蓝主题改造(管理端)——homeLayout编辑器/主题参数表单/站点设置/类型 + 单测
build / build (push) Successful in 2m19s
build / build (push) Successful in 2m19s
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
// @vitest-environment jsdom
|
||||
/**
|
||||
* 首页布局编辑器(v2 工作项 B / 手册 §2.8)挂载回归:T-B-09——
|
||||
* quick_links 的属性面板必须是「数组编辑器」(每行 name + url,可增删、受 12 项上限约束),
|
||||
* notice_bar 必须是正文 + 可选跳转 + level 选择;序列行摘要走 blockSummary 新增分支。
|
||||
* 沿用 siteSetting.spec.ts 的手写 createApp + vi.mock 桩风格。
|
||||
*/
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { createApp, defineComponent, h, nextTick, ref } from "vue";
|
||||
import ViewUIPlus from "view-ui-plus";
|
||||
|
||||
Element.prototype.scrollIntoView = Element.prototype.scrollIntoView || (() => {});
|
||||
|
||||
vi.mock("@/api/module/cms", () => ({
|
||||
categoryApi: { tree: () => ({ send: async () => [] }) },
|
||||
}));
|
||||
|
||||
import HomeLayoutEditor from "@/views/cms/homeLayout/HomeLayoutEditor.vue";
|
||||
|
||||
const cleanups: (() => void)[] = [];
|
||||
|
||||
/** 排空微任务 + 一个宏任务(view-ui-plus 的 Select 标签回填走 nextTick/setTimeout) */
|
||||
async function settle(frames = 4) {
|
||||
for (let i = 0; i < frames; i++) {
|
||||
await nextTick();
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 0));
|
||||
for (let i = 0; i < frames; i++) {
|
||||
await nextTick();
|
||||
}
|
||||
}
|
||||
|
||||
const CONFIG = JSON.stringify({
|
||||
logo: "/a.png",
|
||||
home_layout: {
|
||||
version: 1,
|
||||
blocks: [
|
||||
{ id: "q1", type: "quick_links", items: [{ name: "办事大厅", url: "https://bsch.edu.cn/hall" }] },
|
||||
{ id: "n1", type: "notice_bar", text: "国庆放假安排", url: "/notice/gq", level: "warn" },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
async function mountEditor(configJson: string) {
|
||||
const el = document.createElement("div");
|
||||
document.body.appendChild(el);
|
||||
const model = ref(configJson);
|
||||
const app = createApp(
|
||||
defineComponent({
|
||||
render: () => h(HomeLayoutEditor, { modelValue: model.value, "onUpdate:modelValue": (v: string) => (model.value = v) }),
|
||||
}),
|
||||
);
|
||||
app.use(ViewUIPlus);
|
||||
app.mount(el);
|
||||
cleanups.push(() => {
|
||||
app.unmount();
|
||||
el.remove();
|
||||
});
|
||||
await settle(6);
|
||||
return { el, model };
|
||||
}
|
||||
|
||||
/** 取属性面板中指定 label 的字段容器 */
|
||||
function fieldOf(el: HTMLElement, label: string): HTMLElement | null {
|
||||
return (
|
||||
Array.from(el.querySelectorAll<HTMLElement>(".hle-field")).find(
|
||||
f => f.querySelector(".hle-field-label")?.textContent?.trim() === label,
|
||||
) || null
|
||||
);
|
||||
}
|
||||
|
||||
/** 第 index 行的删除按钮 */
|
||||
function del(field: HTMLElement, index: number): HTMLElement {
|
||||
return field.querySelectorAll<HTMLElement>(".hle-link-del")[index];
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
cleanups.splice(0).forEach(fn => fn());
|
||||
});
|
||||
|
||||
describe("HomeLayoutEditor - quick_links / notice_bar 属性面板(T-B-09)", () => {
|
||||
it("quick_links:入口行编辑器渲染 name+url 双输入,添加/删除可用且回写 items", async () => {
|
||||
const { el, model } = await mountEditor(CONFIG);
|
||||
// 序列行摘要走 blockSummary 新增分支
|
||||
expect(el.textContent).toContain("1 个入口");
|
||||
expect(el.textContent).toContain("醒目提示");
|
||||
|
||||
// 选中 quick_links 行 → 属性面板出现入口列表编辑器
|
||||
(el.querySelectorAll(".hle-row")[0] as HTMLElement).click();
|
||||
await settle();
|
||||
const field = fieldOf(el as HTMLElement, "入口列表");
|
||||
expect(field).not.toBeNull();
|
||||
expect(field!.querySelectorAll(".hle-link-row").length).toBe(1);
|
||||
expect(field!.querySelectorAll(".hle-link-row input").length).toBe(2);
|
||||
const add = field!.querySelector(".hle-link-add") as HTMLElement;
|
||||
expect(add.textContent).toContain("添加入口");
|
||||
|
||||
add.click();
|
||||
await settle();
|
||||
expect(field!.querySelectorAll(".hle-link-row").length).toBe(2);
|
||||
// 新增行是空行:回写布局时空行由 cleanBlock 剔除,故提交形态仍只 1 项
|
||||
const blocks = JSON.parse(model.value).home_layout.blocks;
|
||||
expect(blocks[0].items).toEqual([{ name: "办事大厅", url: "https://bsch.edu.cn/hall" }]);
|
||||
// 其余键无损
|
||||
expect(JSON.parse(model.value).logo).toBe("/a.png");
|
||||
|
||||
// 删掉尾部的空行 → 回到 1 行,提交形态不变
|
||||
del(field!, 1).click();
|
||||
await settle();
|
||||
expect(field!.querySelectorAll(".hle-link-row").length).toBe(1);
|
||||
expect(JSON.parse(model.value).home_layout.blocks[0].items).toEqual([{ name: "办事大厅", url: "https://bsch.edu.cn/hall" }]);
|
||||
|
||||
// 再删唯一入口行 → 空列表(提交时 items 不落键,由后端校验拦下「至少 1 个入口」)
|
||||
del(field!, 0).click();
|
||||
await settle();
|
||||
expect(field!.querySelectorAll(".hle-link-row").length).toBe(0);
|
||||
expect(JSON.parse(model.value).home_layout.blocks[0].items).toBeUndefined();
|
||||
});
|
||||
|
||||
it("quick_links:12 项上限时添加按钮禁用", async () => {
|
||||
const items = Array.from({ length: 12 }, (_, i) => ({ name: `入口${i}`, url: `/e${i}` }));
|
||||
const config = JSON.stringify({ home_layout: { version: 1, blocks: [{ id: "q1", type: "quick_links", items }] } });
|
||||
const { el } = await mountEditor(config);
|
||||
(el.querySelectorAll(".hle-row")[0] as HTMLElement).click();
|
||||
await settle();
|
||||
const field = fieldOf(el as HTMLElement, "入口列表")!;
|
||||
expect(field.querySelectorAll(".hle-link-row").length).toBe(12);
|
||||
expect((field.querySelector(".hle-link-add") as HTMLButtonElement).disabled).toBe(true);
|
||||
});
|
||||
|
||||
it("notice_bar:正文输入放宽到 200 字、跳转可选、level 为 info/warn 选择器", async () => {
|
||||
const { el } = await mountEditor(CONFIG);
|
||||
(el.querySelectorAll(".hle-row")[1] as HTMLElement).click();
|
||||
await settle();
|
||||
const text = fieldOf(el as HTMLElement, "公告正文")!.querySelector("input")!;
|
||||
expect(text.getAttribute("maxlength")).toBe("200");
|
||||
expect(fieldOf(el as HTMLElement, "跳转地址")).not.toBeNull();
|
||||
const levelField = fieldOf(el as HTMLElement, "提示级别")!;
|
||||
expect(levelField.querySelector(".ivu-select")).not.toBeNull();
|
||||
expect(levelField.textContent).toContain("醒目");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user