From d305b542dfaf289f6bbce6861b7acaa23f6737e5 Mon Sep 17 00:00:00 2001 From: Tim van der Meij Date: Sat, 13 Jun 2026 20:59:45 +0200 Subject: [PATCH 1/5] Fix intermittent failure in the `must check that the comment sidebar is resizable` comment integration test We use the generic `page.mouse.move(x, y, { steps }` API, but that purely performs the mouse move steps without having knowledge about if/how the application handles any events caused by it, so it doesn't wait for the sidebar to render before moving on. This causes intermittent failures if the sidebar didn't get enough time to render before the next mouse move is initiated (which can happen in slower environments). This commit fixes the issue by doing the mouse move steps ourselves and by waiting for a browser trip between each of them to make sure that the sidebar got a chance to render. Fixes #21447. Relates to #21044 / #21045 / 24e5377. --- test/integration/comment_spec.mjs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/integration/comment_spec.mjs b/test/integration/comment_spec.mjs index a109ba4aad23e..e910a126c2920 100644 --- a/test/integration/comment_spec.mjs +++ b/test/integration/comment_spec.mjs @@ -537,7 +537,11 @@ describe("Comment", () => { await page.mouse.down(); const steps = 20; - await page.mouse.move(startX - extraWidth, startY, { steps }); + for (let i = 1; i <= steps; i++) { + const x = Math.round(startX - (extraWidth * i) / steps); + await page.mouse.move(x, startY); + await waitForBrowserTrip(page); + } await page.mouse.up(); const rectAfter = await getRect(page, sidebarSelector); From 66c22b1fc506128e18174556fb3e86b27ba6c01b Mon Sep 17 00:00:00 2001 From: Tim van der Meij Date: Sun, 14 Jun 2026 16:26:18 +0200 Subject: [PATCH 2/5] Configure Puppeteer to not download Chrome headless shell Nowadays Chrome has a built-in (new) headless mode in the regular binary, but before that time there was an old headless mode that was essentially a separate binary [1]. We don't use the latter, but it turns out that Puppeteer downloads it automatically if it's not explicitly skipped, which is wasteful because it costs extra time and resources for each `npm install` invocation. This commit therefore skips downloading Chrome headless shell explictly, which results in the local runtime of `npm install` going from 10.125 seconds to 8.998 seconds (which can't hurt in e.g. GitHub Actions). [1] https://developer.chrome.com/blog/chrome-headless-shell. --- .puppeteerrc.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.puppeteerrc.json b/.puppeteerrc.json index 4457c47d724e4..d45a2f2fd346f 100644 --- a/.puppeteerrc.json +++ b/.puppeteerrc.json @@ -2,6 +2,9 @@ "chrome": { "skipDownload": false }, + "chrome-headless-shell": { + "skipDownload": true + }, "firefox": { "skipDownload": false, "version": "nightly" From 6bfefa53da24ccbf703ea8f22380e60f718aa107 Mon Sep 17 00:00:00 2001 From: Tim van der Meij Date: Sun, 14 Jun 2026 16:28:36 +0200 Subject: [PATCH 3/5] Configure Puppeteer to use the stable version of Chrome We currently use the pinned version of Chrome as hardcoded in the Puppeteer release, which is based on the version of Chrome that was deemed stable at the time of the Puppeteer release. However, this is not ideal because it means that Chrome updates are strongly tied to Puppeteer releases, so if Puppeteer releases are slow we could be missing out on e.g. (security) patches being applied on the stable channel. It's also not consistent with Firefox where we don't use a hardcoded pinned version either. This commit therefore configures Puppeteer to use (resolve) the most recent stable version of Chrome at the time of the installation so that determining the browser version to use is fully decoupled from the Puppeteer release we're running. The effect of this change can be seen in the output of running `npx puppeteer browsers list`: Before: `chrome@149.0.7827.22 (linux) ` After (note the slightly newer version): `chrome@149.0.7827.115 (linux)` ` --- .puppeteerrc.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.puppeteerrc.json b/.puppeteerrc.json index d45a2f2fd346f..4401066f6ee1f 100644 --- a/.puppeteerrc.json +++ b/.puppeteerrc.json @@ -1,6 +1,7 @@ { "chrome": { - "skipDownload": false + "skipDownload": false, + "version": "stable" }, "chrome-headless-shell": { "skipDownload": true From 26b4206d879673f8e2cbbb615b59bb211c8ea3fc Mon Sep 17 00:00:00 2001 From: Tim van der Meij Date: Sun, 14 Jun 2026 16:23:43 +0200 Subject: [PATCH 4/5] Configure Puppeteeer to not download Chrome/Firefox by default We currently download Chrome/Firefox immediately on `npm install` invocations because Puppeteer's postinstall script does that by default. However, this is wasteful if the user/workflow doesn't actually need to run Puppeteer or its browsers, for example in GitHub Actions workflows that do linting, static analysis or other tasks like updating locales or publishing artifacts. This commit therefore makes sure no browser binaries get pulled in by default anymore, and defers doing that until it's actually necessary, which is when we want to start the browsers in the `startBrowsers` function of `test.mjs`. Locally this brings the `npm install` runtime down from 8.998 to 1.800 seconds, and as a bonus it results in better log output too because it now shows which browser versions were used in the run (whereas previously with `npm install` this information was not sent to stdout). --- .puppeteerrc.json | 4 ++-- test/test.mjs | 11 ++++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.puppeteerrc.json b/.puppeteerrc.json index 4401066f6ee1f..a1d0552190d22 100644 --- a/.puppeteerrc.json +++ b/.puppeteerrc.json @@ -1,13 +1,13 @@ { "chrome": { - "skipDownload": false, + "skipDownload": true, "version": "stable" }, "chrome-headless-shell": { "skipDownload": true }, "firefox": { - "skipDownload": false, + "skipDownload": true, "version": "nightly" } } diff --git a/test/test.mjs b/test/test.mjs index e35a0e2c64bf2..a2a1a733b8de7 100644 --- a/test/test.mjs +++ b/test/test.mjs @@ -28,6 +28,7 @@ import { downloadManifestFiles, verifyManifestFiles, } from "./downloadutils.mjs"; +import { execSync } from "child_process"; import fs from "fs"; import istanbulCoverage from "istanbul-lib-coverage"; import istanbulReportGenerator from "istanbul-reports"; @@ -1095,9 +1096,13 @@ async function startBrowser({ } async function startBrowsers({ baseUrl, initializeSession, numSessions = 1 }) { - // Remove old browser revisions from Puppeteer's cache. Updating Puppeteer can - // cause new browser revisions to be downloaded, so trimming the cache will - // prevent the disk from filling up over time. + // Install the browsers. + for (const browser of ["firefox@nightly", "chrome@stable"]) { + execSync(`npx puppeteer browsers install ${browser}`, { stdio: "inherit" }); + } + + // Remove old browser revisions from Puppeteer's cache. The commands above can + // download new browser revisions, so this prevents the disk from filling up. await puppeteer.trimCache(); const browserNames = ["firefox", "chrome"]; From 0ea67ed96fd2eb4d62fc172ce8dd52e3ddc806b1 Mon Sep 17 00:00:00 2001 From: Tim van der Meij Date: Sun, 14 Jun 2026 20:09:39 +0200 Subject: [PATCH 5/5] Upgrade `eslint-plugin-unicorn` to version 66.0.0 This is a major version bump, but the changelog at https://github.com/sindresorhus/eslint-plugin-unicorn/releases/tag/v66.0.0 doesn't indicate any breaking changes that should impact us. However, improved rules do require a small number of changes here: - The `prefer-array-some` rule no longer reports a false positive after https://github.com/sindresorhus/eslint-plugin-unicorn/issues/3198 got fixed, so the ignore line that was added in commit 68a5ec1 is removed. - The `prefer-ternary` rule triggers on more cases now, in particular `let` declarations with `if` reassignments, so a number of changes are made to make it pass again. - The `prefer-at` rule triggers on more cases now, in particular `substring` calls that just extract a single character, so one change is made to make it pass again. --- gulpfile.mjs | 5 +---- package-lock.json | 33 +++++++++++++++--------------- package.json | 2 +- src/core/annotation.js | 5 +---- src/core/default_appearance.js | 5 +---- src/core/evaluator.js | 5 +---- src/core/fonts.js | 10 ++------- src/core/pattern.js | 7 +++---- src/core/type1_parser.js | 11 ++++------ src/core/xfa/factory.js | 1 - src/core/xml_parser.js | 2 +- src/scripting_api/event.js | 15 ++++++-------- test/resources/reftest-analyzer.js | 9 ++++---- 13 files changed, 43 insertions(+), 67 deletions(-) diff --git a/gulpfile.mjs b/gulpfile.mjs index 28d78e4517c85..dca06a8d1fbb2 100644 --- a/gulpfile.mjs +++ b/gulpfile.mjs @@ -1030,10 +1030,7 @@ function createBuildNumber(done) { const version = config.versionPrefix + buildNumber; exec('git log --format="%h" -n 1', function (err2, stdout2, stderr2) { - let buildCommit = ""; - if (!err2) { - buildCommit = stdout2.replace("\n", ""); - } + const buildCommit = !err2 ? stdout2.replace("\n", "") : ""; createStringSource( "version.json", diff --git a/package-lock.json b/package-lock.json index 34867c4b63030..1d222448a4853 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,7 +32,7 @@ "eslint-plugin-perfectionist": "^5.9.0", "eslint-plugin-prettier": "^5.5.6", "eslint-plugin-regexp": "^3.1.0", - "eslint-plugin-unicorn": "^65.0.1", + "eslint-plugin-unicorn": "^66.0.0", "globals": "^17.6.0", "gulp": "^5.0.1", "gulp-cli": "^3.1.0", @@ -5282,42 +5282,43 @@ } }, "node_modules/eslint-plugin-unicorn": { - "version": "65.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-65.0.1.tgz", - "integrity": "sha512-daCrQrgxOoOz2uMPWB3Y3vvv/5q+ncwICI8IjoebiwtW87CaY4tAN5EEiRXTYVnf7qi1v1BGBdHOSnZLV0rx6A==", + "version": "66.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-66.0.0.tgz", + "integrity": "sha512-+ywdy8T3foyZ2t3nRBujGa3vfOVMobHIi5iLB0L+fogdVO3EiUJ4BAyIacogWytnweLw3hgT70LQL9KoKTY/kA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", + "@babel/helper-validator-identifier": "^7.29.7", "@eslint-community/eslint-utils": "^4.9.1", + "browserslist": "^4.28.2", "change-case": "^5.4.4", "ci-info": "^4.4.0", "core-js-compat": "^3.49.0", "detect-indent": "^7.0.2", "find-up-simple": "^1.0.1", - "globals": "^17.4.0", + "globals": "^17.6.0", "indent-string": "^5.0.0", "is-builtin-module": "^5.0.0", "jsesc": "^3.1.0", "pluralize": "^8.0.0", - "regjsparser": "^0.13.0", - "semver": "^7.7.4", + "regjsparser": "^0.13.1", + "semver": "^7.8.4", "strip-indent": "^4.1.1" }, "engines": { - "node": "^20.10.0 || >=21.0.0" + "node": ">=22" }, "funding": { "url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1" }, "peerDependencies": { - "eslint": ">=9.38.0" + "eslint": ">=10.4" } }, "node_modules/eslint-plugin-unicorn/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", "dev": true, "license": "ISC", "bin": { @@ -8779,9 +8780,9 @@ "license": "MIT" }, "node_modules/regjsparser": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz", - "integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==", + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.2.tgz", + "integrity": "sha512-NgRBy2Nx/bE+9F27nVHnqcN5HjyLmecqsqx2PJHu3/IEtADD4WuxuXIVExD5PoSDFVrl78dOonfcOe5O+5nbzQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { diff --git a/package.json b/package.json index f51ca4401e72e..d624b2a27f235 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "eslint-plugin-perfectionist": "^5.9.0", "eslint-plugin-prettier": "^5.5.6", "eslint-plugin-regexp": "^3.1.0", - "eslint-plugin-unicorn": "^65.0.1", + "eslint-plugin-unicorn": "^66.0.0", "globals": "^17.6.0", "gulp": "^5.0.1", "gulp-cli": "^3.1.0", diff --git a/src/core/annotation.js b/src/core/annotation.js index c397f68e80ce2..55767305809cc 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -4265,10 +4265,7 @@ class FreeTextAnnotation extends MarkupAnnotation { totalWidth = Math.max(totalWidth, lineWidth); } - let hscale = 1; - if (totalWidth > w) { - hscale = w / totalWidth; - } + const hscale = totalWidth > w ? w / totalWidth : 1; let vscale = 1; const lineHeight = LINE_FACTOR * fontSize; const lineAscent = (LINE_FACTOR - LINE_DESCENT_FACTOR) * fontSize; diff --git a/src/core/default_appearance.js b/src/core/default_appearance.js index 43573cef62998..0c1d0f74615d9 100644 --- a/src/core/default_appearance.js +++ b/src/core/default_appearance.js @@ -418,10 +418,7 @@ class FakeUnicodeFont { [w, h] = [h, w]; } - let hscale = 1; - if (maxWidth > w) { - hscale = w / maxWidth; - } + const hscale = maxWidth > w ? w / maxWidth : 1; let vscale = 1; const lineHeight = LINE_FACTOR * fontSize; const lineDescent = LINE_DESCENT_FACTOR * fontSize; diff --git a/src/core/evaluator.js b/src/core/evaluator.js index ca5b702b3d51e..c07284aeebce0 100644 --- a/src/core/evaluator.js +++ b/src/core/evaluator.js @@ -3535,10 +3535,7 @@ class PartialEvaluator { if (includeMarkedContent) { markedContentData.level++; - let mcid = null; - if (args[1] instanceof Dict) { - mcid = args[1].get("MCID"); - } + const mcid = args[1] instanceof Dict ? args[1].get("MCID") : null; textContent.items.push({ type: "beginMarkedContentProps", id: Number.isInteger(mcid) diff --git a/src/core/fonts.js b/src/core/fonts.js index b858e01c26441..dabdb4e84b014 100644 --- a/src/core/fonts.js +++ b/src/core/fonts.js @@ -3171,10 +3171,7 @@ class Font { // there isn't enough room to duplicate, the glyph id is left the same. In // this case, glyph 0 may not work correctly, but that is better than // having the whole font fail. - let glyphZeroId = numGlyphsOut - 1; - if (!dupFirstEntry) { - glyphZeroId = 0; - } + const glyphZeroId = dupFirstEntry ? numGlyphsOut - 1 : 0; // When `cssFontInfo` is set, the font is used to render text in the HTML // view (e.g. with Xfa) so nothing must be moved in the private use area. @@ -3248,10 +3245,7 @@ class Font { // Type 1 fonts have a notdef inserted at the beginning, so glyph 0 // becomes glyph 1. In a CFF font glyph 0 is appended to the end of the // char strings. - let glyphZeroId = 1; - if (font instanceof CFFFont) { - glyphZeroId = font.numGlyphs - 1; - } + const glyphZeroId = font instanceof CFFFont ? font.numGlyphs - 1 : 1; const mapping = font.getGlyphMapping(properties); let newMapping = null; let newCharCodeToGlyphId = mapping; diff --git a/src/core/pattern.js b/src/core/pattern.js index db55561df3bd6..05697fdf0f0c7 100644 --- a/src/core/pattern.js +++ b/src/core/pattern.js @@ -285,10 +285,9 @@ class RadialAxialShading extends BaseShading { } colorStops.push([1, Util.makeHexColor(rPrev, gPrev, bPrev)]); - let background = "transparent"; - if (dict.has("Background")) { - background = cs.getRgbHex(dict.get("Background"), 0); - } + const background = dict.has("Background") + ? cs.getRgbHex(dict.get("Background"), 0) + : "transparent"; if (!extendStart) { // Insert a color stop at the front and offset the first real color stop diff --git a/src/core/type1_parser.js b/src/core/type1_parser.js index eb85dbf46ef10..6a2bf07bd5740 100644 --- a/src/core/type1_parser.js +++ b/src/core/type1_parser.js @@ -691,13 +691,10 @@ class Type1Parser { subrs, this.seacAnalysisEnabled ); - let output = charString.output; - if (error) { - // It seems when FreeType encounters an error while evaluating a glyph - // that it completely ignores the glyph so we'll mimic that behaviour - // here and put an endchar to make the validator happy. - output = [14]; - } + // It seems when FreeType encounters an error while evaluating a glyph + // that it completely ignores the glyph so we'll mimic that behaviour + // here and put an endchar to make the validator happy. + const output = !error ? charString.output : [14]; const charStringObject = { glyphName: glyph, charstring: output, diff --git a/src/core/xfa/factory.js b/src/core/xfa/factory.js index f9ede693adf1a..930b8a071435b 100644 --- a/src/core/xfa/factory.js +++ b/src/core/xfa/factory.js @@ -101,7 +101,6 @@ class XFAFactory { const missingFonts = []; for (let typeface of this.form[$globalData].usedTypefaces) { typeface = stripQuotes(typeface); - // eslint-disable-next-line unicorn/prefer-array-some const font = this.form[$globalData].fontFinder.find(typeface); if (!font) { missingFonts.push(typeface); diff --git a/src/core/xml_parser.js b/src/core/xml_parser.js index 665c46828b41f..7f9948602264b 100644 --- a/src/core/xml_parser.js +++ b/src/core/xml_parser.js @@ -51,7 +51,7 @@ class XMLParserBase { return s.replaceAll(/&([^;]+);/g, (all, entity) => { if (entity.substring(0, 2) === "#x") { return String.fromCodePoint(parseInt(entity.substring(2), 16)); - } else if (entity.substring(0, 1) === "#") { + } else if (entity.at(0) === "#") { return String.fromCodePoint(parseInt(entity.substring(1), 10)); } switch (entity) { diff --git a/src/scripting_api/event.js b/src/scripting_api/event.js index f84d76688934a..8942c78d3b652 100644 --- a/src/scripting_api/event.js +++ b/src/scripting_api/event.js @@ -255,11 +255,9 @@ class EventDispatcher { this.runCalculate(source, event); const savedValue = (event.value = source.obj._getValue()); - let formattedValue = null; - - if (this.runActions(source, source, event, "Format")) { - formattedValue = event.value?.toString?.(); - } + const formattedValue = this.runActions(source, source, event, "Format") + ? event.value?.toString?.() + : null; source.obj._send({ id: source.obj._id, @@ -365,10 +363,9 @@ class EventDispatcher { } savedValue = target.obj._getValue(); - let formattedValue = null; - if (this.runActions(target, target, event, "Format")) { - formattedValue = event.value?.toString?.(); - } + const formattedValue = this.runActions(target, target, event, "Format") + ? event.value?.toString?.() + : null; target.obj._send({ id: target.obj._id, diff --git a/test/resources/reftest-analyzer.js b/test/resources/reftest-analyzer.js index bd69d4dfc33ca..67e742a52bf93 100644 --- a/test/resources/reftest-analyzer.js +++ b/test/resources/reftest-analyzer.js @@ -555,10 +555,11 @@ window.onload = function () { window.addEventListener("keydown", function keydown(event) { if (event.which === 84) { // 't' switch test/ref images - let val = 0; - if (document.querySelector('input[name="which"][value="0"]:checked')) { - val = 1; - } + const val = document.querySelector( + 'input[name="which"][value="0"]:checked' + ) + ? 1 + : 0; document .querySelector('input[name="which"][value="' + val + '"]') .click();