2026-10-28 11:22:52 +07:00
|
|
|
// @vitest-environment jsdom
|
|
|
|
|
// readContentControlFields (FR-11) — คุมว่าตอนสร้าง version อ่านค่า CC **ทุกชนิด**:
|
|
|
|
|
// host WordApi 1.5+ ต้องยิง Document.getContentControls() (เห็น plain text CC ซึ่งเป็น
|
|
|
|
|
// ส่วนใหญ่ของ template ศาล — เคสจริง T3 มี plain 26 + rich 1 แต่เดิมเหลือแค่ rich ตัว
|
|
|
|
|
// เดียวคือ detail1) ส่วน host ต่ำกว่า 1.3 ยังพังลง rich-text-only ตามเดิม (fallback
|
|
|
|
|
// ไม่ throw) + กติกา tag ว่างข้าม / tag ซ้ำเอาตัวแรกตามลำดับเอกสาร
|
|
|
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
|
|
|
|
|
|
|
|
import { readContentControlFields } from "@/wordaddin/services/metadataService";
|
|
|
|
|
|
|
|
|
|
interface FakeControl {
|
|
|
|
|
tag?: string;
|
|
|
|
|
text: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** stub Word + Office ระดับ global — คืน handle ให้ test ดึง collection ที่ถูกขอไปใช้ */
|
|
|
|
|
function stubWord(controls: FakeControl[], options?: { getAllTypes?: boolean }): {
|
|
|
|
|
documentControls: { load: (p: string) => void };
|
|
|
|
|
allTypesControls: { load: (p: string) => void };
|
|
|
|
|
} {
|
|
|
|
|
const makeCollection = () => ({
|
|
|
|
|
load: vi.fn(),
|
|
|
|
|
items: controls.map((control) => ({ tag: control.tag, text: control.text })),
|
|
|
|
|
});
|
|
|
|
|
const documentControls = makeCollection();
|
|
|
|
|
const allTypesControls = makeCollection();
|
|
|
|
|
vi.stubGlobal("Office", {
|
|
|
|
|
context: {
|
|
|
|
|
requirements: {
|
|
|
|
|
isSetSupported: vi.fn(() => options?.getAllTypes ?? true),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
vi.stubGlobal("Word", {
|
|
|
|
|
run: async (batch: (context: unknown) => Promise<unknown>) =>
|
|
|
|
|
batch({
|
|
|
|
|
document: {
|
|
|
|
|
contentControls: documentControls,
|
|
|
|
|
getContentControls: vi.fn(() => allTypesControls),
|
|
|
|
|
},
|
|
|
|
|
sync: async () => undefined,
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
return { documentControls, allTypesControls };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
vi.unstubAllGlobals();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("readContentControlFields", () => {
|
|
|
|
|
it("host 1.5+ ใช้ getContentControls() — เก็บค่า plain text CC ได้ครบทุกตัว", async () => {
|
|
|
|
|
const { documentControls, allTypesControls } = stubWord([
|
|
|
|
|
{ tag: "case_no", text: "ด1" }, // plain text — ตัวที่เดิมหลุดไปทั้งหมด
|
|
|
|
|
{ tag: "court_name", text: "ศาลปกครอง" },
|
|
|
|
|
{ tag: "detail1", text: "ตุลาการเจ้าของสำนวนได้พิจารณา…" },
|
|
|
|
|
], { getAllTypes: true });
|
|
|
|
|
|
|
|
|
|
const fields = await readContentControlFields();
|
|
|
|
|
|
|
|
|
|
expect(fields).toEqual({
|
|
|
|
|
case_no: "ด1",
|
|
|
|
|
court_name: "ศาลปกครอง",
|
|
|
|
|
detail1: "ตุลาการเจ้าของสำนวนได้พิจารณา…",
|
|
|
|
|
});
|
|
|
|
|
expect(allTypesControls.load).toHaveBeenCalledWith("tag, text");
|
|
|
|
|
expect(documentControls.load).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("host ต่ำกว่า 1.5 (เช่น Office 2019 = 1.3) ยังใช้ collection rich-text-only ตามเดิม", async () => {
|
|
|
|
|
const { documentControls, allTypesControls } = stubWord([{ tag: "detail1", text: "rich ตัวเดียว" }], {
|
|
|
|
|
getAllTypes: false,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const fields = await readContentControlFields();
|
|
|
|
|
|
|
|
|
|
expect(fields).toEqual({ detail1: "rich ตัวเดียว" });
|
|
|
|
|
expect(documentControls.load).toHaveBeenCalledWith("tag, text");
|
|
|
|
|
expect(allTypesControls.load).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("tag ว่างข้าม + tag ซ้ำเอาตัวแรกตามลำดับในเอกสาร + ไม่มี CC คืน {}", async () => {
|
|
|
|
|
stubWord([
|
|
|
|
|
{ tag: " ", text: "tag ว่าง" },
|
|
|
|
|
{ tag: "plain1", text: "ค่าแรก" },
|
|
|
|
|
{ tag: "plain1", text: "ค่าหลัง" },
|
|
|
|
|
]);
|
|
|
|
|
expect(await readContentControlFields()).toEqual({ plain1: "ค่าแรก" });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("ไม่มี content control เลย → คืน {} ไม่ error", async () => {
|
|
|
|
|
stubWord([]);
|
|
|
|
|
expect(await readContentControlFields()).toEqual({});
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-11-05 12:42:02 +07:00
|
|
|
|
|
|
|
|
// ─── enableAutoOpenTaskpane (auto-open 2026-11-05) ──────────────────────────
|
|
|
|
|
// function มี module-level memo (กัน double-call ของ detect) — แยกเคสด้วย
|
|
|
|
|
// vi.resetModules + dynamic import ให้ทุกเคสได้ module ใหม่ ไม่ให้ memo รั่ยข้ามเคส
|
|
|
|
|
interface StubbedSettings {
|
|
|
|
|
get: ReturnType<typeof vi.fn>;
|
|
|
|
|
set: ReturnType<typeof vi.fn>;
|
|
|
|
|
saveAsync: ReturnType<typeof vi.fn>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** stub Office.context.document.settings + Word.run ของ saveDocumentVerified */
|
|
|
|
|
function stubAutoOpen(options?: { existing?: boolean; saveOk?: boolean; nativeSaveThrows?: boolean }): {
|
|
|
|
|
settings: StubbedSettings;
|
|
|
|
|
saveFn: ReturnType<typeof vi.fn>;
|
|
|
|
|
} {
|
|
|
|
|
const settings = {
|
|
|
|
|
get: vi.fn(() => (options?.existing === true ? true : undefined)),
|
|
|
|
|
set: vi.fn(),
|
|
|
|
|
saveAsync: vi.fn((callback: (result: { status: string }) => void) =>
|
|
|
|
|
callback({ status: options?.saveOk === false ? "failed" : "succeeded" }),
|
|
|
|
|
),
|
|
|
|
|
};
|
|
|
|
|
const saveFn = vi.fn(() => {
|
|
|
|
|
if (options?.nativeSaveThrows) throw new Error("read-only");
|
|
|
|
|
});
|
|
|
|
|
vi.stubGlobal("Office", {
|
|
|
|
|
AsyncResultStatus: { Succeeded: "succeeded", Failed: "failed" },
|
|
|
|
|
context: { document: { settings } },
|
|
|
|
|
});
|
|
|
|
|
vi.stubGlobal("Word", {
|
|
|
|
|
run: async (batch: (context: unknown) => Promise<unknown>) =>
|
|
|
|
|
batch({
|
|
|
|
|
document: { save: saveFn, load: vi.fn(), saved: true },
|
|
|
|
|
sync: async () => undefined,
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
return { settings, saveFn };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function importFreshMetadata() {
|
|
|
|
|
vi.resetModules();
|
|
|
|
|
return await import("@/wordaddin/services/metadataService");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
describe("enableAutoOpenTaskpane", () => {
|
|
|
|
|
it("ยังไม่มี setting → set(true) + saveAsync + native save ครบ คืน true", async () => {
|
|
|
|
|
const { settings, saveFn } = stubAutoOpen();
|
|
|
|
|
const metadata = await importFreshMetadata();
|
|
|
|
|
|
|
|
|
|
const ok = await metadata.enableAutoOpenTaskpane();
|
|
|
|
|
|
|
|
|
|
expect(ok).toBe(true);
|
|
|
|
|
expect(metadata.AUTO_OPEN_SETTING).toBe("Office.AutoShowTaskpaneWithDocument");
|
|
|
|
|
expect(settings.set).toHaveBeenCalledWith("Office.AutoShowTaskpaneWithDocument", true);
|
|
|
|
|
expect(settings.saveAsync).toHaveBeenCalledTimes(1);
|
|
|
|
|
expect(saveFn).toHaveBeenCalled(); // saveDocumentVerified ตามหลัง saveAsync
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("มี setting = true อยู่แล้ว → no-op idempotent ไม่ set ไม่ saveAsync", async () => {
|
|
|
|
|
const { settings, saveFn } = stubAutoOpen({ existing: true });
|
|
|
|
|
const metadata = await importFreshMetadata();
|
|
|
|
|
|
|
|
|
|
expect(await metadata.enableAutoOpenTaskpane()).toBe(true);
|
|
|
|
|
expect(settings.set).not.toHaveBeenCalled();
|
|
|
|
|
expect(settings.saveAsync).not.toHaveBeenCalled();
|
|
|
|
|
expect(saveFn).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("นอก host Word (ไม่มี Office global) → คืน false ไม่ throw", async () => {
|
|
|
|
|
const metadata = await importFreshMetadata(); // ไม่ stub อะไรเลย
|
|
|
|
|
expect(await metadata.enableAutoOpenTaskpane()).toBe(false);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("settings.saveAsync ล้ม → คืน false ไม่ throw", async () => {
|
|
|
|
|
stubAutoOpen({ saveOk: false });
|
|
|
|
|
const metadata = await importFreshMetadata();
|
|
|
|
|
expect(await metadata.enableAutoOpenTaskpane()).toBe(false);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("native save ล้ม (ไฟล์ read-only) → ยังคืน true (setting เข้า memory doc แล้ว) + warn ไม่ throw", async () => {
|
|
|
|
|
const warn = vi.spyOn(console, "warn").mockImplementation(() => undefined);
|
|
|
|
|
stubAutoOpen({ nativeSaveThrows: true });
|
|
|
|
|
const metadata = await importFreshMetadata();
|
|
|
|
|
|
|
|
|
|
expect(await metadata.enableAutoOpenTaskpane()).toBe(true);
|
|
|
|
|
expect(warn).toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("memo: ล้มครั้งแรกแล้วล้าง — call ถัดไปยิงใหม่ได้ (สำเร็จในครั้งที่สอง)", async () => {
|
|
|
|
|
const { settings } = stubAutoOpen({ saveOk: false });
|
|
|
|
|
const metadata = await importFreshMetadata();
|
|
|
|
|
|
|
|
|
|
expect(await metadata.enableAutoOpenTaskpane()).toBe(false);
|
|
|
|
|
settings.saveAsync.mockImplementation(
|
|
|
|
|
(callback: (result: { status: string }) => void) => callback({ status: "succeeded" }),
|
|
|
|
|
);
|
|
|
|
|
expect(await metadata.enableAutoOpenTaskpane()).toBe(true);
|
|
|
|
|
expect(settings.saveAsync).toHaveBeenCalledTimes(2);
|
|
|
|
|
});
|
|
|
|
|
});
|