From 2572285827ca435f96190d83aa8188c905185807 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Thu, 26 Feb 2026 15:35:35 +0100 Subject: [PATCH] Add an integration test for PR 20740 --- test/integration/reorganize_pages_spec.mjs | 15 +--- test/integration/test_utils.mjs | 21 ++++++ test/integration/thumbnail_view_spec.mjs | 18 +---- test/integration/viewer_spec.mjs | 77 +++++++++++++++++++++ test/pdfs/.gitignore | 1 + test/pdfs/nested_outline.pdf | Bin 0 -> 5929 bytes 6 files changed, 102 insertions(+), 30 deletions(-) create mode 100644 test/pdfs/nested_outline.pdf diff --git a/test/integration/reorganize_pages_spec.mjs b/test/integration/reorganize_pages_spec.mjs index 9a6d060e1..8d7572e77 100644 --- a/test/integration/reorganize_pages_spec.mjs +++ b/test/integration/reorganize_pages_spec.mjs @@ -28,24 +28,13 @@ import { kbDelete, loadAndWait, scrollIntoView, + showViewsManager, waitAndClick, waitForDOMMutation, } from "./test_utils.mjs"; async function waitForThumbnailVisible(page, pageNums) { - const hasAnimations = await page.evaluate( - () => !window.matchMedia("(prefers-reduced-motion: reduce)").matches - ); - await page.click("#viewsManagerToggleButton"); - if (hasAnimations) { - await page.waitForSelector("#outerContainer.viewsManagerMoving", { - visible: true, - }); - } - await page.waitForSelector( - "#outerContainer:not(.viewsManagerMoving).viewsManagerOpen", - { visible: true } - ); + await showViewsManager(page); const thumbSelector = "#thumbnailsView .thumbnailImageContainer > img"; await page.waitForSelector(thumbSelector, { visible: true }); diff --git a/test/integration/test_utils.mjs b/test/integration/test_utils.mjs index 6e561ac1a..b3f3416c7 100644 --- a/test/integration/test_utils.mjs +++ b/test/integration/test_utils.mjs @@ -964,6 +964,26 @@ async function highlightSpan( await page.waitForSelector(getEditorSelector(nextId)); } +async function showViewsManager(page) { + const hasAnimations = await page.evaluate( + () => !window.matchMedia("(prefers-reduced-motion: reduce)").matches + ); + const movingPromise = hasAnimations + ? page.waitForSelector("#outerContainer.viewsManagerMoving", { + visible: true, + }) + : Promise.resolve(); + await page.click("#viewsManagerToggleButton"); + if (hasAnimations) { + await movingPromise; + } + await page.waitForSelector("#viewsManager", { visible: true }); + await page.waitForSelector( + "#outerContainer:not(.viewsManagerMoving).viewsManagerOpen", + { visible: true } + ); +} + // Unicode bidi isolation characters, Fluent adds these markers to the text. const FSI = "\u2068"; const PDI = "\u2069"; @@ -1030,6 +1050,7 @@ export { selectEditors, serializeBitmapDimensions, setCaretAt, + showViewsManager, switchToEditor, unselectEditor, waitAndClick, diff --git a/test/integration/thumbnail_view_spec.mjs b/test/integration/thumbnail_view_spec.mjs index 919431733..28e11fd4d 100644 --- a/test/integration/thumbnail_view_spec.mjs +++ b/test/integration/thumbnail_view_spec.mjs @@ -5,6 +5,7 @@ import { kbFocusNext, loadAndWait, PDI, + showViewsManager, } from "./test_utils.mjs"; function waitForThumbnailVisible(page, pageNum) { @@ -29,23 +30,6 @@ async function waitForMenu(page, buttonSelector, visible = true) { ); } -async function showViewsManager(page) { - const hasAnimations = await page.evaluate( - () => !window.matchMedia("(prefers-reduced-motion: reduce)").matches - ); - await page.click("#viewsManagerToggleButton"); - if (hasAnimations) { - await page.waitForSelector("#outerContainer.viewsManagerMoving", { - visible: true, - }); - } - await page.waitForSelector("#viewsManager", { visible: true }); - await page.waitForSelector( - "#outerContainer:not(.viewsManagerMoving).viewsManagerOpen", - { visible: true } - ); -} - describe("PDF Thumbnail View", () => { describe("Works without errors", () => { let pages; diff --git a/test/integration/viewer_spec.mjs b/test/integration/viewer_spec.mjs index 68f5e69c7..b9b3f07d1 100644 --- a/test/integration/viewer_spec.mjs +++ b/test/integration/viewer_spec.mjs @@ -21,6 +21,7 @@ import { getSpanRectFromText, loadAndWait, scrollIntoView, + showViewsManager, waitForPageChanging, waitForPageRendered, } from "./test_utils.mjs"; @@ -1451,6 +1452,82 @@ describe("PDF viewer", () => { }); }); + describe("Outline tree shift-click toggle (PR 20740)", () => { + let pages; + + beforeEach(async () => { + pages = await loadAndWait( + "nested_outline.pdf", + "#viewsManagerToggleButton" + ); + }); + + afterEach(async () => { + await closePages(pages); + }); + + it("should only toggle the clicked item's subtree, not the whole outline", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + // Open the sidebar. + await showViewsManager(page); + + // Switch to outline view. + await page.click("#viewsManagerSelectorButton"); + await page.waitForSelector("#outlinesViewMenu", { visible: true }); + await page.click("#outlinesViewMenu"); + + // Wait for the outline tree to render with nesting (toggle buttons). + await page.waitForSelector("#outlinesView.withNesting"); + + // Initially all three top-level togglers must be expanded. + const initialHiddenCount = await page.$$eval( + "#outlinesView > .treeItem > .treeItemToggler", + togglers => + togglers.filter(t => t.classList.contains("treeItemsHidden")) + .length + ); + expect(initialHiddenCount).withContext(`In ${browserName}`).toBe(0); + + // Shift-click the first top-level toggler (section "1. Introduction") + // to collapse only its subtree. + // The toggler has width/height 0 (visual content via ::before), so + // we dispatch the MouseEvent directly rather than using page.click(). + await page.evaluate(() => { + const toggler = document.querySelector( + "#outlinesView > .treeItem:nth-child(1) > .treeItemToggler" + ); + toggler.dispatchEvent( + new MouseEvent("click", { + shiftKey: true, + bubbles: true, + cancelable: true, + }) + ); + }); + + // Section 1's toggler must now be collapsed. + const section1Collapsed = await page.$eval( + "#outlinesView > .treeItem:nth-child(1) > .treeItemToggler", + t => t.classList.contains("treeItemsHidden") + ); + expect(section1Collapsed).withContext(`In ${browserName}`).toBeTrue(); + + // Sections 2 and 3 must remain expanded (the bug collapsed the whole + // outline by passing `this.container` instead of + // `target.parentNode`). + const otherHiddenCount = await page.$$eval( + "#outlinesView > .treeItem:nth-child(n+2) > .treeItemToggler", + togglers => + togglers.filter(t => t.classList.contains("treeItemsHidden")) + .length + ); + expect(otherHiddenCount).withContext(`In ${browserName}`).toBe(0); + }) + ); + }); + }); + describe("Scroll into view", () => { let pages; diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 86cd69044..ea075e8b6 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -874,3 +874,4 @@ !bug2013793.pdf !bug2014080.pdf !two_pages.pdf +!nested_outline.pdf diff --git a/test/pdfs/nested_outline.pdf b/test/pdfs/nested_outline.pdf new file mode 100644 index 0000000000000000000000000000000000000000..fa4a2558e1eb1601ea6274fd7a7cae778408365b GIT binary patch literal 5929 zcmcIoTWl0n7%qt#8EYg^5(y6;i%lxX&YUwdyR+I5y4`NN-D1mb14u*Susbb7w=>Pm zEEK~-CGi1@5=97!O;n5z@PufT7ot(0#0x|M(O^t?&_G{=mH6s8XXY$(>qT(;u+9E^ z{>%4YzWL8|3}tg0VM63OZq42Kl7m11=I*^*R~N_km^H%zX^t=G6&s9*_@^Q#)6W$B zOw&(Dbc!5we~0b?=$?S|$S#iGqE&P?V@`n)0iTxBasnwpkzhhe<6nmlOlp81QpYr# z+q8+(j0%YX;gc+u!AZ^NE<3tuc!2;ur#pSXs8y@jz+la(>V{?mkth{8YHL6}VAdSK z4;osJS*=YNuGj`{z@FD0)lsYFn9nZ(!ultBn037rhUO z&mhrd26KAV(JUN&RduwiRyHdd#}8=6n6n3@(?rR3EKQx@re-IOUgV}h%VYcBzqxk& z&KH-jwSPGJJ6N@MXvIg@kDb19@4`!mPfY$gexu^Nx^45mckK()zg+BFzP|OdlPasK4l*RA@;r6bz0N1xTMCZ8Jk<2joo+Vke6oD`2m+JK&1 z1iF`+`$hCYC%Vkwg3y%!bg}r?O>|k5w|D+&aa%9%7l#FOMHGe}&zB~jT(*48+S_Y( zcAk1~WV*N1Ce5xq{^`#%cMJ8Rv^sPCgR_7CcC@Ya&2zf+`Bm{;%eeF7PaE<-Y}nd& zi7$R7J<{^_;dV2Mob)RTw1He%1i9Pm@kJ-P%wV&SAFLP9rIiI#uP+Jt>3sV)QRHMq zS)dK%kc=svqj~z>Yl_&SQ)OmyLG1E_SN-5BvEDXYulE=0{S^IeR(IL0`y025wB)0$ zEl(%b#gv>97Yp+~@&<#X0kA%G~0 z<86u`vdnTxbHE6XH(kIFYf~6=egao9%?Pre(Ez~Z3};^T3hdJzn%ba`ZnXgGx+qn5|VEPS)B1bq6LAmo$o zoAEwFZiXyv9}vBaG<+hk#dP$2D!DudeEOLoly_RQc%NS0y)D?yySF;HDe&seVxvS% zXqd#tZpm&DPy%dGbpv?2cOW=FrGygVzHo?-&J7{B%CJuGGJ+BixFCkjNxdBK>L;17 zR~mb~SB90f9L4HosNofX629&m6%cjn=bca`-1jN*Ze0|Tx4zI)NZzi(A_`>R3(t#4 zOpwHvt2H~+7}Q{JAR7bVzSoHlhvq&U0Gocd;v&v{sWvg8S_k}jr%LmAT%Z>jO4f2D z?<^e+lnCT%PR-K5cGDUUcnwe(e(6FcB#U3mfX?iTNI*#ep(K>dk97CAu-0DOrAp_ z-J}$W52Z-|gw823vUF4?3NbPmBSSGVF-9iE$mAGVGDeny_&E^7Au7?jiRo12oFt?p zIgpSP=^;r>MrtHUO4MEwMRikT=7aG`itMYAW2t&ov*?@XlKz@T=8Z3!rUN`q%NwI6 zAQdF