// @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) => 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({}); }); }); // ─── enableAutoOpenTaskpane (auto-open 2026-11-05) ────────────────────────── // function มี module-level memo (กัน double-call ของ detect) — แยกเคสด้วย // vi.resetModules + dynamic import ให้ทุกเคสได้ module ใหม่ ไม่ให้ memo รั่ยข้ามเคส interface StubbedSettings { get: ReturnType; set: ReturnType; saveAsync: ReturnType; } /** stub Office.context.document.settings + Word.run ของ saveDocumentVerified */ function stubAutoOpen(options?: { existing?: boolean; saveOk?: boolean; nativeSaveThrows?: boolean }): { settings: StubbedSettings; saveFn: ReturnType; } { 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) => 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); }); });