selenium-webdriver 4.0.0-beta.4 → 4.1.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.
package/lib/webdriver.js CHANGED
@@ -29,7 +29,6 @@ const input = require('./input')
29
29
  const logging = require('./logging')
30
30
  const promise = require('./promise')
31
31
  const Symbols = require('./symbols')
32
- const cdpTargets = ['page', 'browser']
33
32
  const cdp = require('../devtools/CDPConnection')
34
33
  const WebSocket = require('ws')
35
34
  const http = require('../http/index')
@@ -37,19 +36,21 @@ const fs = require('fs')
37
36
  const { Capabilities } = require('./capabilities')
38
37
  const path = require('path')
39
38
  const { NoSuchElementError } = require('./error')
39
+ const cdpTargets = ['page', 'browser']
40
40
 
41
41
  // Capability names that are defined in the W3C spec.
42
42
  const W3C_CAPABILITY_NAMES = new Set([
43
43
  'acceptInsecureCerts',
44
44
  'browserName',
45
45
  'browserVersion',
46
- 'platformName',
47
46
  'pageLoadStrategy',
47
+ 'platformName',
48
48
  'proxy',
49
49
  'setWindowRect',
50
- 'timeouts',
51
50
  'strictFileInteractability',
51
+ 'timeouts',
52
52
  'unhandledPromptBehavior',
53
+ 'webSocketUrl',
53
54
  ])
54
55
 
55
56
  /**
@@ -208,6 +209,9 @@ function fromWireValue(driver, value) {
208
209
  } else if (WebElement.isId(value)) {
209
210
  let id = WebElement.extractId(value)
210
211
  value = new WebElement(driver, id)
212
+ } else if (ShadowRoot.isId(value)) {
213
+ let id = ShadowRoot.extractId(value)
214
+ value = new ShadowRoot(driver, id)
211
215
  } else if (value && typeof value === 'object') {
212
216
  let result = {}
213
217
  for (let key in value) {
@@ -258,18 +262,18 @@ class IWebDriver {
258
262
  /**
259
263
  * @return {!command.Executor} The command executor used by this instance.
260
264
  */
261
- getExecutor() {}
265
+ getExecutor() { }
262
266
 
263
267
  /**
264
268
  * @return {!Promise<!Session>} A promise for this client's session.
265
269
  */
266
- getSession() {}
270
+ getSession() { }
267
271
 
268
272
  /**
269
273
  * @return {!Promise<!Capabilities>} A promise that will resolve with
270
274
  * the this instance's capabilities.
271
275
  */
272
- getCapabilities() {}
276
+ getCapabilities() { }
273
277
 
274
278
  /**
275
279
  * Terminates the browser session. After calling quit, this instance will be
@@ -279,7 +283,7 @@ class IWebDriver {
279
283
  * @return {!Promise<void>} A promise that will be resolved when the
280
284
  * command has completed.
281
285
  */
282
- quit() {}
286
+ quit() { }
283
287
 
284
288
  /**
285
289
  * Creates a new action sequence using this driver. The sequence will not be
@@ -458,7 +462,7 @@ class IWebDriver {
458
462
  timeout = undefined, // eslint-disable-line
459
463
  message = undefined, // eslint-disable-line
460
464
  pollTimeout = undefined // eslint-disable-line
461
- ) {}
465
+ ) { }
462
466
 
463
467
  /**
464
468
  * Makes the driver sleep for the given amount of time.
@@ -475,7 +479,7 @@ class IWebDriver {
475
479
  * @return {!Promise<string>} A promise that will be resolved with the current
476
480
  * window handle.
477
481
  */
478
- getWindowHandle() {}
482
+ getWindowHandle() { }
479
483
 
480
484
  /**
481
485
  * Retrieves a list of all available window handles.
@@ -483,7 +487,7 @@ class IWebDriver {
483
487
  * @return {!Promise<!Array<string>>} A promise that will be resolved with an
484
488
  * array of window handles.
485
489
  */
486
- getAllWindowHandles() {}
490
+ getAllWindowHandles() { }
487
491
 
488
492
  /**
489
493
  * Retrieves the current page's source. The returned source is a representation
@@ -493,7 +497,7 @@ class IWebDriver {
493
497
  * @return {!Promise<string>} A promise that will be resolved with the current
494
498
  * page source.
495
499
  */
496
- getPageSource() {}
500
+ getPageSource() { }
497
501
 
498
502
  /**
499
503
  * Closes the current window.
@@ -501,7 +505,7 @@ class IWebDriver {
501
505
  * @return {!Promise<void>} A promise that will be resolved when this command
502
506
  * has completed.
503
507
  */
504
- close() {}
508
+ close() { }
505
509
 
506
510
  /**
507
511
  * Navigates to the given URL.
@@ -518,7 +522,7 @@ class IWebDriver {
518
522
  * @return {!Promise<string>} A promise that will be resolved with the
519
523
  * current URL.
520
524
  */
521
- getCurrentUrl() {}
525
+ getCurrentUrl() { }
522
526
 
523
527
  /**
524
528
  * Retrieves the current page title.
@@ -526,7 +530,7 @@ class IWebDriver {
526
530
  * @return {!Promise<string>} A promise that will be resolved with the current
527
531
  * page's title.
528
532
  */
529
- getTitle() {}
533
+ getTitle() { }
530
534
 
531
535
  /**
532
536
  * Locates an element on the page. If the element cannot be found, a
@@ -590,23 +594,23 @@ class IWebDriver {
590
594
  * @return {!Promise<string>} A promise that will be resolved to the
591
595
  * screenshot as a base-64 encoded PNG.
592
596
  */
593
- takeScreenshot() {}
597
+ takeScreenshot() { }
594
598
 
595
599
  /**
596
600
  * @return {!Options} The options interface for this instance.
597
601
  */
598
- manage() {}
602
+ manage() { }
599
603
 
600
604
  /**
601
605
  * @return {!Navigation} The navigation interface for this instance.
602
606
  */
603
- navigate() {}
607
+ navigate() { }
604
608
 
605
609
  /**
606
610
  * @return {!TargetLocator} The target locator interface for this
607
611
  * instance.
608
612
  */
609
- switchTo() {}
613
+ switchTo() { }
610
614
 
611
615
  /**
612
616
  *
@@ -665,7 +669,7 @@ class WebDriver {
665
669
  // If session is a rejected promise, add a no-op rejection handler.
666
670
  // This effectively hides setup errors until users attempt to interact
667
671
  // with the session.
668
- this.session_.catch(function () {})
672
+ this.session_.catch(function () { })
669
673
 
670
674
  /** @private {!command.Executor} */
671
675
  this.executor_ = executor
@@ -761,12 +765,12 @@ class WebDriver {
761
765
  this.session_ = Promise.reject(
762
766
  new error.NoSuchSessionError(
763
767
  'This driver instance does not have a valid session ID ' +
764
- '(did you call WebDriver.quit()?) and may no longer be used.'
768
+ '(did you call WebDriver.quit()?) and may no longer be used.'
765
769
  )
766
770
  )
767
771
 
768
772
  // Only want the session rejection to bubble if accessed.
769
- this.session_.catch(function () {})
773
+ this.session_.catch(function () { })
770
774
 
771
775
  if (this.onQuit_) {
772
776
  return this.onQuit_.call(void 0)
@@ -827,18 +831,15 @@ class WebDriver {
827
831
  let timeoutMessage = resolveWaitMessage(message)
828
832
  reject(
829
833
  new error.TimeoutError(
830
- `${timeoutMessage}Timed out waiting for promise to resolve after ${
831
- Date.now() - start
834
+ `${timeoutMessage}Timed out waiting for promise to resolve after ${Date.now() - start
832
835
  }ms`
833
836
  )
834
837
  )
835
838
  } catch (ex) {
836
839
  reject(
837
840
  new error.TimeoutError(
838
- `${
839
- ex.message
840
- }\nTimed out waiting for promise to resolve after ${
841
- Date.now() - start
841
+ `${ex.message
842
+ }\nTimed out waiting for promise to resolve after ${Date.now() - start
842
843
  }ms`
843
844
  )
844
845
  )
@@ -868,7 +869,7 @@ class WebDriver {
868
869
  if (typeof fn !== 'function') {
869
870
  throw TypeError(
870
871
  'Wait condition must be a promise-like object, function, or a ' +
871
- 'Condition object'
872
+ 'Condition object'
872
873
  )
873
874
  }
874
875
 
@@ -920,7 +921,7 @@ class WebDriver {
920
921
  if (!(value instanceof WebElement)) {
921
922
  throw TypeError(
922
923
  'WebElementCondition did not resolve to a WebElement: ' +
923
- Object.prototype.toString.call(value)
924
+ Object.prototype.toString.call(value)
924
925
  )
925
926
  }
926
927
  return value
@@ -1201,19 +1202,43 @@ class WebDriver {
1201
1202
  caps['map_'].get('moz:debuggerAddress') ||
1202
1203
  new Map()
1203
1204
  const debuggerUrl = seCdp || vendorInfo['debuggerAddress'] || vendorInfo
1204
- this._wsUrl = await this.getWsUrl(debuggerUrl, target)
1205
-
1205
+ this._wsUrl = await this.getWsUrl(debuggerUrl, target, caps)
1206
1206
  return new Promise((resolve, reject) => {
1207
1207
  try {
1208
1208
  this._wsConnection = new WebSocket(this._wsUrl)
1209
+ this._cdpConnection = new cdp.CdpConnection(this._wsConnection)
1209
1210
  } catch (err) {
1210
1211
  reject(err)
1211
1212
  return
1212
1213
  }
1213
1214
 
1214
- this._wsConnection.on('open', () => {
1215
- this._cdpConnection = new cdp.CdpConnection(this._wsConnection)
1216
- resolve(this._cdpConnection)
1215
+ this._wsConnection.on('open', async () => {
1216
+ await this.getCdpTargets()
1217
+ })
1218
+
1219
+ this._wsConnection.on('message', async (message) => {
1220
+ const params = JSON.parse(message)
1221
+ if (params.result) {
1222
+ if (params.result.targetInfos) {
1223
+ const targets = params.result.targetInfos
1224
+ const page = targets.find((info) => info.type === 'page')
1225
+ if (page) {
1226
+ this.targetID = page.targetId
1227
+ this._cdpConnection.execute(
1228
+ 'Target.attachToTarget',
1229
+ { targetId: this.targetID, flatten: true },
1230
+ null
1231
+ )
1232
+ } else {
1233
+ reject('Unable to find Page target.')
1234
+ }
1235
+ }
1236
+ if (params.result.sessionId) {
1237
+ this.sessionId = params.result.sessionId
1238
+ this._cdpConnection.sessionId = this.sessionId
1239
+ resolve(this._cdpConnection)
1240
+ }
1241
+ }
1217
1242
  })
1218
1243
 
1219
1244
  this._wsConnection.on('error', (error) => {
@@ -1222,33 +1247,47 @@ class WebDriver {
1222
1247
  })
1223
1248
  }
1224
1249
 
1250
+ async getCdpTargets() {
1251
+ this._cdpConnection.execute('Target.getTargets')
1252
+ }
1253
+
1225
1254
  /**
1226
1255
  * Retrieves 'webSocketDebuggerUrl' by sending a http request using debugger address
1227
1256
  * @param {string} debuggerAddress
1228
- * @param {string} target
1257
+ * @param target
1258
+ * @param caps
1229
1259
  * @return {string} Returns parsed webSocketDebuggerUrl obtained from the http request
1230
1260
  */
1231
- async getWsUrl(debuggerAddress, target) {
1261
+ async getWsUrl(debuggerAddress, target, caps) {
1232
1262
  if (target && cdpTargets.indexOf(target.toLowerCase()) === -1) {
1233
1263
  throw new error.InvalidArgumentError('invalid target value')
1234
1264
  }
1265
+
1266
+ if (debuggerAddress.match(/\/se\/cdp/)) {
1267
+ return debuggerAddress
1268
+ }
1269
+
1235
1270
  let path
1236
- if (target === 'page') {
1271
+ if (target === 'page' && caps['map_'].get('browserName') !== 'firefox') {
1237
1272
  path = '/json'
1273
+ } else if (
1274
+ target === 'page' &&
1275
+ caps['map_'].get('browserName') === 'firefox'
1276
+ ) {
1277
+ path = '/json/list'
1238
1278
  } else {
1239
1279
  path = '/json/version'
1240
1280
  }
1281
+
1241
1282
  let request = new http.Request('GET', path)
1242
1283
  let client = new http.HttpClient('http://' + debuggerAddress)
1243
1284
  let response = await client.send(request)
1244
- let url
1285
+
1245
1286
  if (target.toLowerCase() === 'page') {
1246
- url = JSON.parse(response.body)[0]['webSocketDebuggerUrl']
1287
+ return JSON.parse(response.body)[0]['webSocketDebuggerUrl']
1247
1288
  } else {
1248
- url = JSON.parse(response.body)['webSocketDebuggerUrl']
1289
+ return JSON.parse(response.body)['webSocketDebuggerUrl']
1249
1290
  }
1250
-
1251
- return url
1252
1291
  }
1253
1292
 
1254
1293
  /**
@@ -1260,54 +1299,81 @@ class WebDriver {
1260
1299
  * @param connection CDP Connection
1261
1300
  */
1262
1301
  async register(username, password, connection) {
1302
+ this._wsConnection.on('message', (message) => {
1303
+ const params = JSON.parse(message)
1304
+
1305
+ if (params.method === 'Fetch.authRequired') {
1306
+ const requestParams = params['params']
1307
+ connection.execute('Fetch.continueWithAuth', {
1308
+ requestId: requestParams['requestId'],
1309
+ authChallengeResponse: {
1310
+ response: 'ProvideCredentials',
1311
+ username: username,
1312
+ password: password,
1313
+ },
1314
+ })
1315
+ } else if (params.method === 'Fetch.requestPaused') {
1316
+ const requestPausedParams = params['params']
1317
+ connection.execute('Fetch.continueRequest', {
1318
+ requestId: requestPausedParams['requestId'],
1319
+ })
1320
+ }
1321
+ })
1322
+
1323
+ await connection.execute(
1324
+ 'Fetch.enable',
1325
+ {
1326
+ handleAuthRequests: true,
1327
+ },
1328
+ null
1329
+ )
1263
1330
  await connection.execute(
1264
1331
  'Network.setCacheDisabled',
1265
- this.getRandomNumber(1, 10),
1266
1332
  {
1267
1333
  cacheDisabled: true,
1268
1334
  },
1269
1335
  null
1270
1336
  )
1337
+ }
1271
1338
 
1339
+ /**
1340
+ * Handle Network interception requests
1341
+ * @param connection WebSocket connection to the browser
1342
+ * @param httpResponse Object representing what we are intercepting
1343
+ * as well as what should be returned.
1344
+ * @param callback callback called when we intercept requests.
1345
+ */
1346
+ async onIntercept(connection, httpResponse, callback) {
1272
1347
  this._wsConnection.on('message', (message) => {
1273
1348
  const params = JSON.parse(message)
1274
-
1275
- if (params.method === 'Fetch.authRequired') {
1276
- const requestParams = params['params']
1277
- connection.execute(
1278
- 'Fetch.continueWithAuth',
1279
- this.getRandomNumber(1, 10),
1280
- {
1281
- requestId: requestParams['requestId'],
1282
- authChallengeResponse: {
1283
- response: 'ProvideCredentials',
1284
- username: username,
1285
- password: password,
1286
- },
1287
- }
1288
- )
1289
- } else if (params.method === 'Fetch.requestPaused') {
1349
+ if (params.method === 'Fetch.requestPaused') {
1290
1350
  const requestPausedParams = params['params']
1291
- connection.execute(
1292
- 'Fetch.continueRequest',
1293
- this.getRandomNumber(1, 10),
1294
- {
1351
+ if (requestPausedParams.request.url == httpResponse.urlToIntercept) {
1352
+ connection.execute('Fetch.continueRequest', {
1295
1353
  requestId: requestPausedParams['requestId'],
1296
- }
1297
- )
1354
+ url: httpResponse.urlToIntercept,
1355
+ method: httpResponse.method,
1356
+ headers: httpResponse.headers,
1357
+ postData: httpResponse.body,
1358
+ })
1359
+ callback()
1360
+ } else {
1361
+ connection.execute('Fetch.continueRequest', {
1362
+ requestId: requestPausedParams['requestId'],
1363
+ })
1364
+ }
1298
1365
  }
1299
1366
  })
1300
1367
 
1368
+ await connection.execute('Fetch.enable', {}, null)
1301
1369
  await connection.execute(
1302
- 'Fetch.enable',
1303
- 1,
1370
+ 'Network.setCacheDisabled',
1304
1371
  {
1305
- handleAuthRequests: true,
1372
+ cacheDisabled: true,
1306
1373
  },
1307
1374
  null
1308
1375
  )
1309
1376
  }
1310
-
1311
1377
  /**
1312
1378
  *
1313
1379
  * @param connection
@@ -1315,16 +1381,8 @@ class WebDriver {
1315
1381
  * @returns {Promise<void>}
1316
1382
  */
1317
1383
  async onLogEvent(connection, callback) {
1318
- await connection.execute(
1319
- 'Runtime.enable',
1320
- this.getRandomNumber(1, 10),
1321
- {},
1322
- null
1323
- )
1324
-
1325
1384
  this._wsConnection.on('message', (message) => {
1326
1385
  const params = JSON.parse(message)
1327
-
1328
1386
  if (params.method === 'Runtime.consoleAPICalled') {
1329
1387
  const consoleEventParams = params['params']
1330
1388
  let event = {
@@ -1335,7 +1393,21 @@ class WebDriver {
1335
1393
 
1336
1394
  callback(event)
1337
1395
  }
1396
+
1397
+ if (params.method === 'Log.entryAdded') {
1398
+ const logEventParams = params['params']
1399
+ const logEntry = logEventParams['entry']
1400
+ let event = {
1401
+ level: logEntry['level'],
1402
+ timestamp: new Date(logEntry['timestamp']),
1403
+ message: logEntry['text'],
1404
+ }
1405
+
1406
+ callback(event)
1407
+ }
1408
+
1338
1409
  })
1410
+ await connection.execute('Runtime.enable', {}, null)
1339
1411
  }
1340
1412
 
1341
1413
  /**
@@ -1345,12 +1417,7 @@ class WebDriver {
1345
1417
  * @returns {Promise<void>}
1346
1418
  */
1347
1419
  async onLogException(connection, callback) {
1348
- await connection.execute(
1349
- 'Runtime.enable',
1350
- this.getRandomNumber(1, 10),
1351
- {},
1352
- null
1353
- )
1420
+ await connection.execute('Runtime.enable', {}, null)
1354
1421
 
1355
1422
  this._wsConnection.on('message', (message) => {
1356
1423
  const params = JSON.parse(message)
@@ -1373,22 +1440,11 @@ class WebDriver {
1373
1440
  * @returns {Promise<void>}
1374
1441
  */
1375
1442
  async logMutationEvents(connection, callback) {
1376
- await connection.execute(
1377
- 'Runtime.enable',
1378
- this.getRandomNumber(1, 10),
1379
- {},
1380
- null
1381
- )
1382
- await connection.execute(
1383
- 'Page.enable',
1384
- this.getRandomNumber(1, 10),
1385
- {},
1386
- null
1387
- )
1443
+ await connection.execute('Runtime.enable', {}, null)
1444
+ await connection.execute('Page.enable', {}, null)
1388
1445
 
1389
1446
  await connection.execute(
1390
1447
  'Runtime.addBinding',
1391
- this.getRandomNumber(1, 10),
1392
1448
  {
1393
1449
  name: '__webdriver_attribute',
1394
1450
  },
@@ -1418,7 +1474,6 @@ class WebDriver {
1418
1474
 
1419
1475
  await connection.execute(
1420
1476
  'Page.addScriptToEvaluateOnNewDocument',
1421
- this.getRandomNumber(1, 10),
1422
1477
  {
1423
1478
  source: mutationListener,
1424
1479
  },
@@ -1430,7 +1485,7 @@ class WebDriver {
1430
1485
  if (params.method === 'Runtime.bindingCalled') {
1431
1486
  let payload = JSON.parse(params['params']['payload'])
1432
1487
  let elements = await this.findElements({
1433
- css: '*[data-__webdriver_id=' + payload['target'],
1488
+ css: '*[data-__webdriver_id=' + by.escapeCss(payload['target']) + ']',
1434
1489
  })
1435
1490
 
1436
1491
  if (elements.length === 0) {
@@ -1447,10 +1502,6 @@ class WebDriver {
1447
1502
  }
1448
1503
  })
1449
1504
  }
1450
-
1451
- getRandomNumber(min, max) {
1452
- return Math.floor(Math.random() * (max - min + 1) + min)
1453
- }
1454
1505
  }
1455
1506
 
1456
1507
  /**
@@ -1732,7 +1783,7 @@ class Options {
1732
1783
  } else if (typeof value !== 'undefined') {
1733
1784
  throw TypeError(
1734
1785
  'invalid timeouts configuration:' +
1735
- ` expected "${key}" to be a number, got ${typeof value}`
1786
+ ` expected "${key}" to be a number, got ${typeof value}`
1736
1787
  )
1737
1788
  }
1738
1789
  }
@@ -1793,7 +1844,7 @@ function legacyTimeout(driver, type, ms) {
1793
1844
  *
1794
1845
  * @record
1795
1846
  */
1796
- Options.Cookie = function () {}
1847
+ Options.Cookie = function () { }
1797
1848
 
1798
1849
  /**
1799
1850
  * The name of the cookie.
@@ -2108,7 +2159,7 @@ class TargetLocator {
2108
2159
  * @return {!WebElementPromise} The active element.
2109
2160
  */
2110
2161
  activeElement() {
2111
- var id = this.driver_.execute(
2162
+ const id = this.driver_.execute(
2112
2163
  new command.Command(command.Name.GET_ACTIVE_ELEMENT)
2113
2164
  )
2114
2165
  return new WebElementPromise(this.driver_, id)
@@ -2200,7 +2251,7 @@ class TargetLocator {
2200
2251
  * when the driver has changed focus to the new window.
2201
2252
  */
2202
2253
  newWindow(typeHint) {
2203
- var driver = this.driver_
2254
+ const driver = this.driver_
2204
2255
  return this.driver_
2205
2256
  .execute(
2206
2257
  new command.Command(command.Name.SWITCH_TO_NEW_WINDOW).setParameter(
@@ -2222,10 +2273,10 @@ class TargetLocator {
2222
2273
  * @return {!AlertPromise} The open alert.
2223
2274
  */
2224
2275
  alert() {
2225
- var text = this.driver_.execute(
2276
+ const text = this.driver_.execute(
2226
2277
  new command.Command(command.Name.GET_ALERT_TEXT)
2227
2278
  )
2228
- var driver = this.driver_
2279
+ const driver = this.driver_
2229
2280
  return new AlertPromise(
2230
2281
  driver,
2231
2282
  text.then(function (text) {
@@ -2243,6 +2294,7 @@ class TargetLocator {
2243
2294
 
2244
2295
  const LEGACY_ELEMENT_ID_KEY = 'ELEMENT'
2245
2296
  const ELEMENT_ID_KEY = 'element-6066-11e4-a52e-4f735466cecf'
2297
+ const SHADOWROOT_ID_KEY = 'shadow-6066-11e4-a52e-4f735466cecf'
2246
2298
 
2247
2299
  /**
2248
2300
  * Represents a DOM element. WebElements can be found by searching from the
@@ -2498,18 +2550,18 @@ class WebElement {
2498
2550
  */
2499
2551
  async sendKeys(...args) {
2500
2552
  let keys = []
2501
- ;(await Promise.all(args)).forEach((key) => {
2502
- let type = typeof key
2503
- if (type === 'number') {
2504
- key = String(key)
2505
- } else if (type !== 'string') {
2506
- throw TypeError('each key must be a number of string; got ' + type)
2507
- }
2553
+ ; (await Promise.all(args)).forEach((key) => {
2554
+ let type = typeof key
2555
+ if (type === 'number') {
2556
+ key = String(key)
2557
+ } else if (type !== 'string') {
2558
+ throw TypeError('each key must be a number of string; got ' + type)
2559
+ }
2508
2560
 
2509
- // The W3C protocol requires keys to be specified as an array where
2510
- // each element is a single key.
2511
- keys.push(...key.split(''))
2512
- })
2561
+ // The W3C protocol requires keys to be specified as an array where
2562
+ // each element is a single key.
2563
+ keys.push(...key.split(''))
2564
+ })
2513
2565
 
2514
2566
  if (!this.driver_.fileDetector_) {
2515
2567
  return this.execute_(
@@ -2555,7 +2607,7 @@ class WebElement {
2555
2607
  * requested CSS value.
2556
2608
  */
2557
2609
  getCssValue(cssStyleProperty) {
2558
- var name = command.Name.GET_ELEMENT_VALUE_OF_CSS_PROPERTY
2610
+ const name = command.Name.GET_ELEMENT_VALUE_OF_CSS_PROPERTY
2559
2611
  return this.execute_(
2560
2612
  new command.Command(name).setParameter('propertyName', cssStyleProperty)
2561
2613
  )
@@ -2614,6 +2666,16 @@ class WebElement {
2614
2666
  )
2615
2667
  }
2616
2668
 
2669
+ /**
2670
+ * Get the shadow root of the current web element.
2671
+ * @returns {!Promise<ShadowRoot>} A promise that will be
2672
+ * resolved with the elements shadow root or rejected
2673
+ * with {@link NoSuchShadowRootError}
2674
+ */
2675
+ getShadowRoot() {
2676
+ return this.execute_(new command.Command(command.Name.GET_SHADOW_ROOT))
2677
+ }
2678
+
2617
2679
  /**
2618
2680
  * Get the visible (i.e. not hidden by CSS) innerText of this element,
2619
2681
  * including sub-elements, without any leading or trailing whitespace.
@@ -2699,7 +2761,13 @@ class WebElement {
2699
2761
  * when the form has been submitted.
2700
2762
  */
2701
2763
  submit() {
2702
- return this.execute_(new command.Command(command.Name.SUBMIT_ELEMENT))
2764
+ const form = this.findElement({ xpath: './ancestor-or-self::form' })
2765
+ this.driver_.executeScript(
2766
+ "var e = arguments[0].ownerDocument.createEvent('Event');" +
2767
+ "e.initEvent('submit', true, true);" +
2768
+ 'if (arguments[0].dispatchEvent(e)) { arguments[0].submit() }',
2769
+ form
2770
+ )
2703
2771
  }
2704
2772
 
2705
2773
  /**
@@ -2728,18 +2796,12 @@ class WebElement {
2728
2796
  * Take a screenshot of the visible region encompassed by this element's
2729
2797
  * bounding rectangle.
2730
2798
  *
2731
- * @param {boolean=} scroll Optional argument that indicates whether the
2732
- * element should be scrolled into view before taking a screenshot.
2733
- * Defaults to false.
2734
2799
  * @return {!Promise<string>} A promise that will be
2735
2800
  * resolved to the screenshot as a base-64 encoded PNG.
2736
2801
  */
2737
- takeScreenshot(scroll = false) {
2802
+ takeScreenshot() {
2738
2803
  return this.execute_(
2739
- new command.Command(command.Name.TAKE_ELEMENT_SCREENSHOT).setParameter(
2740
- 'scroll',
2741
- scroll
2742
- )
2804
+ new command.Command(command.Name.TAKE_ELEMENT_SCREENSHOT)
2743
2805
  )
2744
2806
  }
2745
2807
  }
@@ -2788,6 +2850,186 @@ class WebElementPromise extends WebElement {
2788
2850
  }
2789
2851
  }
2790
2852
 
2853
+ //////////////////////////////////////////////////////////////////////////////
2854
+ //
2855
+ // ShadowRoot
2856
+ //
2857
+ //////////////////////////////////////////////////////////////////////////////
2858
+
2859
+ /**
2860
+ * Represents a ShadowRoot of a {@link WebElement}. Provides functions to
2861
+ * retrieve elements that live in the DOM below the ShadowRoot.
2862
+ */
2863
+ class ShadowRoot {
2864
+
2865
+ constructor(driver, id) {
2866
+ this.driver_ = driver
2867
+ this.id_ = id
2868
+ }
2869
+
2870
+ /**
2871
+ * Extracts the encoded ShadowRoot ID from the object.
2872
+ *
2873
+ * @param {?} obj The object to extract the ID from.
2874
+ * @return {string} the extracted ID.
2875
+ * @throws {TypeError} if the object is not a valid encoded ID.
2876
+ */
2877
+ static extractId(obj) {
2878
+ if (obj && typeof obj === 'object') {
2879
+ if (typeof obj[SHADOWROOT_ID_KEY] === 'string') {
2880
+ return obj[SHADOWROOT_ID_KEY]
2881
+ }
2882
+ }
2883
+ throw new TypeError('object is not a ShadowRoot ID')
2884
+ }
2885
+
2886
+ /**
2887
+ * @param {?} obj the object to test.
2888
+ * @return {boolean} whether the object is a valid encoded WebElement ID.
2889
+ */
2890
+ static isId(obj) {
2891
+ return (
2892
+ obj &&
2893
+ typeof obj === 'object' &&
2894
+ typeof obj[SHADOWROOT_ID_KEY] === 'string')
2895
+ }
2896
+
2897
+ /**
2898
+ * @return {!Object} Returns the serialized representation of this ShadowRoot.
2899
+ */
2900
+ [Symbols.serialize]() {
2901
+ return this.getId()
2902
+ }
2903
+
2904
+ /**
2905
+ * Schedules a command that targets this element with the parent WebDriver
2906
+ * instance. Will ensure this element's ID is included in the command
2907
+ * parameters under the "id" key.
2908
+ *
2909
+ * @param {!command.Command} command The command to schedule.
2910
+ * @return {!Promise<T>} A promise that will be resolved with the result.
2911
+ * @template T
2912
+ * @see WebDriver#schedule
2913
+ * @private
2914
+ */
2915
+ execute_(command) {
2916
+ command.setParameter('id', this)
2917
+ return this.driver_.execute(command)
2918
+ }
2919
+
2920
+ /**
2921
+ * Schedule a command to find a descendant of this ShadowROot. If the element
2922
+ * cannot be found, the returned promise will be rejected with a
2923
+ * {@linkplain error.NoSuchElementError NoSuchElementError}.
2924
+ *
2925
+ * The search criteria for an element may be defined using one of the static
2926
+ * factories on the {@link by.By} class, or as a short-hand
2927
+ * {@link ./by.ByHash} object. For example, the following two statements
2928
+ * are equivalent:
2929
+ *
2930
+ * var e1 = shadowroot.findElement(By.id('foo'));
2931
+ * var e2 = shadowroot.findElement({id:'foo'});
2932
+ *
2933
+ * You may also provide a custom locator function, which takes as input this
2934
+ * instance and returns a {@link WebElement}, or a promise that will resolve
2935
+ * to a WebElement. If the returned promise resolves to an array of
2936
+ * WebElements, WebDriver will use the first element. For example, to find the
2937
+ * first visible link on a page, you could write:
2938
+ *
2939
+ * var link = element.findElement(firstVisibleLink);
2940
+ *
2941
+ * function firstVisibleLink(shadowRoot) {
2942
+ * var links = shadowRoot.findElements(By.tagName('a'));
2943
+ * return promise.filter(links, function(link) {
2944
+ * return link.isDisplayed();
2945
+ * });
2946
+ * }
2947
+ *
2948
+ * @param {!(by.By|Function)} locator The locator strategy to use when
2949
+ * searching for the element.
2950
+ * @return {!WebElementPromise} A WebElement that can be used to issue
2951
+ * commands against the located element. If the element is not found, the
2952
+ * element will be invalidated and all scheduled commands aborted.
2953
+ */
2954
+ findElement(locator) {
2955
+ locator = by.checkedLocator(locator)
2956
+ let id
2957
+ if (typeof locator === 'function') {
2958
+ id = this.driver_.findElementInternal_(locator, this)
2959
+ } else {
2960
+ let cmd = new command.Command(command.Name.FIND_ELEMENT_FROM_SHADOWROOT)
2961
+ .setParameter('using', locator.using)
2962
+ .setParameter('value', locator.value)
2963
+ id = this.execute_(cmd)
2964
+ }
2965
+ return new ShadowRootPromise(this.driver_, id)
2966
+ }
2967
+
2968
+ /**
2969
+ * Locates all of the descendants of this element that match the given search
2970
+ * criteria.
2971
+ *
2972
+ * @param {!(by.By|Function)} locator The locator strategy to use when
2973
+ * searching for the element.
2974
+ * @return {!Promise<!Array<!WebElement>>} A promise that will resolve to an
2975
+ * array of WebElements.
2976
+ */
2977
+ async findElements(locator) {
2978
+ locator = by.checkedLocator(locator)
2979
+ if (typeof locator === 'function') {
2980
+ return this.driver_.findElementsInternal_(locator, this)
2981
+ } else {
2982
+ let cmd = new command.Command(command.Name.FIND_ELEMENTS_FROM_SHADOWROOT)
2983
+ .setParameter('using', locator.using)
2984
+ .setParameter('value', locator.value)
2985
+ let result = await this.execute_(cmd)
2986
+ return Array.isArray(result) ? result : []
2987
+ }
2988
+ }
2989
+
2990
+ getId() {
2991
+ return this.id_
2992
+ }
2993
+ }
2994
+
2995
+ /**
2996
+ * ShadowRootPromise is a promise that will be fulfilled with a WebElement.
2997
+ * This serves as a forward proxy on ShadowRoot, allowing calls to be
2998
+ * scheduled without directly on this instance before the underlying
2999
+ * ShadowRoot has been fulfilled.
3000
+ *
3001
+ * @implements { IThenable<!ShadowRoot>}
3002
+ * @final
3003
+ */
3004
+ class ShadowRootPromise extends ShadowRoot {
3005
+ /**
3006
+ * @param {!WebDriver} driver The parent WebDriver instance for this
3007
+ * element.
3008
+ * @param {!Promise<!ShadowRoot>} el A promise
3009
+ * that will resolve to the promised element.
3010
+ */
3011
+ constructor(driver, shadow) {
3012
+ super(driver, 'unused')
3013
+
3014
+ /** @override */
3015
+ this.then = shadow.then.bind(shadow)
3016
+
3017
+ /** @override */
3018
+ this.catch = shadow.catch.bind(shadow)
3019
+
3020
+ /**
3021
+ * Defers returning the ShadowRoot ID until the wrapped WebElement has been
3022
+ * resolved.
3023
+ * @override
3024
+ */
3025
+ this.getId = function () {
3026
+ return shadow.then(function (shadow) {
3027
+ return shadow.getId()
3028
+ })
3029
+ }
3030
+ }
3031
+ }
3032
+
2791
3033
  //////////////////////////////////////////////////////////////////////////////
2792
3034
  //
2793
3035
  // Alert
@@ -2945,6 +3187,7 @@ module.exports = {
2945
3187
  Logs,
2946
3188
  Navigation,
2947
3189
  Options,
3190
+ ShadowRoot,
2948
3191
  TargetLocator,
2949
3192
  IWebDriver,
2950
3193
  WebDriver,