sort comments sidebar in visual reading order (#6439) (#6514)

This commit is contained in:
Anthony Stirling
2026-06-04 17:58:55 +01:00
committed by GitHub
parent 22dacbed01
commit 353b5c807c
5 changed files with 533 additions and 3 deletions
@@ -0,0 +1,106 @@
import { test, expect } from "@app/tests/helpers/stub-test-base";
import path from "path";
const ANNOTATED_PDF = path.join(
__dirname,
"../test-fixtures/annotations_out_of_order.pdf",
);
/**
* Regression test for https://github.com/Stirling-Tools/Stirling-PDF/issues/6439
*
* The fixture PDF (3 pages) has 4 text annotations per page stored in
* deliberately scrambled object order (D, B, C, A). With the sort applied,
* the Comments sidebar must list them in top-to-bottom / left-to-right
* reading order on every page: A, B, C, D.
*/
test.describe("Comments sidebar - annotation reading order", () => {
test("annotations on each page are listed in visual reading order", async ({
page,
}) => {
await page.goto("/read");
await page.waitForLoadState("domcontentloaded");
// Upload the test fixture via the hidden file input (avoids the native
// file picker dialog).
await page
.locator('[data-testid="file-input"]')
.first()
.setInputFiles(ANNOTATED_PDF);
// Sanity-check that the page indicator reflects a 3-page document.
await expect(page.getByText(/\/\s*3/)).toBeVisible({ timeout: 30_000 });
// Open the Comments sidebar via the WorkbenchBar button.
const commentsBtn = page
.getByRole("button", { name: /^Comments$/i })
.first();
if (
!(await commentsBtn.isVisible({ timeout: 10_000 }).catch(() => false))
) {
test.skip(true, "Comments button not visible on this build");
return;
}
await commentsBtn.click();
// Pull the rendered order out of the sidebar. Each comment card is
// identified by `data-comment-card="${pageIndex}_${annotationId}"`.
// The annotation `contents` text (e.g. "Annotation A on page 1") is
// rendered inside the card and is the most reliable label to read.
await expect
.poll(
async () => {
return await page.evaluate(() => {
const cards = Array.from(
document.querySelectorAll<HTMLElement>("[data-comment-card]"),
);
return cards
.map((c) => {
const key = c.getAttribute("data-comment-card") || "";
const pageIndex = Number(key.split("_")[0]);
const text = (c.innerText || "").replace(/\s+/g, " ");
const m = text.match(
/Annotation\s+([ABCD])\s+on\s+page\s+(\d+)/i,
);
return m
? { pageIndex, label: m[1].toUpperCase(), text }
: { pageIndex, label: "?", text: text.slice(0, 80) };
})
.filter((e) => e.label !== "?");
});
},
{ timeout: 30_000, message: "waiting for 12 comment cards" },
)
.toHaveLength(12);
const entries = await page.evaluate(() => {
const cards = Array.from(
document.querySelectorAll<HTMLElement>("[data-comment-card]"),
);
return cards
.map((c) => {
const key = c.getAttribute("data-comment-card") || "";
const pageIndex = Number(key.split("_")[0]);
const text = (c.innerText || "").replace(/\s+/g, " ");
const m = text.match(/Annotation\s+([ABCD])\s+on\s+page\s+(\d+)/i);
return m && pageIndex === Number(m[2]) - 1
? { pageIndex, label: m[1].toUpperCase() }
: null;
})
.filter((e): e is { pageIndex: number; label: string } => e !== null);
});
// Group labels by page in the order they appear in the sidebar.
const byPage: Record<number, string[]> = {};
for (const { pageIndex, label } of entries) {
(byPage[pageIndex] ||= []).push(label);
}
// Each of the 3 pages must show A, B, C, D in that order.
expect(byPage).toEqual({
0: ["A", "B", "C", "D"],
1: ["A", "B", "C", "D"],
2: ["A", "B", "C", "D"],
});
});
});
@@ -0,0 +1,285 @@
%PDF-1.3
%âãÏÓ
1 0 obj
<<
/PageMode /UseNone
/Pages 2 0 R
/Type /Catalog
>>
endobj
2 0 obj
<<
/Count 3
/Kids [ 3 0 R 8 0 R 10 0 R ]
/Type /Pages
>>
endobj
3 0 obj
<<
/Contents 4 0 R
/MediaBox [ 0 0 612 792 ]
/Parent 2 0 R
/Resources <<
/Font 5 0 R
/ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>>
/Rotate 0
/Trans <<
>>
/Type /Page
/Annots [ 13 0 R 14 0 R 15 0 R 16 0 R ]
>>
endobj
4 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ]
/Length 202
>>
stream
Gasc=3tHos'F!Ec?ZAOh&UQ#o*6#tZi,D1%Q)C=AR/[+f8<JTCd[L^oq9HYV@35sY;ZS=WF!6VJQ_5ac^fR&Z)fc6WqHQo8:H>tCS[9WW$NBF!'qbR)!j@'#^38Q_"QI*7quN4(FLQ.GdUWCN)r43J.(@;D;^Xe(<A1$Dd`Bh2V5hZ/[TM[fF!p8pg)-skh$`ZZ=bp)N~>
endstream
endobj
5 0 obj
<<
/F1 6 0 R
/F2 7 0 R
>>
endobj
6 0 obj
<<
/BaseFont /Helvetica
/Encoding /WinAnsiEncoding
/Name /F1
/Subtype /Type1
/Type /Font
>>
endobj
7 0 obj
<<
/BaseFont /Helvetica-Bold
/Encoding /WinAnsiEncoding
/Name /F2
/Subtype /Type1
/Type /Font
>>
endobj
8 0 obj
<<
/Contents 9 0 R
/MediaBox [ 0 0 612 792 ]
/Parent 2 0 R
/Resources <<
/Font 5 0 R
/ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>>
/Rotate 0
/Trans <<
>>
/Type /Page
/Annots [ 17 0 R 18 0 R 19 0 R 20 0 R ]
>>
endobj
9 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ]
/Length 202
>>
stream
Gasc=3tHos'F!Ec?ZAOh&UQ#o*6#tZi,D1%Q)C=AR/[+f8<JTCd[L^oq9HYV@35sY;ZS=WF!6VJQ_5ac^fR&Z)fc6WqHQo8:H>tCS[9WW$NBF!'p&Ik"^_0&I-o!H#fM'Mp]E;/k\f2lV9&f&2nGEs;/VRhVG;T/Wa7sfV3.NC93%2=D6_;Vk>'SkZnYb_],0<?>V]=U~>
endstream
endobj
10 0 obj
<<
/Contents 11 0 R
/MediaBox [ 0 0 612 792 ]
/Parent 2 0 R
/Resources <<
/Font 5 0 R
/ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>>
/Rotate 0
/Trans <<
>>
/Type /Page
/Annots [ 21 0 R 22 0 R 23 0 R 24 0 R ]
>>
endobj
11 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ]
/Length 204
>>
stream
Gasc=3tHos'F!Ec?ZAOh&UOmO*6#tZi,D1%Q%p2kM<Y&$KbR[G+hd,ZP4.1!&GFN@589262J3(_E!;-:(js/V]ci`8[*L%Pacr!ifLe4,XPdH(FbqLH^VHllT<B7q$%ZV7#JWds)r7XNZ8-\*rC/`p36EY?AEMsNWLjQ3^GL-+@&[muU+M]ll.j[t]/E-ZJ+phW!aaY^rr~>
endstream
endobj
12 0 obj
<<
/Author (anonymous)
/CreationDate (D\07220260601133935\05301\04700\047)
/Creator (ReportLab PDF Library \055 www\056reportlab\056com)
/Keywords ()
/ModDate (D\07220260601133935\05301\04700\047)
/Producer (ReportLab PDF Library \055 www\056reportlab\056com)
/Subject (unspecified)
/Title (untitled)
/Trapped /False
>>
endobj
13 0 obj
<<
/Subtype /Text
/Rect [ 100 286 116 308 ]
/Contents (Annotation D on page 1)
/Open false
/Flags 0
/P 3 0 R
>>
endobj
14 0 obj
<<
/Subtype /Text
/Rect [ 380 686 396 708 ]
/Contents (Annotation B on page 1)
/Open false
/Flags 0
/P 3 0 R
>>
endobj
15 0 obj
<<
/Subtype /Text
/Rect [ 100 486 116 508 ]
/Contents (Annotation C on page 1)
/Open false
/Flags 0
/P 3 0 R
>>
endobj
16 0 obj
<<
/Subtype /Text
/Rect [ 100 686 116 708 ]
/Contents (Annotation A on page 1)
/Open false
/Flags 0
/P 3 0 R
>>
endobj
17 0 obj
<<
/Subtype /Text
/Rect [ 100 286 116 308 ]
/Contents (Annotation D on page 2)
/Open false
/Flags 0
/P 8 0 R
>>
endobj
18 0 obj
<<
/Subtype /Text
/Rect [ 380 686 396 708 ]
/Contents (Annotation B on page 2)
/Open false
/Flags 0
/P 8 0 R
>>
endobj
19 0 obj
<<
/Subtype /Text
/Rect [ 100 486 116 508 ]
/Contents (Annotation C on page 2)
/Open false
/Flags 0
/P 8 0 R
>>
endobj
20 0 obj
<<
/Subtype /Text
/Rect [ 100 686 116 708 ]
/Contents (Annotation A on page 2)
/Open false
/Flags 0
/P 8 0 R
>>
endobj
21 0 obj
<<
/Subtype /Text
/Rect [ 100 286 116 308 ]
/Contents (Annotation D on page 3)
/Open false
/Flags 0
/P 10 0 R
>>
endobj
22 0 obj
<<
/Subtype /Text
/Rect [ 380 686 396 708 ]
/Contents (Annotation B on page 3)
/Open false
/Flags 0
/P 10 0 R
>>
endobj
23 0 obj
<<
/Subtype /Text
/Rect [ 100 486 116 508 ]
/Contents (Annotation C on page 3)
/Open false
/Flags 0
/P 10 0 R
>>
endobj
24 0 obj
<<
/Subtype /Text
/Rect [ 100 686 116 708 ]
/Contents (Annotation A on page 3)
/Open false
/Flags 0
/P 10 0 R
>>
endobj
xref
0 25
0000000000 65535 f
0000000015 00000 n
0000000083 00000 n
0000000155 00000 n
0000000384 00000 n
0000000677 00000 n
0000000718 00000 n
0000000825 00000 n
0000000937 00000 n
0000001166 00000 n
0000001459 00000 n
0000001690 00000 n
0000001986 00000 n
0000002322 00000 n
0000002450 00000 n
0000002578 00000 n
0000002706 00000 n
0000002834 00000 n
0000002962 00000 n
0000003090 00000 n
0000003218 00000 n
0000003346 00000 n
0000003475 00000 n
0000003604 00000 n
0000003733 00000 n
trailer
<<
/Size 25
/Root 1 0 R
/Info 12 0 R
/ID [ <f0cdd00ba65dc098aa8b749db481bf6a> <f0cdd00ba65dc098aa8b749db481bf6a> ]
>>
startxref
3862
%%EOF