From 812a29d482469dc2370404abeb5fdadef66e56d9 Mon Sep 17 00:00:00 2001 From: AutomatedTester Date: Thu, 14 May 2026 20:25:32 +0100 Subject: [PATCH 1/3] [javascript] Ship @seleniumhq/atoms npm package from TypeScript atoms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactors the three TypeScript atoms (get-attribute, find-elements, is-displayed) from IIFE closures to proper ES modules with export default, then produces two compilation targets from the same sources: - IIFE path (language bindings): ts_project --module commonjs → cjs-to-iife.js → browser-injectable (function(){…return fn;})() so py/rb/java/dotnet consumers are unchanged. - npm path: ts_project --module esnext + declaration → dist/ published as @seleniumhq/atoms with full TypeScript type declarations. Key changes: * Remove outer IIFE from all three atom .ts files; add export default. * Add cjs-to-iife.js replacing strip-trailing-semicolon.js in the IIFE pipeline. Strips CJS boilerplate and wraps the body in an IIFE that returns the exported default function. * Update wrap-as-global scripts to parse CJS-compiled input instead of asserting (function () at the start. * Switch BUILD.bazel from js_run_binary+tsc(--module none --outFile) to ts_project for each atom, plus a new npm_package target. * Add tsconfig.json (esnext, declaration) and tsconfig-cjs.json for the two compilation paths. * Add index.ts as the package entry point. * Promote javascript/atoms/package.json to @seleniumhq/atoms v4.45.x. * Add get_attribute_typescript and find-elements-typescript CJS module targets to selenium-webdriver/lib/atoms/BUILD.bazel. All 25 Python unit tests pass; //javascript/atoms:closure-test passes. --- javascript/atoms/BUILD.bazel | 170 ++++++++---------- .../strip-trailing-semicolon.js => index.ts} | 14 +- javascript/atoms/package.json | 41 ++++- javascript/atoms/tsconfig.json | 13 ++ javascript/atoms/typescript/cjs-to-iife.js | 71 ++++++++ javascript/atoms/typescript/find-elements.ts | 6 +- javascript/atoms/typescript/get-attribute.ts | 16 +- javascript/atoms/typescript/is-displayed.ts | 6 +- javascript/atoms/typescript/tsconfig-cjs.json | 9 + javascript/atoms/typescript/wrap-as-global.js | 35 +++- .../wrap-find-elements-as-global.js | 32 +++- .../wrap-get-attribute-as-global.js | 35 +++- 12 files changed, 312 insertions(+), 136 deletions(-) rename javascript/atoms/{typescript/strip-trailing-semicolon.js => index.ts} (71%) create mode 100644 javascript/atoms/tsconfig.json create mode 100644 javascript/atoms/typescript/cjs-to-iife.js create mode 100644 javascript/atoms/typescript/tsconfig-cjs.json diff --git a/javascript/atoms/BUILD.bazel b/javascript/atoms/BUILD.bazel index 19db8e3fe08c7..094e573bb8263 100644 --- a/javascript/atoms/BUILD.bazel +++ b/javascript/atoms/BUILD.bazel @@ -1,13 +1,15 @@ load("@aspect_rules_js//js:defs.bzl", "js_binary", "js_run_binary") +load("@aspect_rules_js//npm:defs.bzl", "npm_package") +load("@aspect_rules_ts//ts:defs.bzl", "ts_project") load("@rules_closure//closure:defs.bzl", "closure_js_library") load("//javascript:defs.bzl", "closure_js_deps", "closure_test_suite") package(default_visibility = ["//visibility:public"]) js_binary( - name = "strip_trailing_semicolon", - data = ["typescript/strip-trailing-semicolon.js"], - entry_point = "typescript/strip-trailing-semicolon.js", + name = "cjs_to_iife", + data = ["typescript/cjs-to-iife.js"], + entry_point = "typescript/cjs-to-iife.js", ) js_binary( @@ -16,159 +18,143 @@ js_binary( entry_point = "typescript/wrap-get-attribute-as-global.js", ) -js_run_binary( +js_binary( + name = "wrap_as_global", + data = ["typescript/wrap-as-global.js"], + entry_point = "typescript/wrap-as-global.js", +) + +js_binary( + name = "wrap_find_elements_as_global", + data = ["typescript/wrap-find-elements-as-global.js"], + entry_point = "typescript/wrap-find-elements-as-global.js", +) + +# ── get-attribute ──────────────────────────────────────────────────────────── + +ts_project( name = "get-attribute-typescript-compiled", srcs = ["typescript/get-attribute.ts"], - outs = ["typescript/get-attribute.compiled.js"], - args = [ - "--target", - "ES2017", - "--module", - "none", - "--moduleResolution", - "node", - # These atoms are self-contained and import nothing; pin typeRoots to the (type-free) - # source dir so unrelated ambient @types packages in node_modules cannot break the build. - "--typeRoots", - "javascript/atoms/typescript", - "--removeComments", - "--pretty", - "false", - "--outFile", - "$(rootpath :typescript/get-attribute.compiled.js)", - "$(rootpath :typescript/get-attribute.ts)", - ], - tool = "@npm_typescript//:tsc", + declaration = False, + source_map = False, + tsconfig = "typescript/tsconfig-cjs.json", ) js_run_binary( name = "get-attribute-typescript-generated", - srcs = [":get-attribute-typescript-compiled"], + srcs = [":typescript/get-attribute.js"], outs = ["typescript/get-attribute.generated.js"], args = [ - "$(rootpath :get-attribute-typescript-compiled)", + "$(rootpath :typescript/get-attribute.js)", "$(rootpath :typescript/get-attribute.generated.js)", ], - tool = ":strip_trailing_semicolon", + tool = ":cjs_to_iife", ) js_run_binary( name = "get-attribute-global", - srcs = [":get-attribute-typescript-compiled"], + srcs = [":typescript/get-attribute.js"], outs = ["typescript/get-attribute-global.js"], args = [ - "$(rootpath :get-attribute-typescript-compiled)", + "$(rootpath :typescript/get-attribute.js)", "$(rootpath :typescript/get-attribute-global.js)", ], tool = ":wrap_get_attribute_as_global", ) -js_binary( - name = "wrap_as_global", - data = ["typescript/wrap-as-global.js"], - entry_point = "typescript/wrap-as-global.js", -) - -js_binary( - name = "wrap_find_elements_as_global", - data = ["typescript/wrap-find-elements-as-global.js"], - entry_point = "typescript/wrap-find-elements-as-global.js", -) +# ── find-elements ──────────────────────────────────────────────────────────── -js_run_binary( +ts_project( name = "find-elements-typescript-compiled", srcs = ["typescript/find-elements.ts"], - outs = ["typescript/find-elements.compiled.js"], - args = [ - "--target", - "ES2017", - "--module", - "none", - "--moduleResolution", - "node", - # These atoms are self-contained and import nothing; pin typeRoots to the (type-free) - # source dir so unrelated ambient @types packages in node_modules cannot break the build. - "--typeRoots", - "javascript/atoms/typescript", - "--removeComments", - "--pretty", - "false", - "--outFile", - "$(rootpath :typescript/find-elements.compiled.js)", - "$(rootpath :typescript/find-elements.ts)", - ], - tool = "@npm_typescript//:tsc", + declaration = False, + source_map = False, + tsconfig = "typescript/tsconfig-cjs.json", ) js_run_binary( name = "find-elements-typescript-generated", - srcs = [":find-elements-typescript-compiled"], + srcs = [":typescript/find-elements.js"], outs = ["typescript/find-elements.generated.js"], args = [ - "$(rootpath :find-elements-typescript-compiled)", + "$(rootpath :typescript/find-elements.js)", "$(rootpath :typescript/find-elements.generated.js)", ], - tool = ":strip_trailing_semicolon", + tool = ":cjs_to_iife", ) js_run_binary( name = "find-elements-global", - srcs = [":find-elements-typescript-compiled"], + srcs = [":typescript/find-elements.js"], outs = ["typescript/find-elements-global.js"], args = [ - "$(rootpath :find-elements-typescript-compiled)", + "$(rootpath :typescript/find-elements.js)", "$(rootpath :typescript/find-elements-global.js)", ], tool = ":wrap_find_elements_as_global", ) -js_run_binary( +# ── is-displayed ───────────────────────────────────────────────────────────── + +ts_project( name = "is-displayed-typescript-compiled", srcs = ["typescript/is-displayed.ts"], - outs = ["typescript/is-displayed.compiled.js"], - args = [ - "--target", - "ES2017", - "--module", - "none", - "--moduleResolution", - "node", - # These atoms are self-contained and import nothing; pin typeRoots to the (type-free) - # source dir so unrelated ambient @types packages in node_modules cannot break the build. - "--typeRoots", - "javascript/atoms/typescript", - "--removeComments", - "--pretty", - "false", - "--outFile", - "$(rootpath :typescript/is-displayed.compiled.js)", - "$(rootpath typescript/is-displayed.ts)", - ], - tool = "@npm_typescript//:tsc", + declaration = False, + source_map = False, + tsconfig = "typescript/tsconfig-cjs.json", ) js_run_binary( name = "is-displayed-typescript-generated", - srcs = [":is-displayed-typescript-compiled"], + srcs = [":typescript/is-displayed.js"], outs = ["typescript/is-displayed.generated.js"], args = [ - "$(rootpath :is-displayed-typescript-compiled)", + "$(rootpath :typescript/is-displayed.js)", "$(rootpath :typescript/is-displayed.generated.js)", ], - tool = ":strip_trailing_semicolon", + tool = ":cjs_to_iife", ) js_run_binary( name = "is-displayed-global", - srcs = [":is-displayed-typescript-compiled"], + srcs = [":typescript/is-displayed.js"], outs = ["typescript/is-displayed-global.js"], args = [ - "$(rootpath :is-displayed-typescript-compiled)", + "$(rootpath :typescript/is-displayed.js)", "$(rootpath :typescript/is-displayed-global.js)", ], tool = ":wrap_as_global", ) +# ── @seleniumhq/atoms npm package ──────────────────────────────────────────── + +ts_project( + name = "atoms-npm-compiled", + srcs = [ + "index.ts", + "typescript/find-elements.ts", + "typescript/get-attribute.ts", + "typescript/is-displayed.ts", + ], + declaration = True, + declaration_map = True, + out_dir = "dist", + source_map = True, + tsconfig = "tsconfig.json", +) + +npm_package( + name = "selenium-atoms-package", + srcs = [ + "package.json", + ":atoms-npm-compiled", + ], + package = "@seleniumhq/atoms", + publishable = True, + tags = ["release-artifact"], + visibility = ["//visibility:public"], +) + filegroup( name = "atoms", srcs = glob([ diff --git a/javascript/atoms/typescript/strip-trailing-semicolon.js b/javascript/atoms/index.ts similarity index 71% rename from javascript/atoms/typescript/strip-trailing-semicolon.js rename to javascript/atoms/index.ts index 249b06b4ef074..c0c2f011726c8 100644 --- a/javascript/atoms/typescript/strip-trailing-semicolon.js +++ b/javascript/atoms/index.ts @@ -15,14 +15,6 @@ // specific language governing permissions and limitations // under the License. -const fs = require('node:fs'); - -const [inputPath, outputPath] = process.argv.slice(2); - -if (!inputPath || !outputPath) { - throw new Error('Expected input and output file paths'); -} - -const input = fs.readFileSync(inputPath, 'utf8'); -const output = input.replace(/;\s*$/, ''); -fs.writeFileSync(outputPath, output); +export { default as getAttribute } from './typescript/get-attribute' +export { default as findElements } from './typescript/find-elements' +export { default as isDisplayed } from './typescript/is-displayed' diff --git a/javascript/atoms/package.json b/javascript/atoms/package.json index b7f7d7dcbea01..908271bb5d31c 100644 --- a/javascript/atoms/package.json +++ b/javascript/atoms/package.json @@ -1,13 +1,42 @@ { - "name": "selenium-atoms", - "version": "1.0.0", - "private": true, - "description": "Build tools for Selenium Browser Automation Atoms", - "license": "Apache-2.0", + "name": "@seleniumhq/atoms", + "version": "4.45.0-nightly202605121853", + "description": "Browser injection atoms for Selenium WebDriver", + "type": "module", + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.js", + "require": "./dist/index.cjs", + "types": "./dist/index.d.ts" + }, + "./get-attribute": { + "import": "./dist/typescript/get-attribute.js", + "require": "./dist/typescript/get-attribute.cjs", + "types": "./dist/typescript/get-attribute.d.ts" + }, + "./find-elements": { + "import": "./dist/typescript/find-elements.js", + "require": "./dist/typescript/find-elements.cjs", + "types": "./dist/typescript/find-elements.d.ts" + }, + "./is-displayed": { + "import": "./dist/typescript/is-displayed.js", + "require": "./dist/typescript/is-displayed.cjs", + "types": "./dist/typescript/is-displayed.d.ts" + } + }, + "files": [ + "dist/" + ], "repository": { "type": "git", - "url": "https://github.com/SeleniumHQ/selenium.git" + "url": "https://github.com/SeleniumHQ/selenium.git", + "directory": "javascript/atoms" }, + "license": "Apache-2.0", "devDependencies": { "google-closure-deps": "^20230802.0.0" } diff --git a/javascript/atoms/tsconfig.json b/javascript/atoms/tsconfig.json new file mode 100644 index 0000000000000..fef482b502866 --- /dev/null +++ b/javascript/atoms/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2017", + "module": "esnext", + "moduleResolution": "node", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "dist", + "strict": false + }, + "include": ["index.ts", "typescript/*.ts"] +} diff --git a/javascript/atoms/typescript/cjs-to-iife.js b/javascript/atoms/typescript/cjs-to-iife.js new file mode 100644 index 0000000000000..eecab18806596 --- /dev/null +++ b/javascript/atoms/typescript/cjs-to-iife.js @@ -0,0 +1,71 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +// Converts a CommonJS-compiled TypeScript atom into a browser-injectable IIFE. +// +// TypeScript --module commonjs emits a header consisting of: +// "use strict"; +// Object.defineProperty(exports, "__esModule", { value: true }); +// and a footer of the form: +// exports.default = ; +// +// This script strips those module-system lines, then wraps the remaining body +// in an IIFE that returns the exported default function, producing the same +// browser-injectable format previously generated by --module none. + +const fs = require('node:fs') + +const [inputPath, outputPath] = process.argv.slice(2) + +if (!inputPath || !outputPath) { + throw new Error('Expected input and output file paths') +} + +const input = fs.readFileSync(inputPath, 'utf8') +const lines = input.split('\n') + +// Strip known CJS boilerplate lines. +const stripped = lines.filter(line => { + if (line.trim() === '"use strict";') return false + if (line.startsWith('Object.defineProperty(exports, "__esModule"')) return false + return true +}) + +// Find and remove the `exports.default = ;` line, capturing the name. +let exportedName = null +const body = stripped.filter(line => { + const match = line.match(/^exports\.default\s*=\s*(\w+)\s*;?\s*$/) + if (match) { + exportedName = match[1] + return false + } + return true +}) + +if (!exportedName) { + throw new Error( + `Could not find 'exports.default = ' in: ${inputPath}\n` + + `Last 3 lines:\n${lines.slice(-3).join('\n')}` + ) +} + +// Remove leading/trailing blank lines from the body so the IIFE is compact. +while (body.length && body[0].trim() === '') body.shift() +while (body.length && body[body.length - 1].trim() === '') body.pop() + +const output = `(function () {\n${body.join('\n')}\n return ${exportedName};\n})()` +fs.writeFileSync(outputPath, output) diff --git a/javascript/atoms/typescript/find-elements.ts b/javascript/atoms/typescript/find-elements.ts index 3f55ac8065b24..c54e87f3a9530 100644 --- a/javascript/atoms/typescript/find-elements.ts +++ b/javascript/atoms/typescript/find-elements.ts @@ -15,7 +15,7 @@ // specific language governing permissions and limitations // under the License. -(function findElements(target: Record, root?: Document | Element | ShadowRoot): Element[] { +function findElements(target: Record, root?: Document | Element | ShadowRoot): Element[] { type LocatorTarget = Record type Root = Document | Element | ShadowRoot type Rect = { left: number; top: number; width: number; height: number } @@ -338,4 +338,6 @@ default: throw botError(INVALID_ARGUMENT, 'Unsupported locator strategy: ' + key) } -}) +} + +export default findElements; diff --git a/javascript/atoms/typescript/get-attribute.ts b/javascript/atoms/typescript/get-attribute.ts index 9af14941c8533..c8187017815d4 100644 --- a/javascript/atoms/typescript/get-attribute.ts +++ b/javascript/atoms/typescript/get-attribute.ts @@ -15,7 +15,17 @@ // specific language governing permissions and limitations // under the License. -(function (element: Element, attribute: string): string | null { +/** + * Get the value of the given property or attribute. If the "attribute" is for + * a boolean property, we return null in the case where the value is false. If + * the attribute name is "style" an attempt to convert that style into a string + * is done. + * + * @param element The element to use. + * @param attribute The name of the attribute to look up. + * @return The string value of the attribute or property, or null. + */ +function get(element: Element, attribute: string): string | null { const PROPERTY_ALIASES: Record = { 'class': 'className', 'readonly': 'readOnly', @@ -184,4 +194,6 @@ } return property != null ? String(property) : null; -}) +} + +export default get; diff --git a/javascript/atoms/typescript/is-displayed.ts b/javascript/atoms/typescript/is-displayed.ts index 8510baf82c201..05a0d22949558 100644 --- a/javascript/atoms/typescript/is-displayed.ts +++ b/javascript/atoms/typescript/is-displayed.ts @@ -41,7 +41,7 @@ interface Coordinate { y: number; } -(function isShownElement(elem: Element, optIgnoreOpacity?: boolean): boolean { +function isShownElement(elem: Element, optIgnoreOpacity?: boolean): boolean { // Guards against form children that shadow tagName (e.g.
). var tagNameDescriptor = Object.getOwnPropertyDescriptor(Element.prototype, 'tagName'); @@ -510,4 +510,6 @@ interface Coordinate { } return isShownInternal(elem, !!optIgnoreOpacity, displayed); -}) +} + +export default isShownElement; diff --git a/javascript/atoms/typescript/tsconfig-cjs.json b/javascript/atoms/typescript/tsconfig-cjs.json new file mode 100644 index 0000000000000..78cae9f3af1af --- /dev/null +++ b/javascript/atoms/typescript/tsconfig-cjs.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "target": "ES2017", + "module": "commonjs", + "removeComments": true, + "pretty": false, + "strict": false + } +} diff --git a/javascript/atoms/typescript/wrap-as-global.js b/javascript/atoms/typescript/wrap-as-global.js index d338283512e24..901d1367d7080 100644 --- a/javascript/atoms/typescript/wrap-as-global.js +++ b/javascript/atoms/typescript/wrap-as-global.js @@ -53,14 +53,33 @@ window.bot.dom.typescript.isShown = `; const input = fs.readFileSync(inputPath, 'utf8'); -// The compiled TypeScript output begins with "(function isShownElement(". Prepend the -// browser-global assignment so the atom is accessible as -// window.bot.dom.typescript.isShown in test pages. -if (!input.trimStart().startsWith('(function ')) { - throw new Error( - `Unexpected compiled output format. Expected it to start with "(function ", got: ${input.slice(0, 80)}` - ); +// The compiled TypeScript output is CommonJS. Strip the module-system boilerplate, +// extract the exported default function name, and wrap the body in an IIFE so the +// atom is accessible as window.bot.dom.typescript.isShown in test pages. +const lines = input.split('\n'); +const stripped = lines.filter(line => { + if (line.trim() === '"use strict";') return false; + if (line.startsWith('Object.defineProperty(exports, "__esModule"')) return false; + return true; +}); + +let exportedName = null; +const body = stripped.filter(line => { + const match = line.match(/^exports\.default\s*=\s*(\w+)\s*;?\s*$/); + if (match) { + exportedName = match[1]; + return false; + } + return true; +}); + +if (!exportedName) { + throw new Error(`Could not find 'exports.default = ' in: ${inputPath}`); } -const output = licenseHeader + preamble + input.trimStart(); +while (body.length && body[0].trim() === '') body.shift(); +while (body.length && body[body.length - 1].trim() === '') body.pop(); + +const iife = `(function () {\n${body.join('\n')}\n return ${exportedName};\n})()`; +const output = licenseHeader + preamble + iife; fs.writeFileSync(outputPath, output); diff --git a/javascript/atoms/typescript/wrap-find-elements-as-global.js b/javascript/atoms/typescript/wrap-find-elements-as-global.js index b5b89ff6b597f..f0599837ead4e 100644 --- a/javascript/atoms/typescript/wrap-find-elements-as-global.js +++ b/javascript/atoms/typescript/wrap-find-elements-as-global.js @@ -52,11 +52,33 @@ window.bot.locators.typescript.findElements = ` const input = fs.readFileSync(inputPath, 'utf8') -if (!input.trimStart().startsWith('(function ')) { - throw new Error( - `Unexpected compiled output format. Expected it to start with "(function ", got: ${input.slice(0, 80)}`, - ) +// The compiled TypeScript output is CommonJS. Strip the module-system boilerplate, +// extract the exported default function name, and wrap the body in an IIFE so the +// atom is accessible as window.bot.locators.typescript.findElements in test pages. +const lines = input.split('\n') +const stripped = lines.filter(line => { + if (line.trim() === '"use strict";') return false + if (line.startsWith('Object.defineProperty(exports, "__esModule"')) return false + return true +}) + +let exportedName = null +const body = stripped.filter(line => { + const match = line.match(/^exports\.default\s*=\s*(\w+)\s*;?\s*$/) + if (match) { + exportedName = match[1] + return false + } + return true +}) + +if (!exportedName) { + throw new Error(`Could not find 'exports.default = ' in: ${inputPath}`) } -const output = licenseHeader + preamble + input.trimStart() +while (body.length && body[0].trim() === '') body.shift() +while (body.length && body[body.length - 1].trim() === '') body.pop() + +const iife = `(function () {\n${body.join('\n')}\n return ${exportedName};\n})()` +const output = licenseHeader + preamble + iife fs.writeFileSync(outputPath, output) diff --git a/javascript/atoms/typescript/wrap-get-attribute-as-global.js b/javascript/atoms/typescript/wrap-get-attribute-as-global.js index f988a7b0ce0c7..18015eb228ceb 100644 --- a/javascript/atoms/typescript/wrap-get-attribute-as-global.js +++ b/javascript/atoms/typescript/wrap-get-attribute-as-global.js @@ -52,14 +52,33 @@ window.bot.dom.typescript.getAttribute = `; const input = fs.readFileSync(inputPath, 'utf8'); -// The compiled TypeScript output begins with "(function (". Prepend the -// browser-global assignment so the atom is accessible as -// window.bot.dom.typescript.getAttribute in test pages. -if (!input.trimStart().startsWith('(function (')) { - throw new Error( - `Unexpected compiled output format. Expected it to start with "(function (", got: ${input.slice(0, 80)}` - ); +// The compiled TypeScript output is CommonJS. Strip the module-system boilerplate, +// extract the exported default function name, and wrap the body in an IIFE so the +// atom is accessible as window.bot.dom.typescript.getAttribute in test pages. +const lines = input.split('\n'); +const stripped = lines.filter(line => { + if (line.trim() === '"use strict";') return false; + if (line.startsWith('Object.defineProperty(exports, "__esModule"')) return false; + return true; +}); + +let exportedName = null; +const body = stripped.filter(line => { + const match = line.match(/^exports\.default\s*=\s*(\w+)\s*;?\s*$/); + if (match) { + exportedName = match[1]; + return false; + } + return true; +}); + +if (!exportedName) { + throw new Error(`Could not find 'exports.default = ' in: ${inputPath}`); } -const output = licenseHeader + preamble + input.trimStart(); +while (body.length && body[0].trim() === '') body.shift(); +while (body.length && body[body.length - 1].trim() === '') body.pop(); + +const iife = `(function () {\n${body.join('\n')}\n return ${exportedName};\n})()`; +const output = licenseHeader + preamble + iife; fs.writeFileSync(outputPath, output); From 8146e8dae1092f79ca28bd62fd5dd29355d7498d Mon Sep 17 00:00:00 2001 From: AutomatedTester Date: Fri, 15 May 2026 09:29:59 +0100 Subject: [PATCH 2/3] [javascript] Fix Windows EPERM crash from shared tsconfig across ts_project targets When multiple ts_project targets share a single tsconfig with no files/include restriction, tsc compiles all .ts files for each target. On Windows, Bazel marks outputs read-only after the first action completes; parallel actions then fail with TS5033 EPERM trying to overwrite the same files. Give each per-atom ts_project its own tsconfig that lists only the one .ts file it is responsible for, eliminating the cross-target output collision. --- javascript/atoms/BUILD.bazel | 6 +++--- .../atoms/typescript/tsconfig-find-elements.json | 10 ++++++++++ .../atoms/typescript/tsconfig-get-attribute.json | 10 ++++++++++ .../{tsconfig-cjs.json => tsconfig-is-displayed.json} | 3 ++- 4 files changed, 25 insertions(+), 4 deletions(-) create mode 100644 javascript/atoms/typescript/tsconfig-find-elements.json create mode 100644 javascript/atoms/typescript/tsconfig-get-attribute.json rename javascript/atoms/typescript/{tsconfig-cjs.json => tsconfig-is-displayed.json} (80%) diff --git a/javascript/atoms/BUILD.bazel b/javascript/atoms/BUILD.bazel index 094e573bb8263..eb64c0b1c3080 100644 --- a/javascript/atoms/BUILD.bazel +++ b/javascript/atoms/BUILD.bazel @@ -37,7 +37,7 @@ ts_project( srcs = ["typescript/get-attribute.ts"], declaration = False, source_map = False, - tsconfig = "typescript/tsconfig-cjs.json", + tsconfig = "typescript/tsconfig-get-attribute.json", ) js_run_binary( @@ -69,7 +69,7 @@ ts_project( srcs = ["typescript/find-elements.ts"], declaration = False, source_map = False, - tsconfig = "typescript/tsconfig-cjs.json", + tsconfig = "typescript/tsconfig-find-elements.json", ) js_run_binary( @@ -101,7 +101,7 @@ ts_project( srcs = ["typescript/is-displayed.ts"], declaration = False, source_map = False, - tsconfig = "typescript/tsconfig-cjs.json", + tsconfig = "typescript/tsconfig-is-displayed.json", ) js_run_binary( diff --git a/javascript/atoms/typescript/tsconfig-find-elements.json b/javascript/atoms/typescript/tsconfig-find-elements.json new file mode 100644 index 0000000000000..6e1c5fc9fe8aa --- /dev/null +++ b/javascript/atoms/typescript/tsconfig-find-elements.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "ES2017", + "module": "commonjs", + "removeComments": true, + "pretty": false, + "strict": false + }, + "files": ["find-elements.ts"] +} diff --git a/javascript/atoms/typescript/tsconfig-get-attribute.json b/javascript/atoms/typescript/tsconfig-get-attribute.json new file mode 100644 index 0000000000000..34a85b2f937c3 --- /dev/null +++ b/javascript/atoms/typescript/tsconfig-get-attribute.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "ES2017", + "module": "commonjs", + "removeComments": true, + "pretty": false, + "strict": false + }, + "files": ["get-attribute.ts"] +} diff --git a/javascript/atoms/typescript/tsconfig-cjs.json b/javascript/atoms/typescript/tsconfig-is-displayed.json similarity index 80% rename from javascript/atoms/typescript/tsconfig-cjs.json rename to javascript/atoms/typescript/tsconfig-is-displayed.json index 78cae9f3af1af..a1d1e6b8c6143 100644 --- a/javascript/atoms/typescript/tsconfig-cjs.json +++ b/javascript/atoms/typescript/tsconfig-is-displayed.json @@ -5,5 +5,6 @@ "removeComments": true, "pretty": false, "strict": false - } + }, + "files": ["is-displayed.ts"] } From d32a44c90233a62da50b7726344d47fa5e9c0736 Mon Sep 17 00:00:00 2001 From: AutomatedTester Date: Tue, 6 Oct 2026 19:13:21 +0100 Subject: [PATCH 3/3] [javascript] Make @seleniumhq/atoms installable and resolvable The npm artifact was declared but not consumable. Addresses the integration review on #17459: - Drop the `require` conditions and `main` pointing at `dist/*.cjs`; no CJS compilation exists on the branch, so every `require` entry resolved to a file that is never built. Ship ESM-only and declare `engines.node >= 22.12.0`, where Node's `require(esm)` covers CommonJS consumers. - Add `index.ts` extension specifiers. Extensionless specifiers are emitted verbatim under `module: esnext`, so `import('@seleniumhq/atoms')` failed with ERR_MODULE_NOT_FOUND for every consumer. - Ship LICENSE/NOTICE, as Apache-2.0 4(a) requires. - Put `types` first in each `exports` entry; condition order is significant and resolvers take the first match. - Publish the `.ts` sources so the emitted `.d.ts.map` / `.js.map` resolve instead of pointing outside the tarball. - Drive the version from `node:version` rather than a hardcoded value six minors behind trunk. - Rename the four CommonJS build tools to `.cjs` so they no longer depend on `"type": "module"` not reaching them via runfiles staging. - Add a consumer-facing `README.npm.md`, renamed to README.md on packaging so the contributor README stays in place. - Drop the `google-closure-deps` devDependency left over from when this manifest was the private selenium-atoms build-tools package. Verified by packing the built directory and installing it: both `require` and `import` resolve, all four export paths resolve under node10/node16/bundler per @arethetypeswrong/cli, and the IIFE path for the language bindings is unchanged. --- javascript/atoms/BUILD.bazel | 44 ++++++++++--- javascript/atoms/README.md | 6 ++ javascript/atoms/README.npm.md | 65 +++++++++++++++++++ javascript/atoms/index.ts | 9 ++- javascript/atoms/package.json | 46 +++++++------ .../{cjs-to-iife.js => cjs-to-iife.cjs} | 0 .../{wrap-as-global.js => wrap-as-global.cjs} | 0 ...al.js => wrap-find-elements-as-global.cjs} | 0 ...al.js => wrap-get-attribute-as-global.cjs} | 0 rake_tasks/node.rake | 7 +- 10 files changed, 146 insertions(+), 31 deletions(-) create mode 100644 javascript/atoms/README.npm.md rename javascript/atoms/typescript/{cjs-to-iife.js => cjs-to-iife.cjs} (100%) rename javascript/atoms/typescript/{wrap-as-global.js => wrap-as-global.cjs} (100%) rename javascript/atoms/typescript/{wrap-find-elements-as-global.js => wrap-find-elements-as-global.cjs} (100%) rename javascript/atoms/typescript/{wrap-get-attribute-as-global.js => wrap-get-attribute-as-global.cjs} (100%) diff --git a/javascript/atoms/BUILD.bazel b/javascript/atoms/BUILD.bazel index eb64c0b1c3080..194135e7ab114 100644 --- a/javascript/atoms/BUILD.bazel +++ b/javascript/atoms/BUILD.bazel @@ -6,28 +6,33 @@ load("//javascript:defs.bzl", "closure_js_deps", "closure_test_suite") package(default_visibility = ["//visibility:public"]) +VERSION = "4.51.0-nightly202609302012" + +# These build tools are CommonJS; the .cjs extension keeps that true regardless +# of the "type": "module" declared by the @seleniumhq/atoms package.json above +# them in this directory. js_binary( name = "cjs_to_iife", - data = ["typescript/cjs-to-iife.js"], - entry_point = "typescript/cjs-to-iife.js", + data = ["typescript/cjs-to-iife.cjs"], + entry_point = "typescript/cjs-to-iife.cjs", ) js_binary( name = "wrap_get_attribute_as_global", - data = ["typescript/wrap-get-attribute-as-global.js"], - entry_point = "typescript/wrap-get-attribute-as-global.js", + data = ["typescript/wrap-get-attribute-as-global.cjs"], + entry_point = "typescript/wrap-get-attribute-as-global.cjs", ) js_binary( name = "wrap_as_global", - data = ["typescript/wrap-as-global.js"], - entry_point = "typescript/wrap-as-global.js", + data = ["typescript/wrap-as-global.cjs"], + entry_point = "typescript/wrap-as-global.cjs", ) js_binary( name = "wrap_find_elements_as_global", - data = ["typescript/wrap-find-elements-as-global.js"], - entry_point = "typescript/wrap-find-elements-as-global.js", + data = ["typescript/wrap-find-elements-as-global.cjs"], + entry_point = "typescript/wrap-find-elements-as-global.cjs", ) # ── get-attribute ──────────────────────────────────────────────────────────── @@ -143,21 +148,44 @@ ts_project( tsconfig = "tsconfig.json", ) +# npm_package does not pick up filegroup sources. +genrule( + name = "license", + srcs = ["//:license"], + outs = [ + "LICENSE", + "NOTICE", + ], + cmd = "cp $(locations //:license) $(@D)", +) + npm_package( name = "selenium-atoms-package", srcs = [ + # README.md in this directory is the contributor guide; the npm page + # gets its own file, renamed into place below. + "README.npm.md", + # Published so the emitted .d.ts.map / .js.map resolve for consumers. + "index.ts", "package.json", + "typescript/find-elements.ts", + "typescript/get-attribute.ts", + "typescript/is-displayed.ts", ":atoms-npm-compiled", + ":license", ], package = "@seleniumhq/atoms", publishable = True, + replace_prefixes = {"README.npm.md": "README.md"}, tags = ["release-artifact"], + version = VERSION, visibility = ["//visibility:public"], ) filegroup( name = "atoms", srcs = glob([ + "**/*.cjs", "**/*.html", "**/*.jpg", "**/*.js", diff --git a/javascript/atoms/README.md b/javascript/atoms/README.md index 9c646a2a5ad08..8a2121b699114 100644 --- a/javascript/atoms/README.md +++ b/javascript/atoms/README.md @@ -5,6 +5,12 @@ These "atoms" provide reusable building blocks for browser automation Google Closure Compiler, but at some point, we'd love to migrate them to TypeScript since Closure isn't as widely known. +The atoms that have already been migrated to TypeScript are additionally +published to npm as `@seleniumhq/atoms`, for tooling outside the Selenium +bindings that needs to match Selenium's behaviour. `README.npm.md` is the +consumer-facing documentation for that package; this file is for contributors +working on the atoms themselves. + ## Testing the Atoms ### Iteratively diff --git a/javascript/atoms/README.npm.md b/javascript/atoms/README.npm.md new file mode 100644 index 0000000000000..ffe2c9812ca92 --- /dev/null +++ b/javascript/atoms/README.npm.md @@ -0,0 +1,65 @@ +# @seleniumhq/atoms + +Browser injection atoms for Selenium WebDriver, published as TypeScript-typed +ES modules. + +Atoms are small, self-contained functions that run inside the page under +automation. They encode the browser-compatibility behaviour that the W3C +WebDriver specification leaves to implementations — what "displayed" means, how +`getAttribute` reconciles attributes with IDL properties, and how a locator +resolves to elements. Selenium's own language bindings inject the same logic, +compiled to an IIFE; this package exposes it to other JavaScript tooling that +needs to match Selenium's behaviour exactly. + +## Installation + +```sh +npm install @seleniumhq/atoms +``` + +This is an ES-module-only package and requires Node.js 18 or newer. + +## Usage + +```js +import { getAttribute, findElements, isDisplayed } from '@seleniumhq/atoms' +``` + +Each atom is also available as its own subpath export, so a bundler can tree +shake down to just the one you need: + +```js +import getAttribute from '@seleniumhq/atoms/get-attribute' +import findElements from '@seleniumhq/atoms/find-elements' +import isDisplayed from '@seleniumhq/atoms/is-displayed' +``` + +### API + +```ts +getAttribute(element: Element, attribute: string): string | null +findElements(target: Record, root?: Document | Element | ShadowRoot): Element[] +isDisplayed(element: Element, ignoreOpacity?: boolean): boolean +``` + +`findElements` takes a locator object keyed by strategy, matching the WebDriver +wire format: + +```js +findElements({ css: '.result' }) +findElements({ 'link text': 'Continue' }, shadowRoot) +``` + +The atoms operate on the DOM of the document they are evaluated in, so they must +run in the page rather than in the driver process — for example via +`Runtime.evaluate` over CDP, `script.callFunction` over WebDriver BiDi, or +Selenium's own `executeScript`. + +## Versioning + +This package is released from the Selenium monorepo and shares its version with +the `selenium-webdriver` npm package. + +## License + +Apache-2.0. See `LICENSE` and `NOTICE`. diff --git a/javascript/atoms/index.ts b/javascript/atoms/index.ts index c0c2f011726c8..1d65b61624191 100644 --- a/javascript/atoms/index.ts +++ b/javascript/atoms/index.ts @@ -15,6 +15,9 @@ // specific language governing permissions and limitations // under the License. -export { default as getAttribute } from './typescript/get-attribute' -export { default as findElements } from './typescript/find-elements' -export { default as isDisplayed } from './typescript/is-displayed' +// Specifiers carry the emitted .js extension because Node's ESM resolver does +// not probe for extensions. "module": "esnext" rewrites nothing, so what is +// written here is what consumers resolve against. +export { default as getAttribute } from './typescript/get-attribute.js' +export { default as findElements } from './typescript/find-elements.js' +export { default as isDisplayed } from './typescript/is-displayed.js' diff --git a/javascript/atoms/package.json b/javascript/atoms/package.json index 908271bb5d31c..2794814b10972 100644 --- a/javascript/atoms/package.json +++ b/javascript/atoms/package.json @@ -1,43 +1,51 @@ { "name": "@seleniumhq/atoms", - "version": "4.45.0-nightly202605121853", + "version": "4.51.0-nightly202609302012", "description": "Browser injection atoms for Selenium WebDriver", "type": "module", - "main": "./dist/index.cjs", + "main": "./dist/index.js", "module": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { ".": { - "import": "./dist/index.js", - "require": "./dist/index.cjs", - "types": "./dist/index.d.ts" + "types": "./dist/index.d.ts", + "default": "./dist/index.js" }, "./get-attribute": { - "import": "./dist/typescript/get-attribute.js", - "require": "./dist/typescript/get-attribute.cjs", - "types": "./dist/typescript/get-attribute.d.ts" + "types": "./dist/typescript/get-attribute.d.ts", + "default": "./dist/typescript/get-attribute.js" }, "./find-elements": { - "import": "./dist/typescript/find-elements.js", - "require": "./dist/typescript/find-elements.cjs", - "types": "./dist/typescript/find-elements.d.ts" + "types": "./dist/typescript/find-elements.d.ts", + "default": "./dist/typescript/find-elements.js" }, "./is-displayed": { - "import": "./dist/typescript/is-displayed.js", - "require": "./dist/typescript/is-displayed.cjs", - "types": "./dist/typescript/is-displayed.d.ts" + "types": "./dist/typescript/is-displayed.d.ts", + "default": "./dist/typescript/is-displayed.js" } }, + "typesVersions": { + "*": { + "get-attribute": ["./dist/typescript/get-attribute.d.ts"], + "find-elements": ["./dist/typescript/find-elements.d.ts"], + "is-displayed": ["./dist/typescript/is-displayed.d.ts"] + } + }, + "engines": { + "node": ">= 22.12.0" + }, "files": [ - "dist/" + "dist/", + "index.ts", + "typescript/", + "LICENSE", + "NOTICE", + "README.md" ], "repository": { "type": "git", "url": "https://github.com/SeleniumHQ/selenium.git", "directory": "javascript/atoms" }, - "license": "Apache-2.0", - "devDependencies": { - "google-closure-deps": "^20230802.0.0" - } + "license": "Apache-2.0" } diff --git a/javascript/atoms/typescript/cjs-to-iife.js b/javascript/atoms/typescript/cjs-to-iife.cjs similarity index 100% rename from javascript/atoms/typescript/cjs-to-iife.js rename to javascript/atoms/typescript/cjs-to-iife.cjs diff --git a/javascript/atoms/typescript/wrap-as-global.js b/javascript/atoms/typescript/wrap-as-global.cjs similarity index 100% rename from javascript/atoms/typescript/wrap-as-global.js rename to javascript/atoms/typescript/wrap-as-global.cjs diff --git a/javascript/atoms/typescript/wrap-find-elements-as-global.js b/javascript/atoms/typescript/wrap-find-elements-as-global.cjs similarity index 100% rename from javascript/atoms/typescript/wrap-find-elements-as-global.js rename to javascript/atoms/typescript/wrap-find-elements-as-global.cjs diff --git a/javascript/atoms/typescript/wrap-get-attribute-as-global.js b/javascript/atoms/typescript/wrap-get-attribute-as-global.cjs similarity index 100% rename from javascript/atoms/typescript/wrap-get-attribute-as-global.js rename to javascript/atoms/typescript/wrap-get-attribute-as-global.cjs diff --git a/rake_tasks/node.rake b/rake_tasks/node.rake index 975e2aff07c8e..57327b1beb806 100644 --- a/rake_tasks/node.rake +++ b/rake_tasks/node.rake @@ -160,7 +160,12 @@ task :version, [:version] do |_task, arguments| new_version = SeleniumRake.updated_version(old_version, arguments[:version], nightly) puts "Updating Node from #{old_version} to #{new_version}" - %w[javascript/selenium-webdriver/package.json javascript/selenium-webdriver/BUILD.bazel].each do |file| + %w[ + javascript/selenium-webdriver/package.json + javascript/selenium-webdriver/BUILD.bazel + javascript/atoms/package.json + javascript/atoms/BUILD.bazel + ].each do |file| text = File.read(file).gsub(old_version, new_version) File.open(file, 'w') { |f| f.puts text } end