implement phase 5 และ fix bugs
This commit is contained in:
parent
f4b1c3e14b
commit
1bb850c6b8
40 changed files with 1507 additions and 429 deletions
216
addin/src/services/versionFlow.test.ts
Normal file
216
addin/src/services/versionFlow.test.ts
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
import * as authService from "./authService";
|
||||
import { ADDIN_SIGNATURE_GUID, buildVersionHistoryXml } from "./metadataService";
|
||||
import { saveAndVersion, startVersioning } from "./versionFlow";
|
||||
import { installWordMock } from "../test/wordMock";
|
||||
import { installFetchMock, makeFakeJwt, mockResponse } from "../test/httpMock";
|
||||
|
||||
/** mock Office.context.document (url สำหรับชื่อไฟล์ + getFileAsync สำหรับ fileService) */
|
||||
function installOfficeDocumentMock(url: string | undefined): void {
|
||||
const slice = new Uint8Array([1, 2, 3, 4]);
|
||||
(globalThis as unknown as Record<string, unknown>).Office = {
|
||||
FileType: { Compressed: "compressed", Text: "text" },
|
||||
AsyncResultStatus: { Succeeded: "succeeded", Failed: "failed" },
|
||||
context: {
|
||||
document: {
|
||||
url,
|
||||
getFileAsync: (
|
||||
_fileType: unknown,
|
||||
_options: unknown,
|
||||
callback: (result: unknown) => void
|
||||
) => {
|
||||
callback({
|
||||
status: "succeeded",
|
||||
value: {
|
||||
sliceCount: 1,
|
||||
getSliceAsync: (index: number, cb: (result: unknown) => void) =>
|
||||
cb({ status: "succeeded", value: { index, data: slice.buffer } }),
|
||||
closeAsync: (cb?: (result: unknown) => void) => cb?.({ status: "succeeded" }),
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function seedSession(): void {
|
||||
authService.setSessionForTest(
|
||||
makeFakeJwt(Date.now() + 600_000, "user-sub", { preferred_username: "tester" }),
|
||||
"refresh-1",
|
||||
Date.now() + 600_000
|
||||
);
|
||||
}
|
||||
|
||||
function parseUploadCall(call: [string, { body?: FormData }]): { url: URL; form: FormData } {
|
||||
return { url: new URL(call[0]), form: call[1].body as FormData };
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
seedSession();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
authService.signOut();
|
||||
});
|
||||
|
||||
describe("startVersioning (FR-1 / AC-5)", () => {
|
||||
it("stamp รอบแรกครบ → save → upload v1 พร้อม fields + ชื่อไฟล์จาก URL → เขียน XML part v1", async () => {
|
||||
const state = installWordMock({
|
||||
contentControls: [
|
||||
{ tag: "employee_name", text: "สมชาย ใจดี" },
|
||||
{ tag: "salary", text: "50000" },
|
||||
],
|
||||
});
|
||||
installOfficeDocumentMock("C:\\Users\\tester\\Documents\\สัญญาจ้าง.docx");
|
||||
const fetchMock = installFetchMock();
|
||||
fetchMock.mockImplementationOnce(async () =>
|
||||
mockResponse({ status: 201, body: { versionId: "backend-version-1", versionNumber: 1 } })
|
||||
);
|
||||
|
||||
const result = await startVersioning();
|
||||
|
||||
// ผลลัพธ์ผูกกันทั้งชุด
|
||||
expect(result).toEqual({
|
||||
documentId: result.documentId,
|
||||
versionId: "backend-version-1",
|
||||
versionNumber: 1,
|
||||
});
|
||||
expect(result.documentId).toMatch(/^[0-9a-f-]{36}$/i);
|
||||
|
||||
// ครบทั้ง 4 properties + SavedByAddIn = GUID ของ add-in
|
||||
expect(state.properties.SavedByAddIn).toBe(ADDIN_SIGNATURE_GUID);
|
||||
expect(state.properties.DocumentTrackingId).toBe(result.documentId);
|
||||
expect(state.properties.LastVersionId).toMatch(/^[0-9a-f-]{36}$/i);
|
||||
expect(state.properties.LastVersionTimestamp).toMatch(/^\d{4}-\d{2}-\d{2}T/);
|
||||
// เรียก native save หนึ่งครั้ง (FR-2 ขั้นตอนที่ 4)
|
||||
expect(state.saveCalls).toBe(1);
|
||||
|
||||
// upload ไปที่ documentId ที่ generate + แนบ fields + clientVersionId + ชื่อไฟล์จาก URL
|
||||
const { url, form } = parseUploadCall(fetchMock.mock.calls[0] as [string, { body?: FormData }]);
|
||||
expect(`${url.origin}${url.pathname}`).toBe(
|
||||
`https://localhost:7232/api/documents/${result.documentId}/versions`
|
||||
);
|
||||
expect(String(form.get("clientVersionId"))).toBe(state.properties.LastVersionId);
|
||||
expect(JSON.parse(String(form.get("fields")))).toEqual({
|
||||
employee_name: "สมชาย ใจดี",
|
||||
salary: "50000",
|
||||
});
|
||||
expect(String((form.get("file") as File).name)).toBe("สัญญาจ้าง.docx");
|
||||
|
||||
// XML part มี entry v1 ผูกกับ clientVersionId เดียวกับ LastVersionId
|
||||
expect(state.xmlParts).toHaveLength(1);
|
||||
expect(state.xmlParts[0].xml).toContain('id="');
|
||||
expect(state.xmlParts[0].xml).toContain('number="1"');
|
||||
expect(state.xmlParts[0].xml).toContain('user="tester"');
|
||||
});
|
||||
|
||||
it("URL ไม่มี (Word Online) → ชื่อไฟล์ fallback", async () => {
|
||||
installWordMock({});
|
||||
installOfficeDocumentMock(undefined);
|
||||
const fetchMock = installFetchMock();
|
||||
fetchMock.mockImplementationOnce(async () =>
|
||||
mockResponse({ status: 201, body: { versionId: "v", versionNumber: 1 } })
|
||||
);
|
||||
|
||||
await startVersioning();
|
||||
|
||||
const { form } = parseUploadCall(fetchMock.mock.calls[0] as [string, { body?: FormData }]);
|
||||
expect(String((form.get("file") as File).name)).toMatch(/\.docx$/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("saveAndVersion (FR-2 / AC-7, 13)", () => {
|
||||
it("stamp → append XML entry (เดาเลขถัดไป) → save → upload พร้อม fields ล่าสุด", async () => {
|
||||
const state = installWordMock({
|
||||
properties: {
|
||||
SavedByAddIn: ADDIN_SIGNATURE_GUID,
|
||||
DocumentTrackingId: "11111111-1111-1111-1111-111111111111",
|
||||
LastVersionId: "old-client-version-id",
|
||||
},
|
||||
xmlParts: [
|
||||
buildVersionHistoryXml([
|
||||
{
|
||||
id: "old-client-version-id",
|
||||
number: 1,
|
||||
timestamp: "2026-08-19T09:00:00Z",
|
||||
user: "tester",
|
||||
},
|
||||
]),
|
||||
],
|
||||
contentControls: [{ tag: "salary", text: "60000" }],
|
||||
});
|
||||
installOfficeDocumentMock(undefined);
|
||||
const fetchMock = installFetchMock();
|
||||
fetchMock.mockImplementationOnce(async () =>
|
||||
mockResponse({ status: 201, body: { versionId: "backend-version-2", versionNumber: 2 } })
|
||||
);
|
||||
|
||||
const result = await saveAndVersion("11111111-1111-1111-1111-111111111111", 1);
|
||||
|
||||
expect(result).toEqual({ versionId: "backend-version-2", versionNumber: 2 });
|
||||
|
||||
// properties: tracking id คงเดิม, LastVersionId เปลี่ยนเป็น clientVersionId ใหม่
|
||||
expect(state.properties.DocumentTrackingId).toBe("11111111-1111-1111-1111-111111111111");
|
||||
const newLastVersionId = state.properties.LastVersionId;
|
||||
expect(newLastVersionId).not.toBe("old-client-version-id");
|
||||
expect(state.saveCalls).toBe(1);
|
||||
|
||||
// XML part: 2 entries เรียง v2 ก่อน v1
|
||||
expect(state.xmlParts).toHaveLength(1);
|
||||
const xml = state.xmlParts[0].xml;
|
||||
expect(xml).toContain('number="2"');
|
||||
expect(xml).toContain('number="1"');
|
||||
expect(xml.indexOf('number="2"')).toBeLessThan(xml.indexOf('number="1"'));
|
||||
|
||||
// upload ไปยัง document เดิม + fields ล่าสุด
|
||||
const { url, form } = parseUploadCall(fetchMock.mock.calls[0] as [string, { body?: FormData }]);
|
||||
expect(url.pathname).toBe("/api/documents/11111111-1111-1111-1111-111111111111/versions");
|
||||
expect(String(form.get("clientVersionId"))).toBe(newLastVersionId);
|
||||
expect(JSON.parse(String(form.get("fields")))).toEqual({ salary: "60000" });
|
||||
});
|
||||
|
||||
it("backend ตอบเลข version ต่างจากที่เดา (มี client อื่นแทรก) → แก้ XML part ให้ตรงเลขจริง", async () => {
|
||||
const state = installWordMock({
|
||||
properties: {
|
||||
SavedByAddIn: ADDIN_SIGNATURE_GUID,
|
||||
DocumentTrackingId: "11111111-1111-1111-1111-111111111111",
|
||||
LastVersionId: "old",
|
||||
},
|
||||
xmlParts: [buildVersionHistoryXml([{ id: "old", number: 1, timestamp: "t", user: "u" }])],
|
||||
});
|
||||
installOfficeDocumentMock(undefined);
|
||||
const fetchMock = installFetchMock();
|
||||
fetchMock.mockImplementationOnce(async () =>
|
||||
mockResponse({ status: 201, body: { versionId: "backend-version-5", versionNumber: 5 } })
|
||||
);
|
||||
|
||||
const result = await saveAndVersion("11111111-1111-1111-1111-111111111111", 1);
|
||||
|
||||
expect(result.versionNumber).toBe(5);
|
||||
// entry ใหม่ถูกแก้จาก 2 (ที่เดา) เป็น 5 (ของจริง)
|
||||
const xml = state.xmlParts[0].xml;
|
||||
expect(xml).toContain('number="5"');
|
||||
expect(xml).not.toContain('number="2"');
|
||||
expect(xml).toContain('number="1"');
|
||||
});
|
||||
|
||||
it("XML part เขียนไม่ได้ (known bug บน Mac) → flow ยังสำเร็จต่อ (AC-10)", async () => {
|
||||
installWordMock({
|
||||
properties: {
|
||||
SavedByAddIn: ADDIN_SIGNATURE_GUID,
|
||||
DocumentTrackingId: "11111111-1111-1111-1111-111111111111",
|
||||
},
|
||||
failXmlAdd: true,
|
||||
});
|
||||
installOfficeDocumentMock(undefined);
|
||||
const fetchMock = installFetchMock();
|
||||
fetchMock.mockImplementationOnce(async () =>
|
||||
mockResponse({ status: 201, body: { versionId: "backend-version-2", versionNumber: 2 } })
|
||||
);
|
||||
|
||||
await expect(saveAndVersion("11111111-1111-1111-1111-111111111111", 1)).resolves.toEqual({
|
||||
versionId: "backend-version-2",
|
||||
versionNumber: 2,
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue