Phase 11: version PDF (FR-15)
- เลข version ใหม่: เว็บ-generate = v0 ฉบับตั้งต้น (ไม่มี PDF), add-in save (Start Versioning + Save & Version) = v1+ มี PDF เสมอ - PDF ผลิตจาก Word จริงผ่าน task pane เท่านั้น (getFileAsync Pdf + runtime guard, manifest ไม่เพิ่ม requirement); capture/upload เป็น best-effort — docx ผ่านแม้ PDF ล้ม + MessageBar เตือนตาม hasPdf - Backend: DocumentVersion.PdfObjectKey (nullable, migration AddVersionPdf) + sibling .pdf ใน bucket docx-versions · multipart part pdf (ผิด type → 400) · /download + /download-link รับ ?format=pdf (ไม่มี → 404) · presigned ฝังชื่อไฟล์ไทย + _vN (RFC 5987) - Web: เปิดไฟล์แทน blob-download — PDF แท็บใหม่, docx ผ่าน ms-word:ofe|u| พร้อม heuristic fallback ลิงก์สำรอง (useVersionOpen, versionDownload) · ปุ่ม "เปิดใน Word" ที่หน้า success ของฟอร์มสร้างเอกสาร - Spec v7 + CONTEXT.md + ADR 0001 + แผน docs/plans/version-pdf.md - Automated QA: backend 68 / add-in 63 / web 55 ผ่าน · manual acceptance ผ่านบน Word for Mac (2026-09-08) รวม fidelity + track changes — ค้างเฉพาะ Windows / Word on the web Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
parent
618328ffa7
commit
39d610a781
39 changed files with 1387 additions and 212 deletions
|
|
@ -5,12 +5,17 @@ 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 {
|
||||
function installOfficeDocumentMock(
|
||||
url: string | undefined,
|
||||
options: { failPdf?: boolean; emptyPdf?: boolean } = {}
|
||||
): string[] {
|
||||
const requested: string[] = [];
|
||||
const slice = new Uint8Array([1, 2, 3, 4]);
|
||||
(globalThis as unknown as Record<string, unknown>).Office = {
|
||||
FileType: { Compressed: "compressed", Text: "text" },
|
||||
FileType: { Compressed: "compressed", Pdf: "pdf", Text: "text" },
|
||||
AsyncResultStatus: { Succeeded: "succeeded", Failed: "failed" },
|
||||
context: {
|
||||
requirements: { isSetSupported: () => true },
|
||||
document: {
|
||||
url,
|
||||
getFileAsync: (
|
||||
|
|
@ -18,10 +23,15 @@ function installOfficeDocumentMock(url: string | undefined): void {
|
|||
_options: unknown,
|
||||
callback: (result: unknown) => void
|
||||
) => {
|
||||
requested.push(String(_fileType));
|
||||
if (_fileType === "pdf" && options.failPdf) {
|
||||
callback({ status: "failed", error: { message: "PDF unavailable" } });
|
||||
return;
|
||||
}
|
||||
callback({
|
||||
status: "succeeded",
|
||||
value: {
|
||||
sliceCount: 1,
|
||||
sliceCount: _fileType === "pdf" && options.emptyPdf ? 0 : 1,
|
||||
getSliceAsync: (index: number, cb: (result: unknown) => void) =>
|
||||
cb({ status: "succeeded", value: { index, data: slice.buffer } }),
|
||||
closeAsync: (cb?: (result: unknown) => void) => cb?.({ status: "succeeded" }),
|
||||
|
|
@ -31,6 +41,7 @@ function installOfficeDocumentMock(url: string | undefined): void {
|
|||
},
|
||||
},
|
||||
};
|
||||
return requested;
|
||||
}
|
||||
|
||||
function seedSession(): void {
|
||||
|
|
@ -266,3 +277,35 @@ describe("saveAndVersion (FR-2 / AC-7, 13)", () => {
|
|||
expect(form.get("message")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe.each(["start", "save"])("FR-15 %s", (flow) => {
|
||||
it.each(["success", "failure", "empty", "storage-failure"])(
|
||||
"PDF %s preserves docx upload",
|
||||
async (mode) => {
|
||||
installWordMock({ properties: { DocumentTrackingId: "doc" } });
|
||||
const requested = installOfficeDocumentMock(undefined, {
|
||||
failPdf: mode === "failure",
|
||||
emptyPdf: mode === "empty",
|
||||
});
|
||||
const warn = jest.spyOn(console, "warn").mockImplementation(() => {});
|
||||
const fetchMock = installFetchMock();
|
||||
fetchMock.mockImplementationOnce(async () =>
|
||||
mockResponse({
|
||||
status: 201,
|
||||
body: { versionId: "v", versionNumber: 1, hasPdf: mode === "success" },
|
||||
})
|
||||
);
|
||||
const result = flow === "start" ? await startVersioning() : await saveAndVersion("doc", 0);
|
||||
const { form } = parseUploadCall(fetchMock.mock.calls[0] as [string, { body?: FormData }]);
|
||||
expect(requested).toEqual(["compressed", "pdf"]);
|
||||
expect(form.get("file")).toBeInstanceOf(Blob);
|
||||
if (mode === "success" || mode === "storage-failure") {
|
||||
expect((form.get("pdf") as File).name).toMatch(/\.pdf$/);
|
||||
expect((form.get("pdf") as File).type).toBe("application/pdf");
|
||||
} else expect(form.get("pdf")).toBeNull();
|
||||
expect(result.hasPdf).toBe(mode === "success");
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
warn.mockRestore();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue