add-in: auto-open taskpane phase 1 + final save after versioning + tracking id = fid

- manifest: TaskpaneId -> Office.AutoShowTaskpaneWithDocument (auto-open marker), bump 1.0.0.4
- metadataService.enableAutoOpenTaskpane: tag doc setting (idempotent, best-effort) called from detect() after sign-in
- versionFlow: final saveDocumentVerified at end of both versioning flows (bug: stamped docx never persisted -> reopen showed untracked)
- tracking id now uses ticket fid (TRACKING_ID_SOURCE="fid", rollback by switching to "guid"), resolveTrackingId + authService.getCurrentFid
- WordAddinGate: restore history.pushState/replaceState that office.js nullifies in taskpane (fixes Next App Router crash)

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
Suphonchai Phoonsawat 2026-11-05 12:42:02 +07:00
parent 02894f2705
commit 9fecaa7bca
8 changed files with 256 additions and 6 deletions

View file

@ -93,3 +93,103 @@ describe("readContentControlFields", () => {
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<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);
});
});