From 2b57ef4b56cd7ca381ad08b5b5dd8e0a1b7ead20 Mon Sep 17 00:00:00 2001 From: calixteman Date: Sun, 15 Feb 2026 22:05:28 +0100 Subject: [PATCH 1/3] Fix code coverage line mapping When checking the code coverage report, it was noticed that the line numbers were off. It was due to the fact that the files used for coverage were the transpiled ones, when the ones used by Codecov were the original ones. So this patches adds the source maps to the transpiled files, and also updates the license header in the original files in using a babel plugin in order to make sure the line numbers are correct. As a side effect of this work, it's now possible to have the correct line numbers in the stack traces when running tests with the transpiled files. --- gulpfile.mjs | 112 +++++++++--- package-lock.json | 421 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 2 + 3 files changed, 508 insertions(+), 27 deletions(-) diff --git a/gulpfile.mjs b/gulpfile.mjs index 01f9d64ed915c..87d9757385b01 100644 --- a/gulpfile.mjs +++ b/gulpfile.mjs @@ -40,6 +40,7 @@ import { preprocess } from "./external/builder/builder.mjs"; import relative from "metalsmith-html-relative"; import rename from "gulp-rename"; import replace from "gulp-replace"; +import sourcemaps from "gulp-sourcemaps"; import stream from "stream"; import TerserPlugin from "terser-webpack-plugin"; import Vinyl from "vinyl"; @@ -1526,27 +1527,13 @@ gulp.task("types", function (done) { }); function buildLibHelper(bundleDefines, inputStream, outputDir) { - function preprocessLib(content) { - const skipBabel = bundleDefines.SKIP_BABEL; - content = babel.transform(content, { - sourceType: "module", - presets: skipBabel - ? undefined - : [ - [ - "@babel/preset-env", - { ...BABEL_PRESET_ENV_OPTS, loose: false, modules: false }, - ], - ], - plugins: [[babelPluginPDFJSPreprocessor, ctx]], - targets: BABEL_TARGETS, - }).code; - content = content.replaceAll( - /(\sfrom\s".*?)(?:\/src)(\/[^"]*"?;)$/gm, - (all, prefix, suffix) => prefix + suffix - ); - return licenseHeaderLibre + content; - } + const licenseHeader = fs + .readFileSync("./src/license_header.js") + .toString() + .split("\n") + .slice(1, -2) + .map(line => line.replace(/^\s*\*\s?/, "")); + const ctx = { rootPath: __dirname, defines: bundleDefines, @@ -1564,12 +1551,82 @@ function buildLibHelper(bundleDefines, inputStream, outputDir) { "web-null_l10n": "../web/genericl10n.js", }, }; - const licenseHeaderLibre = fs - .readFileSync("./src/license_header_libre.js") - .toString(); - return inputStream - .pipe(transform("utf8", preprocessLib)) - .pipe(gulp.dest(outputDir)); + const enableSourceMaps = bundleDefines.TESTING; + + function preprocessLib(file, _enc, callback) { + const skipBabel = bundleDefines.SKIP_BABEL; + + if (file.isNull()) { + return callback(null, file); + } + + if (file.isStream()) { + return callback(new Error("Streaming not supported")); + } + + try { + // Calculate where the output file will be + const outputFilePath = path.join(__dirname, outputDir, file.relative); + const outputFileDir = path.dirname(outputFilePath); + // Calculate relative path from output directory to source file + const relativeSourcePath = path.relative(outputFileDir, file.path); + + const result = babel.transform(file.contents.toString(), { + sourceType: "module", + presets: skipBabel + ? undefined + : [ + [ + "@babel/preset-env", + { ...BABEL_PRESET_ENV_OPTS, loose: false, modules: false }, + ], + ], + plugins: [ + [babelPluginPDFJSPreprocessor, ctx], + [ + "add-header-comment", + { + header: licenseHeader, + }, + ], + ], + targets: BABEL_TARGETS, + sourceMaps: enableSourceMaps, + sourceFileName: relativeSourcePath, + }); + + let code = result.code; + code = code.replaceAll( + /(\sfrom\s".*?)(?:\/src)(\/[^"]*"?;)$/gm, + (all, prefix, suffix) => prefix + suffix + ); + + file.contents = Buffer.from(code); + // Attach the source map to the file for gulp-sourcemaps + if (result.map) { + file.sourceMap = result.map; + } + + return callback(null, file); + } catch (err) { + return callback(err); + } + } + + let pipeline = inputStream; + if (enableSourceMaps) { + pipeline = pipeline.pipe(sourcemaps.init({ loadMaps: true })); + } + pipeline = pipeline.pipe( + new stream.Transform({ + objectMode: true, + transform: preprocessLib, + }) + ); + if (enableSourceMaps) { + pipeline = pipeline.pipe(sourcemaps.write(".")); + } + return pipeline.pipe(gulp.dest(outputDir)); } function buildLib(defines, dir) { @@ -1956,6 +2013,7 @@ gulp.task( jasmineProcess = spawn("node", options, { stdio: "inherit" }); } else { const options = [ + "--enable-source-maps", "node_modules/jasmine/bin/jasmine", "JASMINE_CONFIG_PATH=test/unit/clitests.json", ]; diff --git a/package-lock.json b/package-lock.json index ab5b52e6ed0d8..4f44e354f008f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "@types/node": "^25.2.2", "autoprefixer": "^10.4.24", "babel-loader": "^10.0.0", + "babel-plugin-add-header-comment": "^1.0.3", "c8": "^10.1.3", "cached-iterable": "^0.3.0", "caniuse-lite": "^1.0.30001769", @@ -38,6 +39,7 @@ "gulp-postcss": "^10.0.0", "gulp-rename": "^2.1.0", "gulp-replace": "^1.1.4", + "gulp-sourcemaps": "^3.0.0", "gulp-zip": "^6.1.0", "highlight.js": "^11.11.1", "jasmine": "^5.13.0", @@ -2135,6 +2137,110 @@ "npm": ">=7.0.0" } }, + "node_modules/@gulp-sourcemaps/identity-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@gulp-sourcemaps/identity-map/-/identity-map-2.0.1.tgz", + "integrity": "sha512-Tb+nSISZku+eQ4X1lAkevcQa+jknn/OVUgZ3XCxEKIsLsqYuPoJwJOPQeaOk75X3WPftb29GWY1eqE7GLsXb1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^6.4.1", + "normalize-path": "^3.0.0", + "postcss": "^7.0.16", + "source-map": "^0.6.0", + "through2": "^3.0.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@gulp-sourcemaps/identity-map/node_modules/acorn": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", + "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/@gulp-sourcemaps/identity-map/node_modules/picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "dev": true, + "license": "ISC" + }, + "node_modules/@gulp-sourcemaps/identity-map/node_modules/postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + } + }, + "node_modules/@gulp-sourcemaps/identity-map/node_modules/through2": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", + "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "2 || 3" + } + }, + "node_modules/@gulp-sourcemaps/map-sources": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@gulp-sourcemaps/map-sources/-/map-sources-1.0.0.tgz", + "integrity": "sha512-o/EatdaGt8+x2qpb0vFLC/2Gug/xYPRXb6a+ET1wGYKozKN3krDWC/zZFZAtrzxJHuDL12mwdfEFKcKMNvc55A==", + "dev": true, + "license": "MIT", + "dependencies": { + "normalize-path": "^2.0.1", + "through2": "^2.0.3" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@gulp-sourcemaps/map-sources/node_modules/normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "remove-trailing-separator": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@gulp-sourcemaps/map-sources/node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, "node_modules/@gulpjs/messages": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@gulpjs/messages/-/messages-1.1.0.tgz", @@ -3826,6 +3932,19 @@ "node": ">= 10.13.0" } }, + "node_modules/atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true, + "license": "(MIT OR Apache-2.0)", + "bin": { + "atob": "bin/atob.js" + }, + "engines": { + "node": ">= 4.5.0" + } + }, "node_modules/autoprefixer": { "version": "10.4.24", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.24.tgz", @@ -3911,6 +4030,13 @@ "webpack": ">=5.61.0" } }, + "node_modules/babel-plugin-add-header-comment": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/babel-plugin-add-header-comment/-/babel-plugin-add-header-comment-1.0.3.tgz", + "integrity": "sha512-AYzYg2cGNYfQNPfJU0YHxPxBArEVilJPe3cMJ+g6c3oly2GY1tTfTLaaMqVRd67aiFEPYyHF6dqYjxPHpZ53ow==", + "dev": true, + "license": "MIT" + }, "node_modules/babel-plugin-polyfill-corejs2": { "version": "0.4.15", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.15.tgz", @@ -5131,6 +5257,18 @@ "node": ">= 8" } }, + "node_modules/css": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/css/-/css-3.0.0.tgz", + "integrity": "sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "source-map": "^0.6.1", + "source-map-resolve": "^0.6.0" + } + }, "node_modules/css-functions-list": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/css-functions-list/-/css-functions-list-3.2.3.tgz", @@ -5198,6 +5336,20 @@ "node": ">=4" } }, + "node_modules/d": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", + "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", + "dev": true, + "license": "ISC", + "dependencies": { + "es5-ext": "^0.10.64", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.12" + } + }, "node_modules/data-uri-to-buffer": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", @@ -5280,6 +5432,28 @@ } } }, + "node_modules/debug-fabulous": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/debug-fabulous/-/debug-fabulous-1.1.0.tgz", + "integrity": "sha512-GZqvGIgKNlUnHUPQhepnUZFIMoi3dgZKQBzKDeL2g7oJF9SNAji/AAu36dusFUas0O+pae74lNeoIPHqXWDkLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "3.X", + "memoizee": "0.4.X", + "object-assign": "4.X" + } + }, + "node_modules/debug-fabulous/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, "node_modules/decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", @@ -5317,6 +5491,16 @@ "node": ">=0.10.0" } }, + "node_modules/decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -5405,6 +5589,16 @@ "node": ">=4" } }, + "node_modules/detect-newline": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", + "integrity": "sha512-CwffZFvlJffUg9zZA0uqrjQayUTC8ob94pnr5sFwaVv3IOmkfUHcWH+jXaQK3askE51Cqe8/9Ql/0uXNwqZ8Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/devtools-protocol": { "version": "0.0.1566079", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1566079.tgz", @@ -5841,6 +6035,62 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es5-ext": { + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", + "dev": true, + "hasInstallScript": true, + "license": "ISC", + "dependencies": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "dev": true, + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/es6-symbol": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", + "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "^1.0.2", + "ext": "^1.7.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/es6-weak-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", + "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.46", + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.1" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -6233,6 +6483,22 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/espree": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", @@ -6311,6 +6577,17 @@ "node": ">=0.10.0" } }, + "node_modules/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -6417,6 +6694,16 @@ "node": ">=0.10.0" } }, + "node_modules/ext": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", + "dev": true, + "license": "ISC", + "dependencies": { + "type": "^2.7.2" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -7496,6 +7783,60 @@ "node": ">=10" } }, + "node_modules/gulp-sourcemaps": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/gulp-sourcemaps/-/gulp-sourcemaps-3.0.0.tgz", + "integrity": "sha512-RqvUckJkuYqy4VaIH60RMal4ZtG0IbQ6PXMNkNsshEGJ9cldUPRb/YCgboYae+CLAs1HQNb4ADTKCx65HInquQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@gulp-sourcemaps/identity-map": "^2.0.1", + "@gulp-sourcemaps/map-sources": "^1.0.0", + "acorn": "^6.4.1", + "convert-source-map": "^1.0.0", + "css": "^3.0.0", + "debug-fabulous": "^1.0.0", + "detect-newline": "^2.0.0", + "graceful-fs": "^4.0.0", + "source-map": "^0.6.0", + "strip-bom-string": "^1.0.0", + "through2": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gulp-sourcemaps/node_modules/acorn": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", + "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/gulp-sourcemaps/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/gulp-sourcemaps/node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, "node_modules/gulp-zip": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/gulp-zip/-/gulp-zip-6.1.0.tgz", @@ -9201,6 +9542,16 @@ "yallist": "^3.0.2" } }, + "node_modules/lru-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", + "integrity": "sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es5-ext": "~0.10.2" + } + }, "node_modules/make-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", @@ -9326,6 +9677,26 @@ "dev": true, "license": "MIT" }, + "node_modules/memoizee": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.17.tgz", + "integrity": "sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "^1.0.2", + "es5-ext": "^0.10.64", + "es6-weak-map": "^2.0.3", + "event-emitter": "^0.3.5", + "is-promise": "^2.2.2", + "lru-queue": "^0.1.0", + "next-tick": "^1.1.0", + "timers-ext": "^0.1.7" + }, + "engines": { + "node": ">=0.12" + } + }, "node_modules/memory-fs": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", @@ -9652,6 +10023,13 @@ "node": ">= 0.4.0" } }, + "node_modules/next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", + "dev": true, + "license": "ISC" + }, "node_modules/node-readable-to-web-readable-stream": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/node-readable-to-web-readable-stream/-/node-readable-to-web-readable-stream-0.4.2.tgz", @@ -11861,6 +12239,18 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-resolve": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz", + "integrity": "sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==", + "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", + "dev": true, + "license": "MIT", + "dependencies": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0" + } + }, "node_modules/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", @@ -12999,6 +13389,20 @@ "node": ">=0.10.0" } }, + "node_modules/timers-ext": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.8.tgz", + "integrity": "sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==", + "dev": true, + "license": "ISC", + "dependencies": { + "es5-ext": "^0.10.64", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.12" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -13173,6 +13577,13 @@ "summary": "^2.0.0" } }, + "node_modules/type": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", + "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", + "dev": true, + "license": "ISC" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -14275,6 +14686,16 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index f03691f45c12f..03ca97ee2ac41 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@types/node": "^25.2.2", "autoprefixer": "^10.4.24", "babel-loader": "^10.0.0", + "babel-plugin-add-header-comment": "^1.0.3", "c8": "^10.1.3", "cached-iterable": "^0.3.0", "caniuse-lite": "^1.0.30001769", @@ -33,6 +34,7 @@ "gulp-postcss": "^10.0.0", "gulp-rename": "^2.1.0", "gulp-replace": "^1.1.4", + "gulp-sourcemaps": "^3.0.0", "gulp-zip": "^6.1.0", "highlight.js": "^11.11.1", "jasmine": "^5.13.0", From f01e4d477e244d4ad591de0c86e3959db3d32c4c Mon Sep 17 00:00:00 2001 From: Matthew Lawrence Date: Tue, 17 Feb 2026 13:46:02 +1100 Subject: [PATCH 2/3] fix: prevent search highlights from interfering with drag-selection --- test/integration/text_layer_spec.mjs | 74 ++++++++++++++++++++++++++++ web/text_layer_builder.js | 3 ++ 2 files changed, 77 insertions(+) diff --git a/test/integration/text_layer_spec.mjs b/test/integration/text_layer_spec.mjs index 47799fef4bd3f..f54f24289b5de 100644 --- a/test/integration/text_layer_spec.mjs +++ b/test/integration/text_layer_spec.mjs @@ -432,6 +432,80 @@ describe("Text layer", () => { ); }); }); + + describe("when selecting text with find highlights active", () => { + let pages; + + beforeEach(async () => { + pages = await loadAndWait("find_all.pdf", ".textLayer", 100); + }); + + afterEach(async () => { + await closePages(pages); + }); + + it("doesn't jump when selection anchor is inside a highlight element", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + // Highlight all occurrences of the letter A (case insensitive). + await page.click("#viewFindButton"); + await page.waitForSelector("#findInput", { visible: true }); + await page.type("#findInput", "a"); + await page.click("#findHighlightAll + label"); + await page.waitForSelector(".textLayer .highlight"); + + // find_all.pdf contains 'AB BA' in a monospace font. These are + // the glyph metrics at 100% zoom, extracted from the PDF. + const glyphWidth = 15.98; + const expectedFirstAX = 30; + + // Compute the drag coordinates to select exactly "AB". The + // horizontal positions use the page origin and PDF glyph + // metrics; the vertical center comes from the highlight. + const pageDiv = await page.$(".page canvas"); + const pageBox = await pageDiv.boundingBox(); + const firstHighlight = await page.$(".textLayer .highlight"); + const highlightBox = await firstHighlight.boundingBox(); + + // Drag from beginning of first 'A' to end of second 'B' + const aStart = pageBox.x + expectedFirstAX; + const startY = Math.round( + highlightBox.y + highlightBox.height / 2 + ); + const bEnd = Math.round(aStart + glyphWidth * 2); + + await page.mouse.move(aStart, startY); + await page.mouse.down(); + await moveInSteps( + page, + { x: aStart, y: startY }, + { x: bEnd, y: startY }, + 20 + ); + await page.mouse.up(); + + const selection = await page.evaluate(() => + window.getSelection().toString() + ); + expect(selection).withContext(`In ${browserName}`).toEqual("AB"); + + // The selectionchange handler in TextLayerBuilder walks up + // from .highlight to its parent span before placing + // endOfContent (see text_layer_builder.js). Without that + // fix, endOfContent would be inserted inside the text span + // (as a sibling of the .highlight) instead of as a direct + // child of .textLayer. Verify the correct DOM structure. + const endOfContentIsDirectChild = await page.evaluate(() => { + const eoc = document.querySelector(".textLayer .endOfContent"); + return eoc?.parentElement?.classList.contains("textLayer"); + }); + expect(endOfContentIsDirectChild) + .withContext(`In ${browserName}`) + .toBeTrue(); + }) + ); + }); + }); }); describe("using selection carets", () => { diff --git a/web/text_layer_builder.js b/web/text_layer_builder.js index 184c244a3d90a..8fe0110abb6be 100644 --- a/web/text_layer_builder.js +++ b/web/text_layer_builder.js @@ -307,6 +307,9 @@ class TextLayerBuilder { if (anchor.nodeType === Node.TEXT_NODE) { anchor = anchor.parentNode; } + if (anchor.classList?.contains("highlight")) { + anchor = anchor.parentNode; + } if (!modifyStart && range.endOffset === 0) { do { while (!anchor.previousSibling) { From f4a2fd60db0fff3a0187516e19ae7b03c6053447 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Tue, 17 Feb 2026 10:28:11 +0100 Subject: [PATCH 3/3] Fix the keyboard accessibility of the manage button in the thumbnails view (bug 2015916) --- test/integration/thumbnail_view_spec.mjs | 116 +++++++++++++++++++++++ web/menu.js | 66 +++++++------ web/viewer.html | 8 +- 3 files changed, 159 insertions(+), 31 deletions(-) diff --git a/test/integration/thumbnail_view_spec.mjs b/test/integration/thumbnail_view_spec.mjs index 8232ef20d619c..5b35956f6be58 100644 --- a/test/integration/thumbnail_view_spec.mjs +++ b/test/integration/thumbnail_view_spec.mjs @@ -12,6 +12,21 @@ function waitForThumbnailVisible(page, pageNum) { ); } +async function waitForMenu(page, buttonSelector, visible = true) { + return page.waitForFunction( + (selector, vis) => { + const button = document.querySelector(selector); + if (!button) { + return false; + } + return button.getAttribute("aria-expanded") === (vis ? "true" : "false"); + }, + {}, + buttonSelector, + visible + ); +} + describe("PDF Thumbnail View", () => { describe("Works without errors", () => { let pages; @@ -201,4 +216,105 @@ describe("PDF Thumbnail View", () => { ); }); }); + + describe("The manage dropdown menu", () => { + let pages; + + beforeEach(async () => { + pages = await loadAndWait( + "tracemonkey.pdf", + "#viewsManagerToggleButton", + null, + null, + { enableSplitMerge: true } + ); + }); + + afterEach(async () => { + await closePages(pages); + }); + + async function enableMenuItems(page) { + await page.evaluate(() => { + document + .querySelectorAll("#viewsManagerStatusActionOptions button") + .forEach(button => { + button.disabled = false; + }); + }); + } + + it("should open with Enter key and remain open", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await page.click("#viewsManagerToggleButton"); + await waitForThumbnailVisible(page, 1); + + await enableMenuItems(page); + + // Focus the manage button + await kbFocusNext(page); + await kbFocusNext(page); + await page.waitForSelector("#viewsManagerStatusActionButton:focus", { + visible: true, + }); + + // Press Enter to open the menu + await page.keyboard.press("Enter"); + + await waitForMenu(page, "#viewsManagerStatusActionButton"); + + // Verify first menu item can be focused + await page.waitForSelector("#viewsManagerStatusActionCopy:focus", { + visible: true, + }); + + // Close menu with Escape + await page.keyboard.press("Escape"); + await waitForMenu(page, "#viewsManagerStatusActionButton", false); + }) + ); + }); + + it("should open with Space key and remain open", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await page.click("#viewsManagerToggleButton"); + await waitForThumbnailVisible(page, 1); + + await enableMenuItems(page); + + // Focus the manage button + await kbFocusNext(page); + await kbFocusNext(page); + await page.waitForSelector("#viewsManagerStatusActionButton:focus", { + visible: true, + }); + + // Press Space to open the menu + await page.keyboard.press(" "); + + await waitForMenu(page, "#viewsManagerStatusActionButton"); + + // Verify first menu item can be focused + await page.waitForSelector("#viewsManagerStatusActionCopy:focus", { + visible: true, + }); + + // Navigate menu items with arrow keys + await page.keyboard.press("ArrowDown"); + await page.waitForSelector("#viewsManagerStatusActionCut:focus", { + visible: true, + }); + + // Menu should still be open + await waitForMenu(page, "#viewsManagerStatusActionButton"); + + // Close menu with Escape + await page.keyboard.press("Escape"); + await waitForMenu(page, "#viewsManagerStatusActionButton", false); + }) + ); + }); + }); }); diff --git a/web/menu.js b/web/menu.js index 27218093da33b..de917c6ec6689 100644 --- a/web/menu.js +++ b/web/menu.js @@ -70,6 +70,36 @@ class Menu { this.#lastIndex = -1; } + /** + * Open the menu. + */ + #openMenu() { + if (this.#openMenuAC) { + return; + } + + const menu = this.#menu; + this.#triggeringButton.ariaExpanded = "true"; + this.#openMenuAC = new AbortController(); + const signal = AbortSignal.any([ + this.#menuAC.signal, + this.#openMenuAC.signal, + ]); + window.addEventListener( + "pointerdown", + ({ target }) => { + if ( + !this.#triggeringButton.contains(target) && + !menu.contains(target) + ) { + this.#closeMenu(); + } + }, + { signal } + ); + window.addEventListener("blur", this.#closeMenu.bind(this), { signal }); + } + /** * Set up the menu. */ @@ -80,23 +110,7 @@ class Menu { return; } - const menu = this.#menu; - this.#triggeringButton.ariaExpanded = "true"; - this.#openMenuAC = new AbortController(); - const signal = AbortSignal.any([ - this.#menuAC.signal, - this.#openMenuAC.signal, - ]); - window.addEventListener( - "pointerdown", - ({ target }) => { - if (target !== this.#triggeringButton && !menu.contains(target)) { - this.#closeMenu(); - } - }, - { signal } - ); - window.addEventListener("blur", this.#closeMenu.bind(this), { signal }); + this.#openMenu(); }); const { signal } = this.#menuAC; @@ -110,12 +124,10 @@ class Menu { stopEvent(e); break; case "ArrowDown": - case "Tab": this.#goToNextItem(e.target, true); stopEvent(e); break; case "ArrowUp": - case "ShiftTab": this.#goToNextItem(e.target, false); stopEvent(e); break; @@ -124,7 +136,7 @@ class Menu { .find( item => !item.disabled && !item.classList.contains("hidden") ) - .focus(); + ?.focus(); stopEvent(e); break; case "End": @@ -132,7 +144,7 @@ class Menu { .findLast( item => !item.disabled && !item.classList.contains("hidden") ) - .focus(); + ?.focus(); stopEvent(e); break; default: @@ -159,27 +171,27 @@ class Menu { case "Enter": case "ArrowDown": case "Home": + stopEvent(e); if (!this.#openMenuAC) { - this.#triggeringButton.click(); + this.#openMenu(); } this.#menuItems .find( item => !item.disabled && !item.classList.contains("hidden") ) - .focus(); - stopEvent(e); + ?.focus(); break; case "ArrowUp": case "End": + stopEvent(e); if (!this.#openMenuAC) { - this.#triggeringButton.click(); + this.#openMenu(); } this.#menuItems .findLast( item => !item.disabled && !item.classList.contains("hidden") ) - .focus(); - stopEvent(e); + ?.focus(); break; case "Escape": this.#closeMenu(); diff --git a/web/viewer.html b/web/viewer.html index 42b2345e7cc5e..bd521d6487f5c 100644 --- a/web/viewer.html +++ b/web/viewer.html @@ -207,22 +207,22 @@
  • -
  • -
  • -
  • -