selenium-webdriver 4.19.0 → 4.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/CHANGES.md +21 -0
  2. package/README.md +1 -1
  3. package/bidi/addInterceptParameters.js +28 -2
  4. package/bidi/browser.js +17 -0
  5. package/bidi/browsingContext.js +206 -39
  6. package/bidi/browsingContextInspector.js +51 -0
  7. package/bidi/browsingContextTypes.js +31 -0
  8. package/bidi/captureScreenshotParameters.js +96 -0
  9. package/bidi/clipRectangle.js +127 -0
  10. package/bidi/continueRequestParameters.js +39 -0
  11. package/bidi/continueResponseParameters.js +40 -0
  12. package/bidi/cookieFilter.js +60 -0
  13. package/bidi/createContextParameters.js +73 -0
  14. package/bidi/evaluateResult.js +17 -0
  15. package/bidi/index.js +0 -1
  16. package/bidi/input.js +27 -3
  17. package/bidi/interceptPhase.js +4 -0
  18. package/bidi/logEntries.js +66 -0
  19. package/bidi/network.js +102 -0
  20. package/bidi/networkTypes.js +346 -1
  21. package/bidi/partialCookie.js +48 -0
  22. package/bidi/partitionDescriptor.js +33 -0
  23. package/bidi/partitionKey.js +17 -0
  24. package/bidi/protocolType.js +20 -0
  25. package/bidi/protocolValue.js +129 -2
  26. package/bidi/provideResponseParameters.js +40 -0
  27. package/bidi/realmInfo.js +27 -0
  28. package/bidi/resultOwnership.js +4 -0
  29. package/bidi/scriptManager.js +119 -1
  30. package/bidi/scriptTypes.js +36 -0
  31. package/bidi/storage.js +30 -0
  32. package/bidi/urlPattern.js +35 -0
  33. package/bin/linux/selenium-manager +0 -0
  34. package/bin/macos/selenium-manager +0 -0
  35. package/bin/windows/selenium-manager.exe +0 -0
  36. package/chromium.js +12 -9
  37. package/common/driverFinder.js +36 -4
  38. package/common/seleniumManager.js +7 -39
  39. package/eslint.config.js +107 -0
  40. package/firefox.js +20 -6
  41. package/http/index.js +6 -6
  42. package/ie.js +8 -4
  43. package/index.js +11 -0
  44. package/io/exec.js +1 -1
  45. package/io/index.js +3 -2
  46. package/io/zip.js +1 -1
  47. package/lib/atoms/find-elements.js +2 -2
  48. package/lib/http.js +5 -4
  49. package/lib/input.js +0 -1
  50. package/lib/pinnedScript.js +1 -1
  51. package/lib/select.js +102 -60
  52. package/lib/until.js +3 -3
  53. package/lib/util.js +1 -0
  54. package/lib/webdriver.js +2 -2
  55. package/net/index.js +1 -1
  56. package/net/portprober.js +1 -1
  57. package/package.json +13 -7
  58. package/remote/index.js +1 -1
  59. package/remote/util.js +2 -2
  60. package/safari.js +3 -3
  61. package/testing/index.js +12 -8
@@ -0,0 +1,107 @@
1
+ // Licensed to the Software Freedom Conservancy (SFC) under one
2
+ // or more contributor license agreements. See the NOTICE file
3
+ // distributed with this work for additional information
4
+ // regarding copyright ownership. The SFC licenses this file
5
+ // to you under the Apache License, Version 2.0 (the
6
+ // "License"); you may not use this file except in compliance
7
+ // with the License. You may obtain a copy of the License at
8
+ //
9
+ // http://www.apache.org/licenses/LICENSE-2.0
10
+ //
11
+ // Unless required by applicable law or agreed to in writing,
12
+ // software distributed under the License is distributed on an
13
+ // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
+ // KIND, either express or implied. See the License for the
15
+ // specific language governing permissions and limitations
16
+ // under the License.
17
+
18
+ const globals = require('globals')
19
+ const noOnlyTests = require('eslint-plugin-no-only-tests')
20
+ const js = require('@eslint/js')
21
+ const eslintPluginPrettierRecommended = require('eslint-plugin-prettier/recommended')
22
+ const mochaPlugin = require('eslint-plugin-mocha')
23
+ const nodePlugin = require('eslint-plugin-n')
24
+
25
+ module.exports = [
26
+ js.configs.recommended,
27
+ eslintPluginPrettierRecommended,
28
+ mochaPlugin.configs.flat.recommended,
29
+ nodePlugin.configs['flat/recommended-script'],
30
+ {
31
+ languageOptions: {
32
+ globals: {
33
+ mocha: true,
34
+ es6: true,
35
+ ...globals.node,
36
+ },
37
+ parserOptions: {
38
+ ecmaVersion: 2022,
39
+ },
40
+ },
41
+ files: ['**/*.js', 'lib/http.js'],
42
+ ignores: ['node_modules/*', 'generator/*', 'devtools/generator/'],
43
+ plugins: {
44
+ 'no-only-tests': noOnlyTests,
45
+ },
46
+ rules: {
47
+ 'no-const-assign': 'error',
48
+ 'no-this-before-super': 'error',
49
+ 'no-undef': 'error',
50
+ 'no-unreachable': 'error',
51
+ 'no-unused-vars': [
52
+ 'error',
53
+ {
54
+ varsIgnorePattern: '^_',
55
+ args: 'all',
56
+ argsIgnorePattern: '^_',
57
+ },
58
+ ],
59
+ 'constructor-super': 'error',
60
+ 'valid-typeof': 'error',
61
+ 'no-only-tests/no-only-tests': 'error',
62
+ 'n/no-deprecated-api': ['error'],
63
+ 'n/no-missing-import': ['error'],
64
+ 'n/no-missing-require': ['error'],
65
+ 'n/no-mixed-requires': ['error'],
66
+ 'n/no-new-require': ['error'],
67
+ 'n/no-unpublished-import': ['error'],
68
+ 'n/no-unpublished-require': [
69
+ 'error',
70
+ {
71
+ allowModules: [
72
+ 'globals',
73
+ '@eslint/js',
74
+ 'eslint-plugin-mocha',
75
+ 'eslint-plugin-prettier',
76
+ 'eslint-plugin-n',
77
+ 'eslint-plugin-no-only-tests',
78
+ ],
79
+ tryExtensions: ['.js'],
80
+ },
81
+ ],
82
+ 'n/prefer-node-protocol': ['error'],
83
+ 'mocha/no-skipped-tests': ['off'],
84
+ 'mocha/no-mocha-arrows': ['off'],
85
+ 'mocha/no-setup-in-describe': ['off'],
86
+ 'mocha/no-top-level-hooks': ['off'],
87
+ 'mocha/no-sibling-hooks': ['off'],
88
+ 'mocha/no-exports': ['off'],
89
+ 'mocha/no-empty-description': ['off'],
90
+ 'mocha/max-top-level-suites': ['off'],
91
+ 'mocha/consistent-spacing-between-blocks': ['off'],
92
+ 'mocha/no-nested-tests': ['off'],
93
+ 'mocha/no-pending-tests': ['off'],
94
+ 'mocha/no-identical-title': ['off'],
95
+ 'prettier/prettier': [
96
+ 'error',
97
+ {
98
+ endOfLine: 'lf',
99
+ printWidth: 120,
100
+ semi: false,
101
+ singleQuote: true,
102
+ trailingComma: 'all',
103
+ },
104
+ ],
105
+ },
106
+ },
107
+ ]
package/firefox.js CHANGED
@@ -107,8 +107,8 @@
107
107
 
108
108
  'use strict'
109
109
 
110
- const fs = require('fs')
111
- const path = require('path')
110
+ const fs = require('node:fs')
111
+ const path = require('node:path')
112
112
  const Symbols = require('./lib/symbols')
113
113
  const command = require('./lib/command')
114
114
  const http = require('./http')
@@ -116,9 +116,9 @@ const io = require('./io')
116
116
  const remote = require('./remote')
117
117
  const webdriver = require('./lib/webdriver')
118
118
  const zip = require('./io/zip')
119
- const { Browser, Capabilities } = require('./lib/capabilities')
119
+ const { Browser, Capabilities, Capability } = require('./lib/capabilities')
120
120
  const { Zip } = require('./io/zip')
121
- const { getPath } = require('./common/driverFinder')
121
+ const { getBinaryPaths } = require('./common/driverFinder')
122
122
  const FIREFOX_CAPABILITY_KEY = 'moz:firefoxOptions'
123
123
 
124
124
  /**
@@ -444,6 +444,7 @@ const ExtensionCommand = {
444
444
  SET_CONTEXT: 'setContext',
445
445
  INSTALL_ADDON: 'install addon',
446
446
  UNINSTALL_ADDON: 'uninstall addon',
447
+ FULL_PAGE_SCREENSHOT: 'fullPage screenshot',
447
448
  }
448
449
 
449
450
  /**
@@ -470,6 +471,8 @@ function configureExecutor(executor) {
470
471
  executor.defineCommand(ExtensionCommand.INSTALL_ADDON, 'POST', '/session/:sessionId/moz/addon/install')
471
472
 
472
473
  executor.defineCommand(ExtensionCommand.UNINSTALL_ADDON, 'POST', '/session/:sessionId/moz/addon/uninstall')
474
+
475
+ executor.defineCommand(ExtensionCommand.FULL_PAGE_SCREENSHOT, 'GET', '/session/:sessionId/moz/screenshot/full')
473
476
  }
474
477
 
475
478
  /**
@@ -538,7 +541,7 @@ class Driver extends webdriver.WebDriver {
538
541
  configureExecutor(executor)
539
542
  } else if (opt_executor instanceof remote.DriverService) {
540
543
  if (!opt_executor.getExecutable()) {
541
- const { driverPath, browserPath } = getPath(caps)
544
+ const { driverPath, browserPath } = getBinaryPaths(caps)
542
545
  opt_executor.setExecutable(driverPath)
543
546
  firefoxBrowserPath = browserPath
544
547
  }
@@ -547,7 +550,7 @@ class Driver extends webdriver.WebDriver {
547
550
  } else {
548
551
  let service = new ServiceBuilder().build()
549
552
  if (!service.getExecutable()) {
550
- const { driverPath, browserPath } = getPath(caps)
553
+ const { driverPath, browserPath } = getBinaryPaths(caps)
551
554
  service.setExecutable(driverPath)
552
555
  firefoxBrowserPath = browserPath
553
556
  }
@@ -563,6 +566,7 @@ class Driver extends webdriver.WebDriver {
563
566
  } else {
564
567
  caps.set(FIREFOX_CAPABILITY_KEY, { binary: firefoxBrowserPath })
565
568
  }
569
+ caps.delete(Capability.BROWSER_VERSION)
566
570
  }
567
571
 
568
572
  return /** @type {!Driver} */ (super.createSession(executor, caps, onQuit))
@@ -645,6 +649,16 @@ class Driver extends webdriver.WebDriver {
645
649
  id = await Promise.resolve(id)
646
650
  return this.execute(new command.Command(ExtensionCommand.UNINSTALL_ADDON).setParameter('id', id))
647
651
  }
652
+
653
+ /**
654
+ * Take full page screenshot of the visible region
655
+ *
656
+ * @return {!Promise<string>} A promise that will be
657
+ * resolved to the screenshot as a base-64 encoded PNG.
658
+ */
659
+ takeFullPageScreenshot() {
660
+ return this.execute(new command.Command(ExtensionCommand.FULL_PAGE_SCREENSHOT))
661
+ }
648
662
  }
649
663
 
650
664
  /**
package/http/index.js CHANGED
@@ -22,9 +22,9 @@
22
22
 
23
23
  'use strict'
24
24
 
25
- const http = require('http')
26
- const https = require('https')
27
- const url = require('url')
25
+ const http = require('node:http')
26
+ const https = require('node:https')
27
+ const url = require('node:url')
28
28
 
29
29
  const httpLib = require('../lib/http')
30
30
 
@@ -45,7 +45,7 @@ let RequestOptions // eslint-disable-line
45
45
  * @throws {Error} if the URL does not include a hostname.
46
46
  */
47
47
  function getRequestOptions(aUrl) {
48
- //eslint-disable-next-line node/no-deprecated-api
48
+ // eslint-disable-next-line n/no-deprecated-api
49
49
  let options = url.parse(aUrl)
50
50
  if (!options.hostname) {
51
51
  throw new Error('Invalid URL: ' + aUrl)
@@ -140,7 +140,7 @@ class HttpClient {
140
140
  } else {
141
141
  path += httpRequest.path
142
142
  }
143
- //eslint-disable-next-line node/no-deprecated-api
143
+ // eslint-disable-next-line n/no-deprecated-api
144
144
  let parsedPath = url.parse(path)
145
145
 
146
146
  let options = {
@@ -218,7 +218,7 @@ function sendRequest(options, onOk, onError, opt_data, opt_proxy, opt_retries) {
218
218
  if (response.statusCode == 302 || response.statusCode == 303) {
219
219
  let location
220
220
  try {
221
- // eslint-disable-next-line node/no-deprecated-api
221
+ // eslint-disable-next-line n/no-deprecated-api
222
222
  location = url.parse(response.headers['location'])
223
223
  } catch (ex) {
224
224
  onError(
package/ie.js CHANGED
@@ -33,7 +33,7 @@ const remote = require('./remote')
33
33
  const webdriver = require('./lib/webdriver')
34
34
  const { Browser, Capabilities } = require('./lib/capabilities')
35
35
  const error = require('./lib/error')
36
- const { getPath } = require('./common/driverFinder')
36
+ const { getBinaryPaths } = require('./common/driverFinder')
37
37
 
38
38
  const OPTIONS_CAPABILITY_KEY = 'se:ieOptions'
39
39
  const SCROLL_BEHAVIOUR = {
@@ -223,7 +223,9 @@ class Options extends Capabilities {
223
223
 
224
224
  addBrowserCommandSwitches(...args) {
225
225
  let current = this.options_[Key.BROWSER_COMMAND_LINE_SWITCHES] || []
226
- if (typeof current == 'string') current = current.split(' ')
226
+ if (typeof current == 'string') {
227
+ current = current.split(' ')
228
+ }
227
229
  this.options_[Key.BROWSER_COMMAND_LINE_SWITCHES] = current.concat(args).join(' ')
228
230
  return this
229
231
  }
@@ -239,7 +241,9 @@ class Options extends Capabilities {
239
241
 
240
242
  addArguments(...args) {
241
243
  let current = this.options_[Key.BROWSER_COMMAND_LINE_SWITCHES] || []
242
- if (typeof current == 'string') current = current.split(' ')
244
+ if (typeof current == 'string') {
245
+ current = current.split(' ')
246
+ }
243
247
  this.options_[Key.BROWSER_COMMAND_LINE_SWITCHES] = current.concat(args).join(' ')
244
248
  return this
245
249
  }
@@ -447,7 +451,7 @@ class Driver extends webdriver.WebDriver {
447
451
  service = createServiceFromCapabilities(options)
448
452
  }
449
453
  if (!service.getExecutable()) {
450
- service.setExecutable(getPath(options).driverPath)
454
+ service.setExecutable(getBinaryPaths(options).driverPath)
451
455
  }
452
456
 
453
457
  let client = service.start().then((url) => new http.HttpClient(url))
package/index.js CHANGED
@@ -596,6 +596,17 @@ class Builder {
596
596
 
597
597
  browser = capabilities.get(Capability.BROWSER_NAME)
598
598
 
599
+ /**
600
+ * If browser is not defined in forBrowser, check if browserOptions are defined to pick the browserName
601
+ */
602
+ if (!browser) {
603
+ const options =
604
+ this.chromeOptions_ || this.firefoxOptions_ || this.ieOptions_ || this.safariOptions_ || this.edgeOptions_
605
+ if (options) {
606
+ browser = options['map_'].get(Capability.BROWSER_NAME)
607
+ }
608
+ }
609
+
599
610
  if (typeof browser !== 'string') {
600
611
  throw TypeError(
601
612
  `Target browser must be a string, but is <${typeof browser}>;` + ' did you forget to call forBrowser()?',
package/io/exec.js CHANGED
@@ -17,7 +17,7 @@
17
17
 
18
18
  'use strict'
19
19
 
20
- const childProcess = require('child_process')
20
+ const childProcess = require('node:child_process')
21
21
 
22
22
  /**
23
23
  * Options for configuring an executed command.
package/io/index.js CHANGED
@@ -17,8 +17,8 @@
17
17
 
18
18
  'use strict'
19
19
 
20
- const fs = require('fs')
21
- const path = require('path')
20
+ const fs = require('node:fs')
21
+ const path = require('node:path')
22
22
  const tmp = require('tmp')
23
23
 
24
24
  /**
@@ -219,6 +219,7 @@ function findInPath(file, opt_checkCwd) {
219
219
  try {
220
220
  let stats = fs.statSync(tmp)
221
221
  return stats.isFile() && !stats.isDirectory()
222
+ /*eslint no-unused-vars: "off"*/
222
223
  } catch (ex) {
223
224
  return false
224
225
  }
package/io/zip.js CHANGED
@@ -18,7 +18,7 @@
18
18
  'use strict'
19
19
 
20
20
  const jszip = require('jszip')
21
- const path = require('path')
21
+ const path = require('node:path')
22
22
 
23
23
  const io = require('./index')
24
24
  const { InvalidArgumentError } = require('../lib/error')
@@ -106,8 +106,8 @@ function gd(a){var b=a.shape.toLowerCase();a=a.coords.split(",");if("rect"==b&&4
106
106
  function hd(a){return a.replace(/^[^\S\xa0]+|[^\S\xa0]+$/g,"")}function id(a){var b=[];Yc?jd(a,b):kd(a,b);a=qa(b,hd);return hd(a.join("\n")).replace(/\xa0/g," ")}
107
107
  function ld(a,b,c){if(S(a,"BR"))b.push("");else{var d=S(a,"TD"),e=U(a,"display"),f=!d&&!(0<=oa(md,e)),g=void 0!==a.previousElementSibling?a.previousElementSibling:gb(a.previousSibling);g=g?U(g,"display"):"";var h=U(a,"float")||U(a,"cssFloat")||U(a,"styleFloat");!f||"run-in"==g&&"none"==h||/^[\s\xa0]*$/.test(b[b.length-1]||"")||b.push("");var n=ed(a),u=null,p=null;n&&(u=U(a,"white-space"),p=U(a,"text-transform"));l(a.childNodes,function(G){c(G,b,n,u,p)});a=b[b.length-1]||"";!d&&"table-cell"!=e||!a||
108
108
  za(a)||(b[b.length-1]+=" ");f&&"run-in"!=e&&!/^[\s\xa0]*$/.test(a)&&b.push("")}}function kd(a,b){ld(a,b,function(c,d,e,f,g){3==c.nodeType&&e?nd(c,d,f,g):S(c)&&kd(c,d)})}var md="inline inline-block inline-table none table-cell table-column table-column-group".split(" ");
109
- function nd(a,b,c,d){a=a.nodeValue.replace(/[\u200b\u200e\u200f]/g,"");a=a.replace(/(\r\n|\r|\n)/g,"\n");if("normal"==c||"nowrap"==c)a=a.replace(/\n/g," ");a="pre"==c||"pre-wrap"==c?a.replace(/[ \f\t\v\u2028\u2029]/g,"\u00a0"):a.replace(/[ \f\t\v\u2028\u2029]+/g," ");"capitalize"==d?a=a.replace(t?/(^|\s|\b)(\S)/g:/(^|[^\d\p{L}\p{S}])([\p{Ll}|\p{S}])/gu,function(e,f,g){return f+g.toUpperCase()}):"uppercase"==d?a=a.toUpperCase():"lowercase"==d&&(a=a.toLowerCase());c=b.pop()||"";za(c)&&0==a.lastIndexOf(" ",
110
- 0)&&(a=a.substr(1));b.push(c+a)}function dd(a){if(Mc){if("relative"==U(a,"position"))return 1;a=U(a,"filter");return(a=a.match(/^alpha\(opacity=(\d*)\)/)||a.match(/^progid:DXImageTransform.Microsoft.Alpha\(Opacity=(\d*)\)/))?Number(a[1])/100:1}return od(a)}function od(a){var b=1,c=U(a,"opacity");c&&(b=Number(c));(a=Zc(a))&&(b*=od(a));return b}
109
+ function nd(a,b,c,d){a=a.nodeValue.replace(/[\u200b\u200e\u200f]/g,"");a=a.replace(/(\r\n|\r|\n)/g,"\n");if("normal"==c||"nowrap"==c)a=a.replace(/\n/g," ");a="pre"==c||"pre-wrap"==c?a.replace(/[ \f\t\v\u2028\u2029]/g,"\u00a0"):a.replace(/[ \f\t\v\u2028\u2029]+/g," ");"capitalize"==d?a=a.replace(t?/(^|\s|\b)(\S)/g:/(^|\s|\b)(\S)/gu,function(e,f,g){return f+g.toUpperCase()}):"uppercase"==d?a=a.toUpperCase():"lowercase"==d&&(a=a.toLowerCase());c=b.pop()||"";za(c)&&0==a.lastIndexOf(" ",0)&&(a=a.substr(1));
110
+ b.push(c+a)}function dd(a){if(Mc){if("relative"==U(a,"position"))return 1;a=U(a,"filter");return(a=a.match(/^alpha\(opacity=(\d*)\)/)||a.match(/^progid:DXImageTransform.Microsoft.Alpha\(Opacity=(\d*)\)/))?Number(a[1])/100:1}return od(a)}function od(a){var b=1,c=U(a,"opacity");c&&(b=Number(c));(a=Zc(a))&&(b*=od(a));return b}
111
111
  function pd(a,b,c,d,e){if(3==a.nodeType&&c)nd(a,b,d,e);else if(S(a))if(S(a,"CONTENT")||S(a,"SLOT")){for(var f=a;f.parentNode;)f=f.parentNode;f instanceof ShadowRoot?(f=S(a,"CONTENT")?a.getDistributedNodes():a.assignedNodes(),l(0<f.length?f:a.childNodes,function(g){pd(g,b,c,d,e)})):jd(a,b)}else if(S(a,"SHADOW")){for(f=a;f.parentNode;)f=f.parentNode;if(f instanceof ShadowRoot&&(a=f))for(a=a.olderShadowRoot;a;)l(a.childNodes,function(g){pd(g,b,c,d,e)}),a=a.olderShadowRoot}else jd(a,b)}
112
112
  function jd(a,b){a.shadowRoot&&l(a.shadowRoot.childNodes,function(c){pd(c,b,!0,null,null)});ld(a,b,function(c,d,e,f,g){var h=null;1==c.nodeType?h=c:3==c.nodeType&&(h=c);null!=h&&(null!=h.assignedSlot||h.getDestinationInsertionPoints&&0<h.getDestinationInsertionPoints().length)||pd(c,d,e,f,g)})};var qd={C:function(a,b){return!(!a.querySelectorAll||!a.querySelector)&&!/^\d.*/.test(b)},o:function(a,b){var c=eb(b),d="string"===typeof a?c.a.getElementById(a):a;return d?Uc(d,"id")==a&&b!=d&&hb(b,d)?d:ua(mb(c,"*"),function(e){return Uc(e,"id")==a&&b!=e&&hb(b,e)}):null},j:function(a,b){if(!a)return[];if(qd.C(b,a))try{return b.querySelectorAll("#"+qd.T(a))}catch(c){return[]}b=mb(eb(b),"*",null,b);return pa(b,function(c){return Uc(c,"id")==a})},T:function(a){return a.replace(/([\s'"\\#.:;,!?+<>=~*^$|%&@`{}\-\/\[\]\(\)])/g,
113
113
  "\\$1")}};var X={},rd={};X.N=function(a,b,c){try{var d=Nc.j("a",b)}catch(e){d=mb(eb(b),"A",null,b)}return ua(d,function(e){e=id(e);e=e.replace(/^[\s]+|[\s]+$/g,"");return c&&-1!=e.indexOf(a)||e==a})};X.K=function(a,b,c){try{var d=Nc.j("a",b)}catch(e){d=mb(eb(b),"A",null,b)}return pa(d,function(e){e=id(e);e=e.replace(/^[\s]+|[\s]+$/g,"");return c&&-1!=e.indexOf(a)||e==a})};X.o=function(a,b){return X.N(a,b,!1)};X.j=function(a,b){return X.K(a,b,!1)};rd.o=function(a,b){return X.N(a,b,!0)};
package/lib/http.js CHANGED
@@ -25,7 +25,7 @@
25
25
 
26
26
  'use strict'
27
27
 
28
- const path = require('path')
28
+ const path = require('node:path')
29
29
  const cmd = require('./command')
30
30
  const error = require('./error')
31
31
  const logging = require('./logging')
@@ -161,10 +161,10 @@ function resource(method, path) {
161
161
  }
162
162
 
163
163
  /** @typedef {{method: string, path: string}} */
164
- var CommandSpec // eslint-disable-line
164
+ var CommandSpec
165
165
 
166
166
  /** @typedef {function(!cmd.Command): !cmd.Command} */
167
- var CommandTransformer // eslint-disable-line
167
+ var CommandTransformer
168
168
 
169
169
  class InternalTypeError extends TypeError {}
170
170
 
@@ -337,7 +337,7 @@ class Client {
337
337
  * @return {!Promise<Response>} A promise that will be fulfilled with the
338
338
  * server's response.
339
339
  */
340
- send(httpRequest) {} // eslint-disable-line
340
+ send(httpRequest) {}
341
341
  }
342
342
 
343
343
  /**
@@ -472,6 +472,7 @@ class Executor {
472
472
  function tryParse(str) {
473
473
  try {
474
474
  return JSON.parse(str)
475
+ /*eslint no-unused-vars: "off"*/
475
476
  } catch (ignored) {
476
477
  // Do nothing.
477
478
  }
package/lib/input.js CHANGED
@@ -162,7 +162,6 @@ class FileDetector {
162
162
  * @package
163
163
  */
164
164
  handleFile(_driver, path) {
165
- // eslint-disable-line
166
165
  return Promise.resolve(path)
167
166
  }
168
167
  }
@@ -15,7 +15,7 @@
15
15
  // specific language governing permissions and limitations
16
16
  // under the License.
17
17
 
18
- const crypto = require('crypto')
18
+ const crypto = require('node:crypto')
19
19
 
20
20
  class PinnedScript {
21
21
  constructor(script) {
package/lib/select.js CHANGED
@@ -36,7 +36,7 @@
36
36
 
37
37
  'use strict'
38
38
 
39
- const { By, escapeCss } = require('./by')
39
+ const { By } = require('./by')
40
40
  const error = require('./error')
41
41
 
42
42
  /**
@@ -51,23 +51,23 @@ class ISelect {
51
51
  * @return {!Promise<boolean>} Whether this select element supports selecting multiple options at the same time? This
52
52
  * is done by checking the value of the "multiple" attribute.
53
53
  */
54
- isMultiple() {} // eslint-disable-line
54
+ isMultiple() {}
55
55
 
56
56
  /**
57
57
  * @return {!Promise<!Array<!WebElement>>} All options belonging to this select tag
58
58
  */
59
- getOptions() {} // eslint-disable-line
59
+ getOptions() {}
60
60
 
61
61
  /**
62
62
  * @return {!Promise<!Array<!WebElement>>} All selected options belonging to this select tag
63
63
  */
64
- getAllSelectedOptions() {} // eslint-disable-line
64
+ getAllSelectedOptions() {}
65
65
 
66
66
  /**
67
67
  * @return {!Promise<!WebElement>} The first selected option in this select tag (or the currently selected option in a
68
68
  * normal select)
69
69
  */
70
- getFirstSelectedOption() {} // eslint-disable-line
70
+ getFirstSelectedOption() {}
71
71
 
72
72
  /**
73
73
  * Select all options that display text matching the argument. That is, when given "Bar" this
@@ -105,7 +105,7 @@ class ISelect {
105
105
  *
106
106
  * @return {Promise<void>}
107
107
  */
108
- deselectAll() {} // eslint-disable-line
108
+ deselectAll() {}
109
109
 
110
110
  /**
111
111
  * Deselect all options that display text matching the argument. That is, when given "Bar" this
@@ -146,6 +146,10 @@ class Select {
146
146
  * @param {WebElement} element Select WebElement.
147
147
  */
148
148
  constructor(element) {
149
+ if (element === null) {
150
+ throw new Error(`Element must not be null. Please provide a valid <select> element.`)
151
+ }
152
+
149
153
  this.element = element
150
154
 
151
155
  this.element.getAttribute('tagName').then(function (tagName) {
@@ -153,6 +157,10 @@ class Select {
153
157
  throw new Error(`Select only works on <select> elements`)
154
158
  }
155
159
  })
160
+
161
+ this.element.getAttribute('multiple').then((multiple) => {
162
+ this.multiple = multiple !== null && multiple !== 'false'
163
+ })
156
164
  }
157
165
 
158
166
  /**
@@ -216,9 +224,7 @@ class Select {
216
224
  let matched = false
217
225
  let isMulti = await this.isMultiple()
218
226
 
219
- let options = await this.element.findElements({
220
- css: 'option[value =' + escapeCss(value) + ']',
221
- })
227
+ let options = await this.element.findElements(By.xpath('.//option[@value = ' + escapeQuotes(value) + ']'))
222
228
 
223
229
  for (let option of options) {
224
230
  await this.setSelected(option)
@@ -254,30 +260,46 @@ class Select {
254
260
  async selectByVisibleText(text) {
255
261
  text = typeof text === 'number' ? text.toString() : text
256
262
 
257
- const normalized = text
258
- .trim() // strip leading and trailing white-space characters
259
- .replace(/\s+/, ' ') // replace sequences of whitespace characters by a single space
263
+ const xpath = './/option[normalize-space(.) = ' + escapeQuotes(text) + ']'
260
264
 
261
- /**
262
- * find option element using xpath
263
- */
264
- const formatted = /"/.test(normalized)
265
- ? 'concat("' + normalized.split('"').join('", \'"\', "') + '")'
266
- : `"${normalized}"`
267
- const dotFormat = `[. = ${formatted}]`
268
- const spaceFormat = `[normalize-space(text()) = ${formatted}]`
269
-
270
- const selections = [
271
- `./option${dotFormat}`,
272
- `./option${spaceFormat}`,
273
- `./optgroup/option${dotFormat}`,
274
- `./optgroup/option${spaceFormat}`,
275
- ]
276
-
277
- const optionElement = await this.element.findElement({
278
- xpath: selections.join('|'),
279
- })
280
- await this.setSelected(optionElement)
265
+ const options = await this.element.findElements(By.xpath(xpath))
266
+
267
+ for (let option of options) {
268
+ await this.setSelected(option)
269
+ if (!(await this.isMultiple())) {
270
+ return
271
+ }
272
+ }
273
+
274
+ let matched = Array.isArray(options) && options.length > 0
275
+
276
+ if (!matched && text.includes(' ')) {
277
+ const subStringWithoutSpace = getLongestSubstringWithoutSpace(text)
278
+ let candidates
279
+ if ('' === subStringWithoutSpace) {
280
+ candidates = await this.element.findElements(By.tagName('option'))
281
+ } else {
282
+ const xpath = './/option[contains(., ' + escapeQuotes(subStringWithoutSpace) + ')]'
283
+ candidates = await this.element.findElements(By.xpath(xpath))
284
+ }
285
+
286
+ const trimmed = text.trim()
287
+
288
+ for (let option of candidates) {
289
+ const optionText = await option.getText()
290
+ if (trimmed === optionText.trim()) {
291
+ await this.setSelected(option)
292
+ if (!(await this.isMultiple())) {
293
+ return
294
+ }
295
+ matched = true
296
+ }
297
+ }
298
+ }
299
+
300
+ if (!matched) {
301
+ throw new Error(`Cannot locate option with text: ${text}`)
302
+ }
281
303
  }
282
304
 
283
305
  /**
@@ -293,7 +315,7 @@ class Select {
293
315
  * @returns {Promise<boolean>}
294
316
  */
295
317
  async isMultiple() {
296
- return (await this.element.getAttribute('multiple')) !== null
318
+ return this.multiple
297
319
  }
298
320
 
299
321
  /**
@@ -353,29 +375,9 @@ class Select {
353
375
  */
354
376
  text = typeof text === 'number' ? text.toString() : text
355
377
 
356
- const normalized = text
357
- .trim() // strip leading and trailing white-space characters
358
- .replace(/\s+/, ' ') // replace sequences of whitespace characters by a single space
359
-
360
- /**
361
- * find option element using xpath
362
- */
363
- const formatted = /"/.test(normalized)
364
- ? 'concat("' + normalized.split('"').join('", \'"\', "') + '")'
365
- : `"${normalized}"`
366
- const dotFormat = `[. = ${formatted}]`
367
- const spaceFormat = `[normalize-space(text()) = ${formatted}]`
368
-
369
- const selections = [
370
- `./option${dotFormat}`,
371
- `./option${spaceFormat}`,
372
- `./optgroup/option${dotFormat}`,
373
- `./optgroup/option${spaceFormat}`,
374
- ]
375
-
376
- const optionElement = await this.element.findElement({
377
- xpath: selections.join('|'),
378
- })
378
+ const optionElement = await this.element.findElement(
379
+ By.xpath('.//option[normalize-space(.) = ' + escapeQuotes(text) + ']'),
380
+ )
379
381
  if (await optionElement.isSelected()) {
380
382
  await optionElement.click()
381
383
  }
@@ -431,9 +433,11 @@ class Select {
431
433
 
432
434
  let matched = false
433
435
 
434
- let options = await this.element.findElements({
435
- css: 'option[value =' + escapeCss(value) + ']',
436
- })
436
+ let options = await this.element.findElements(By.xpath('.//option[@value = ' + escapeQuotes(value) + ']'))
437
+
438
+ if (options.length === 0) {
439
+ throw new Error(`Cannot locate option with value: ${value}`)
440
+ }
437
441
 
438
442
  for (let option of options) {
439
443
  if (await option.isSelected()) {
@@ -457,4 +461,42 @@ class Select {
457
461
  }
458
462
  }
459
463
 
460
- module.exports = { Select }
464
+ function escapeQuotes(toEscape) {
465
+ if (toEscape.includes(`"`) && toEscape.includes(`'`)) {
466
+ const quoteIsLast = toEscape.lastIndexOf(`"`) === toEscape.length - 1
467
+ const substrings = toEscape.split(`"`)
468
+
469
+ // Remove the last element if it's an empty string
470
+ if (substrings[substrings.length - 1] === '') {
471
+ substrings.pop()
472
+ }
473
+
474
+ let result = 'concat('
475
+
476
+ for (let i = 0; i < substrings.length; i++) {
477
+ result += `"${substrings[i]}"`
478
+ result += i === substrings.length - 1 ? (quoteIsLast ? `, '"')` : `)`) : `, '"', `
479
+ }
480
+ return result
481
+ }
482
+
483
+ if (toEscape.includes('"')) {
484
+ return `'${toEscape}'`
485
+ }
486
+
487
+ // Otherwise return the quoted string
488
+ return `"${toEscape}"`
489
+ }
490
+
491
+ function getLongestSubstringWithoutSpace(text) {
492
+ let words = text.split(' ')
493
+ let longestString = ''
494
+ for (let word of words) {
495
+ if (word.length > longestString.length) {
496
+ longestString = word
497
+ }
498
+ }
499
+ return longestString
500
+ }
501
+
502
+ module.exports = { Select, escapeQuotes }
package/lib/until.js CHANGED
@@ -43,9 +43,9 @@
43
43
 
44
44
  const by = require('./by')
45
45
  const error = require('./error')
46
- const webdriver = require('./webdriver'),
47
- Condition = webdriver.Condition,
48
- WebElementCondition = webdriver.WebElementCondition
46
+ const webdriver = require('./webdriver')
47
+ const Condition = webdriver.Condition
48
+ const WebElementCondition = webdriver.WebElementCondition
49
49
 
50
50
  /**
51
51
  * Creates a condition that will wait until the input driver is able to switch
package/lib/util.js CHANGED
@@ -38,6 +38,7 @@ function isPromise(value) {
38
38
  // Use array notation so the Closure compiler does not obfuscate away our
39
39
  // contract.
40
40
  return (typeof value === 'object' || typeof value === 'function') && typeof value['then'] === 'function'
41
+ /*eslint no-unused-vars: "off"*/
41
42
  } catch (ex) {
42
43
  return false
43
44
  }