// autoSaveService (FR-17/FR-3) — คุมพฤติกรรม flag "มีการแก้ไขที่ยังไม่เป็น version": // ติดเฉพาะ clean→dirty (ผู้ใช้แก้จริง) หรือ notifyDocumentChanged ของ service ที่แก้เอกสาร // ส่วนไฟล์ที่เปิดมา dirty ตั้งแต่แรก (quirk ของ Word หลัง stamp properties หรือแก้ค้าง // จาก session ก่อน) ต้องถูก save เงียบ ๆ ไม่เด้ง banner ให้ผู้ใช้งงว่าไม่ได้ทำอะไร // (คำขอผู้ใช้ 2026-11-03) — mock documentService แทน Word.run ทั้งหมด import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { autoSaveConfig, autoSaveService } from "@/wordaddin/services/autoSaveService"; const doc = vi.hoisted(() => ({ dirty: false, saves: 0 })); vi.mock("@/wordaddin/services/documentService", () => ({ isDocumentDirty: vi.fn(async () => doc.dirty), saveDocument: vi.fn(async () => { doc.saves += 1; }), })); /** หมายเหตุ timing: seedBaseline ของ start() อ่าน dirty ณ ตอนเรียก — จะซิม "เปิดมา dirty" * ต้องตั้ง doc.dirty ก่อน startService() แล้ว flush microtask ให้ baseline จบก่อนเดินเวลา */ async function flushMicrotasks(): Promise { await vi.advanceTimersByTimeAsync(0); } /** เดินเวลาไปอีกหนึ่ง periodic tick (30 วิ) แบบ flush microtask ของ tick ด้วย */ async function tick(): Promise { await vi.advanceTimersByTimeAsync(autoSaveConfig.periodicIntervalMs); } beforeEach(() => { vi.useFakeTimers(); doc.dirty = false; doc.saves = 0; }); afterEach(() => { autoSaveService.stop(); autoSaveService.clearUncommittedSinceVersion(); vi.useRealTimers(); vi.restoreAllMocks(); }); describe("flag FR-3 (hasUncommitted) กับ periodic save", () => { it("เปิดไฟล์สะอาด → ผู้ใช้แก้จริง (dirty ภายหลัง) → tick ตั้ง flag + save", async () => { autoSaveService.start(); await flushMicrotasks(); // baseline: สะอาด doc.dirty = true; // ผู้ใช้พิมพ์ await tick(); expect(autoSaveService.hasUncommittedSinceVersion).toBe(true); expect(doc.saves).toBe(1); }); it("เปิดมา dirty ตั้งแต่แรก (quirk หลัง stamp/แก้ค้าง) → save เงียบ ๆ ไม่ตั้ง flag", async () => { doc.dirty = true; // ก่อน start — ไฟล์เปิดมา dirty autoSaveService.start(); await flushMicrotasks(); await tick(); expect(autoSaveService.hasUncommittedSinceVersion).toBe(false); expect(doc.saves).toBe(1); }); it("quirk ถาวร (save แล้วยัง dirty ทุกรอบ) → หลาย tick ก็ไม่ตั้ง flag (แต่ยัง save)", async () => { doc.dirty = true; autoSaveService.start(); await flushMicrotasks(); await tick(); await tick(); await tick(); expect(autoSaveService.hasUncommittedSinceVersion).toBe(false); expect(doc.saves).toBe(3); }); it("dirty แรก (quirk) → save แล้วสะอาด → แก้จริงหลังจากนั้น → flag ติด (เห็น clean→dirty)", async () => { doc.dirty = true; autoSaveService.start(); await flushMicrotasks(); await tick(); // save + ไม่ flag doc.dirty = false; // save ทำให้สะอาด await tick(); // เห็น clean → จุดอ้างอิงกลับมา doc.dirty = true; // ผู้ใช้แก้จริง await tick(); expect(autoSaveService.hasUncommittedSinceVersion).toBe(true); }); it("notifyDocumentChanged (service ที่แก้เอกสาร เช่น เครื่องมือ template) → flag ทันที + debounce save 3 วิ", async () => { autoSaveService.start(); await flushMicrotasks(); doc.dirty = true; autoSaveService.notifyDocumentChanged(); expect(autoSaveService.hasUncommittedSinceVersion).toBe(true); // ไม่รอ tick expect(doc.saves).toBe(0); // ยังไม่ save — รอ debounce await vi.advanceTimersByTimeAsync(autoSaveConfig.debounceMs); expect(doc.saves).toBe(1); }); it("สร้าง version สำเร็จ → clearUncommittedSinceVersion เคลียร์ flag", async () => { autoSaveService.start(); await flushMicrotasks(); doc.dirty = true; await tick(); expect(autoSaveService.hasUncommittedSinceVersion).toBe(true); autoSaveService.clearUncommittedSinceVersion(); expect(autoSaveService.hasUncommittedSinceVersion).toBe(false); }); it("suspend ระหว่าง flow (versioning) → tick ไม่ save ไม่แตะ flag", async () => { autoSaveService.start(); await flushMicrotasks(); doc.dirty = true; await autoSaveService.suspend(); await tick(); expect(autoSaveService.hasUncommittedSinceVersion).toBe(false); expect(doc.saves).toBe(0); autoSaveService.resume(); }); });