selenium-webdriver 3.1.0 → 3.6.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 (65) hide show
  1. package/CHANGES.md +112 -0
  2. package/NOTICE +1 -1
  3. package/README.md +5 -11
  4. package/chrome.js +122 -33
  5. package/edge.js +2 -2
  6. package/example/async_await_test.js +69 -0
  7. package/example/chrome_android.js +2 -3
  8. package/example/chrome_mobile_emulation.js +3 -4
  9. package/example/firefox_channels.js +3 -3
  10. package/example/google_search.js +3 -3
  11. package/example/google_search_generator.js +2 -5
  12. package/example/google_search_test.js +5 -6
  13. package/example/headless.js +55 -0
  14. package/example/logging.js +3 -3
  15. package/example/parallel_flows.js +4 -7
  16. package/firefox/binary.js +3 -0
  17. package/firefox/extension.js +118 -81
  18. package/firefox/index.js +117 -263
  19. package/firefox/profile.js +18 -137
  20. package/ie.js +2 -2
  21. package/index.js +4 -1
  22. package/io/index.js +59 -0
  23. package/io/zip.js +214 -0
  24. package/lib/atoms/get-attribute.js +9 -0
  25. package/lib/atoms/is-displayed.js +73 -78
  26. package/lib/by.js +8 -0
  27. package/lib/capabilities.js +61 -22
  28. package/lib/command.js +2 -0
  29. package/lib/error.js +32 -2
  30. package/lib/http.js +53 -44
  31. package/lib/promise.js +72 -25
  32. package/lib/test/data/click_tests/disabled_element.html +12 -0
  33. package/lib/test/data/content-editable.html +10 -0
  34. package/lib/test/data/firefox/webextension.xpi +0 -0
  35. package/lib/test/data/key_logger.html +34 -0
  36. package/lib/test/data/nestedElements.html +10 -1
  37. package/lib/test/data/scrolling_tests/page_with_partially_hidden_element.html +14 -0
  38. package/lib/test/data/single_text_input.html +12 -0
  39. package/lib/test/index.js +4 -36
  40. package/lib/webdriver.js +147 -55
  41. package/net/portprober.js +1 -1
  42. package/opera.js +1 -1
  43. package/package.json +2 -2
  44. package/phantomjs.js +2 -2
  45. package/remote/index.js +21 -14
  46. package/safari.js +2 -2
  47. package/test/firefox/extension_test.js +41 -17
  48. package/test/firefox/firefox_test.js +37 -19
  49. package/test/firefox/profile_test.js +14 -59
  50. package/test/http/http_test.js +18 -0
  51. package/test/{io_test.js → io/io_test.js} +40 -1
  52. package/test/io/zip_test.js +128 -0
  53. package/test/lib/by_test.js +21 -0
  54. package/test/lib/error_test.js +21 -1
  55. package/test/lib/http_test.js +11 -3
  56. package/test/lib/webdriver_test.js +107 -3
  57. package/test/page_loading_test.js +0 -6
  58. package/test/remote_test.js +3 -3
  59. package/test/session_test.js +68 -23
  60. package/testing/index.js +2 -3
  61. package/lib/atoms/getAttribute.js +0 -12
  62. package/lib/firefox/amd64/libnoblur64.so +0 -0
  63. package/lib/firefox/i386/libnoblur.so +0 -0
  64. package/lib/firefox/webdriver.json +0 -69
  65. package/lib/firefox/webdriver.xpi +0 -0
@@ -132,7 +132,7 @@ const Capability = {
132
132
  * Defines how the driver should handle unexpected alerts. The value should
133
133
  * be one of "accept", "dismiss", or "ignore".
134
134
  */
135
- UNEXPECTED_ALERT_BEHAVIOR: 'unexpectedAlertBehavior',
135
+ UNEXPECTED_ALERT_BEHAVIOR: 'unexpectedAlertBehaviour',
136
136
 
137
137
  /** Defines the browser version. */
138
138
  VERSION: 'version'
@@ -204,16 +204,19 @@ function toMap(hash) {
204
204
  /**
205
205
  * Describes a set of capabilities for a WebDriver session.
206
206
  */
207
- class Capabilities extends Map {
207
+ class Capabilities {
208
208
  /**
209
- * @param {(Capabilities|Map<string, ?>|Object)=} opt_other Another set of
209
+ * @param {(Capabilities|Map<string, ?>|Object)=} other Another set of
210
210
  * capabilities to initialize this instance from.
211
211
  */
212
- constructor(opt_other) {
213
- if (opt_other && !(opt_other instanceof Map)) {
214
- opt_other = toMap(opt_other);
212
+ constructor(other = undefined) {
213
+ if (other instanceof Capabilities) {
214
+ other = other.map_;
215
+ } else if (other && !(other instanceof Map)) {
216
+ other = toMap(other);
215
217
  }
216
- super(opt_other);
218
+ /** @private @const {!Map<string, ?>} */
219
+ this.map_ = new Map(other);
217
220
  }
218
221
 
219
222
  /**
@@ -221,8 +224,7 @@ class Capabilities extends Map {
221
224
  */
222
225
  static android() {
223
226
  return new Capabilities()
224
- .set(Capability.BROWSER_NAME, Browser.ANDROID)
225
- .set(Capability.PLATFORM, 'ANDROID');
227
+ .set(Capability.BROWSER_NAME, Browser.ANDROID);
226
228
  }
227
229
 
228
230
  /**
@@ -237,8 +239,7 @@ class Capabilities extends Map {
237
239
  */
238
240
  static edge() {
239
241
  return new Capabilities()
240
- .set(Capability.BROWSER_NAME, Browser.EDGE)
241
- .set(Capability.PLATFORM, 'WINDOWS');
242
+ .set(Capability.BROWSER_NAME, Browser.EDGE);
242
243
  }
243
244
 
244
245
  /**
@@ -253,8 +254,7 @@ class Capabilities extends Map {
253
254
  */
254
255
  static ie() {
255
256
  return new Capabilities().
256
- set(Capability.BROWSER_NAME, Browser.INTERNET_EXPLORER).
257
- set(Capability.PLATFORM, 'WINDOWS');
257
+ set(Capability.BROWSER_NAME, Browser.INTERNET_EXPLORER);
258
258
  }
259
259
 
260
260
  /**
@@ -262,8 +262,7 @@ class Capabilities extends Map {
262
262
  */
263
263
  static ipad() {
264
264
  return new Capabilities().
265
- set(Capability.BROWSER_NAME, Browser.IPAD).
266
- set(Capability.PLATFORM, 'MAC');
265
+ set(Capability.BROWSER_NAME, Browser.IPAD);
267
266
  }
268
267
 
269
268
  /**
@@ -271,8 +270,7 @@ class Capabilities extends Map {
271
270
  */
272
271
  static iphone() {
273
272
  return new Capabilities().
274
- set(Capability.BROWSER_NAME, Browser.IPHONE).
275
- set(Capability.PLATFORM, 'MAC');
273
+ set(Capability.BROWSER_NAME, Browser.IPHONE);
276
274
  }
277
275
 
278
276
  /**
@@ -296,8 +294,7 @@ class Capabilities extends Map {
296
294
  */
297
295
  static safari() {
298
296
  return new Capabilities().
299
- set(Capability.BROWSER_NAME, Browser.SAFARI).
300
- set(Capability.PLATFORM, 'MAC');
297
+ set(Capability.BROWSER_NAME, Browser.SAFARI);
301
298
  }
302
299
 
303
300
  /**
@@ -328,6 +325,35 @@ class Capabilities extends Map {
328
325
  return serialize(this);
329
326
  }
330
327
 
328
+ /**
329
+ * @param {string} key the parameter key to get.
330
+ * @return {T} the stored parameter value.
331
+ * @template T
332
+ */
333
+ get(key) {
334
+ return this.map_.get(key);
335
+ }
336
+
337
+ /**
338
+ * @param {string} key the key to test.
339
+ * @return {boolean} whether this capability set has the specified key.
340
+ */
341
+ has(key) {
342
+ return this.map_.has(key);
343
+ }
344
+
345
+ /**
346
+ * @return {!Iterator<string>} an iterator of the keys set.
347
+ */
348
+ keys() {
349
+ return this.map_.keys();
350
+ }
351
+
352
+ /** @return {number} The number of capabilities set. */
353
+ get size() {
354
+ return this.map_.size;
355
+ }
356
+
331
357
  /**
332
358
  * Merges another set of capabilities into this instance.
333
359
  * @param {!(Capabilities|Map<String, ?>|Object<string, ?>)} other The other
@@ -339,7 +365,12 @@ class Capabilities extends Map {
339
365
  throw new TypeError('no capabilities provided for merge');
340
366
  }
341
367
 
342
- if (!(other instanceof Map)) {
368
+ let map;
369
+ if (other instanceof Capabilities) {
370
+ map = other.map_;
371
+ } else if (other instanceof Map) {
372
+ map = other;
373
+ } else {
343
374
  other = toMap(other);
344
375
  }
345
376
 
@@ -350,18 +381,26 @@ class Capabilities extends Map {
350
381
  return this;
351
382
  }
352
383
 
384
+ /**
385
+ * Deletes an entry from this set of capabilities.
386
+ *
387
+ * @param {string} key the capability key to delete.
388
+ */
389
+ delete(key) {
390
+ this.map_.delete(key);
391
+ }
392
+
353
393
  /**
354
394
  * @param {string} key The capability key.
355
395
  * @param {*} value The capability value.
356
396
  * @return {!Capabilities} A self reference.
357
397
  * @throws {TypeError} If the `key` is not a string.
358
- * @override
359
398
  */
360
399
  set(key, value) {
361
400
  if (typeof key !== 'string') {
362
401
  throw new TypeError('Capability keys must be strings: ' + typeof key);
363
402
  }
364
- super.set(key, value);
403
+ this.map_.set(key, value);
365
404
  return this;
366
405
  }
367
406
 
package/lib/command.js CHANGED
@@ -150,6 +150,8 @@ const Name = {
150
150
  TAKE_ELEMENT_SCREENSHOT: 'takeElementScreenshot',
151
151
  IMPLICITLY_WAIT: 'implicitlyWait',
152
152
  SET_SCRIPT_TIMEOUT: 'setScriptTimeout',
153
+
154
+ GET_TIMEOUT: 'getTimeout',
153
155
  SET_TIMEOUT: 'setTimeout',
154
156
 
155
157
  ACCEPT_ALERT: 'acceptAlert',
package/lib/error.js CHANGED
@@ -28,6 +28,14 @@ class WebDriverError extends Error {
28
28
 
29
29
  /** @override */
30
30
  this.name = this.constructor.name;
31
+
32
+ /**
33
+ * A stacktrace reported by the remote webdriver endpoint that initially
34
+ * reported this error. This property will be an empty string if the remote
35
+ * end did not provide a stacktrace.
36
+ * @type {string}
37
+ */
38
+ this.remoteStacktrace = '';
31
39
  }
32
40
  }
33
41
 
@@ -43,11 +51,27 @@ class ElementNotSelectableError extends WebDriverError {
43
51
  }
44
52
 
45
53
 
54
+ /**
55
+ * Indicates a command could not be completed because the target element is
56
+ * not pointer or keyboard interactable. This will often occur if an element
57
+ * is present in the DOM, but not rendered (i.e. its CSS style has
58
+ * "display: none").
59
+ */
60
+ class ElementNotInteractableError extends WebDriverError {
61
+ /** @param {string=} opt_error the error message, if any. */
62
+ constructor(opt_error) {
63
+ super(opt_error);
64
+ }
65
+ }
66
+
67
+
46
68
  /**
47
69
  * An element command could not be completed because the element is not visible
48
70
  * on the page.
71
+ *
72
+ * @deprecated Use {@link ElementNotInteractable} instead.
49
73
  */
50
- class ElementNotVisibleError extends WebDriverError {
74
+ class ElementNotVisibleError extends ElementNotInteractableError {
51
75
  /** @param {string=} opt_error the error message, if any. */
52
76
  constructor(opt_error) {
53
77
  super(opt_error);
@@ -392,6 +416,7 @@ const LEGACY_ERROR_CODE_TO_TYPE = new Map([
392
416
 
393
417
  const ERROR_CODE_TO_TYPE = new Map([
394
418
  ['unknown error', WebDriverError],
419
+ ['element not interactable', ElementNotInteractableError],
395
420
  ['element not selectable', ElementNotSelectableError],
396
421
  ['element not visible', ElementNotVisibleError],
397
422
  ['invalid argument', InvalidArgumentError],
@@ -486,7 +511,11 @@ function isErrorResponse(data) {
486
511
  function throwDecodedError(data) {
487
512
  if (isErrorResponse(data)) {
488
513
  let ctor = ERROR_CODE_TO_TYPE.get(data.error) || WebDriverError;
489
- throw new ctor(data.message);
514
+ let err = new ctor(data.message);
515
+ if (typeof data.stacktrace === 'string') {
516
+ err.remoteStacktrace = data.stacktrace;
517
+ }
518
+ throw err;
490
519
  }
491
520
  throw new WebDriverError('Unknown error: ' + JSON.stringify(data));
492
521
  }
@@ -535,6 +564,7 @@ module.exports = {
535
564
  ErrorCode: ErrorCode,
536
565
 
537
566
  WebDriverError: WebDriverError,
567
+ ElementNotInteractableError: ElementNotInteractableError,
538
568
  ElementNotSelectableError: ElementNotSelectableError,
539
569
  ElementNotVisibleError: ElementNotVisibleError,
540
570
  InvalidArgumentError: InvalidArgumentError,
package/lib/http.js CHANGED
@@ -32,10 +32,10 @@ const promise = require('./promise');
32
32
  const Session = require('./session').Session;
33
33
  const WebElement = require('./webdriver').WebElement;
34
34
 
35
- const {getAttribute, isDisplayed} = (function() {
35
+ const {getAttribute, isDisplayed} = /** @suppress {undefinedVars|uselessCode} */(function() {
36
36
  try {
37
37
  return {
38
- getAttribute: require('./atoms/getAttribute.js'),
38
+ getAttribute: require('./atoms/get-attribute.js'),
39
39
  isDisplayed: require('./atoms/is-displayed.js')
40
40
  };
41
41
  } catch (ex) {
@@ -225,6 +225,7 @@ const COMMAND_MAP = new Map([
225
225
  [cmd.Name.EXECUTE_SCRIPT, post('/session/:sessionId/execute')],
226
226
  [cmd.Name.EXECUTE_ASYNC_SCRIPT, post('/session/:sessionId/execute_async')],
227
227
  [cmd.Name.SCREENSHOT, get('/session/:sessionId/screenshot')],
228
+ [cmd.Name.GET_TIMEOUT, get('/session/:sessionId/timeouts')],
228
229
  [cmd.Name.SET_TIMEOUT, post('/session/:sessionId/timeouts')],
229
230
  [cmd.Name.MOVE_TO, post('/session/:sessionId/moveto')],
230
231
  [cmd.Name.CLICK, post('/session/:sessionId/click')],
@@ -256,6 +257,10 @@ const COMMAND_MAP = new Map([
256
257
  /** @const {!Map<string, (CommandSpec|CommandTransformer)>} */
257
258
  const W3C_COMMAND_MAP = new Map([
258
259
  [cmd.Name.GET_ACTIVE_ELEMENT, get('/session/:sessionId/element/active')],
260
+ [cmd.Name.GET_ALERT_TEXT, get('/session/:sessionId/alert/text')],
261
+ [cmd.Name.SET_ALERT_TEXT, post('/session/:sessionId/alert/text')],
262
+ [cmd.Name.ACCEPT_ALERT, post('/session/:sessionId/alert/accept')],
263
+ [cmd.Name.DISMISS_ALERT, post('/session/:sessionId/alert/dismiss')],
259
264
  [cmd.Name.GET_ELEMENT_ATTRIBUTE, (cmd) => {
260
265
  return toExecuteAtomCommand(cmd, Atom.GET_ATTRIBUTE, 'id', 'name');
261
266
  }],
@@ -264,11 +269,15 @@ const W3C_COMMAND_MAP = new Map([
264
269
  [cmd.Name.IS_ELEMENT_DISPLAYED, (cmd) => {
265
270
  return toExecuteAtomCommand(cmd, Atom.IS_DISPLAYED, 'id');
266
271
  }],
272
+ [cmd.Name.EXECUTE_SCRIPT, post('/session/:sessionId/execute/sync')],
273
+ [cmd.Name.EXECUTE_ASYNC_SCRIPT, post('/session/:sessionId/execute/async')],
267
274
  [cmd.Name.MAXIMIZE_WINDOW, post('/session/:sessionId/window/maximize')],
268
275
  [cmd.Name.GET_WINDOW_POSITION, get('/session/:sessionId/window/position')],
269
276
  [cmd.Name.SET_WINDOW_POSITION, post('/session/:sessionId/window/position')],
270
277
  [cmd.Name.GET_WINDOW_SIZE, get('/session/:sessionId/window/size')],
271
278
  [cmd.Name.SET_WINDOW_SIZE, post('/session/:sessionId/window/size')],
279
+ [cmd.Name.GET_CURRENT_WINDOW_HANDLE, get('/session/:sessionId/window')],
280
+ [cmd.Name.GET_WINDOW_HANDLES, get('/session/:sessionId/window/handles')],
272
281
  ]);
273
282
 
274
283
 
@@ -428,34 +437,29 @@ class Executor {
428
437
  return doSend(this, request).then(response => {
429
438
  this.log_.finer(() => `>>>\n${request}\n<<<\n${response}`);
430
439
 
431
- let parsed =
432
- parseHttpResponse(
433
- command, /** @type {!Response} */ (response), this.w3c);
440
+ let httpResponse = /** @type {!Response} */(response);
441
+ let {isW3C, value} = parseHttpResponse(command, httpResponse);
434
442
 
435
443
  if (command.getName() === cmd.Name.NEW_SESSION
436
444
  || command.getName() === cmd.Name.DESCRIBE_SESSION) {
437
- if (!parsed || !parsed['sessionId']) {
445
+ if (!value || !value.sessionId) {
438
446
  throw new error.WebDriverError(
439
- 'Unable to parse new session response: ' + response.body);
447
+ `Unable to parse new session response: ${response.body}`);
440
448
  }
441
449
 
442
450
  // The remote end is a W3C compliant server if there is no `status`
443
451
  // field in the response. This is not applicable for the DESCRIBE_SESSION
444
452
  // command, which is not defined in the W3C spec.
445
453
  if (command.getName() === cmd.Name.NEW_SESSION) {
446
- this.w3c = this.w3c || !('status' in parsed);
454
+ this.w3c = this.w3c || isW3C;
447
455
  }
448
456
 
449
- return new Session(parsed['sessionId'], parsed['value']);
457
+ // No implementations use the `capabilities` key yet...
458
+ let capabilities = value.capabilities || value.value;
459
+ return new Session(value.sessionId, capabilities);
450
460
  }
451
461
 
452
- if (parsed
453
- && typeof parsed === 'object'
454
- && 'value' in parsed) {
455
- let value = parsed['value'];
456
- return typeof value === 'undefined' ? null : value;
457
- }
458
- return parsed;
462
+ return typeof value === 'undefined' ? null : value;
459
463
  });
460
464
  });
461
465
  }
@@ -481,40 +485,45 @@ function tryParse(str) {
481
485
  *
482
486
  * @param {!cmd.Command} command The command the response is for.
483
487
  * @param {!Response} httpResponse The HTTP response to parse.
484
- * @param {boolean} w3c Whether the response should be processed using the
485
- * W3C wire protocol.
486
- * @return {?} The parsed response.
488
+ * @return {{isW3C: boolean, value: ?}} An object describing the parsed
489
+ * response. This object will have two fields: `isW3C` indicates whether
490
+ * the response looks like it came from a remote end that conforms with the
491
+ * W3C WebDriver spec, and `value`, the actual response value.
487
492
  * @throws {WebDriverError} If the HTTP response is an error.
488
493
  */
489
- function parseHttpResponse(command, httpResponse, w3c) {
490
- let parsed = tryParse(httpResponse.body);
491
- if (parsed !== undefined) {
492
- if (httpResponse.status < 200) {
493
- // This should never happen, but throw the raw response so
494
- // users report it.
495
- throw new error.WebDriverError(
496
- `Unexpected HTTP response:\n${httpResponse}`);
497
- }
494
+ function parseHttpResponse(command, httpResponse) {
495
+ if (httpResponse.status < 200) {
496
+ // This should never happen, but throw the raw response so users report it.
497
+ throw new error.WebDriverError(
498
+ `Unexpected HTTP response:\n${httpResponse}`);
499
+ }
498
500
 
499
- if (w3c) {
500
- if (httpResponse.status > 399) {
501
- error.throwDecodedError(parsed);
501
+ let parsed = tryParse(httpResponse.body);
502
+ if (parsed && typeof parsed === 'object') {
503
+ let value = parsed.value;
504
+ let isW3C =
505
+ value !== null && typeof value === 'object'
506
+ && typeof parsed.status === 'undefined';
507
+
508
+ if (!isW3C) {
509
+ error.checkLegacyResponse(parsed);
510
+
511
+ // Adjust legacy new session responses to look like W3C to simplify
512
+ // later processing.
513
+ if (command.getName() === cmd.Name.NEW_SESSION
514
+ || command.getName() == cmd.Name.DESCRIBE_SESSION) {
515
+ value = parsed;
502
516
  }
503
- return parsed;
504
- }
505
517
 
506
- // If this is a new session command, we need to check for a W3C compliant
507
- // error object. This is necessary since a successful new session command
508
- // is what puts the executor into W3C mode.
509
- if (httpResponse.status > 399
510
- && (command.getName() == cmd.Name.NEW_SESSION
511
- || command.getName() === cmd.Name.DESCRIBE_SESSION)
512
- && error.isErrorResponse(parsed)) {
513
- error.throwDecodedError(parsed);
518
+ } else if (httpResponse.status > 399) {
519
+ error.throwDecodedError(value);
514
520
  }
515
521
 
516
- error.checkLegacyResponse(parsed);
517
- return parsed;
522
+ return {isW3C, value};
523
+ }
524
+
525
+ if (parsed !== undefined) {
526
+ return {isW3C: false, value: parsed};
518
527
  }
519
528
 
520
529
  let value = httpResponse.body.replace(/\r\n/g, '\n');
@@ -527,7 +536,7 @@ function parseHttpResponse(command, httpResponse, w3c) {
527
536
  throw new error.WebDriverError(value);
528
537
  }
529
538
 
530
- return value || null;
539
+ return {isW3C: false, value: value || null};
531
540
  }
532
541
 
533
542
 
package/lib/promise.js CHANGED
@@ -33,13 +33,12 @@
33
33
  * > style tests:
34
34
  * >
35
35
  * > ```js
36
- * > const {Builder, By, promise, until} = require('selenium-webdriver');
36
+ * > const {Builder, By, Key, promise, until} = require('selenium-webdriver');
37
37
  * >
38
38
  * > let result = promise.consume(function* doGoogleSearch() {
39
39
  * > let driver = new Builder().forBrowser('firefox').build();
40
40
  * > yield driver.get('http://www.google.com/ncr');
41
- * > yield driver.findElement(By.name('q')).sendKeys('webdriver');
42
- * > yield driver.findElement(By.name('btnG')).click();
41
+ * > yield driver.findElement(By.name('q')).sendKeys('webdriver', Key.RETURN);
43
42
  * > yield driver.wait(until.titleIs('webdriver - Google Search'), 1000);
44
43
  * > yield driver.quit();
45
44
  * > });
@@ -63,8 +62,7 @@
63
62
  * control flow, allowing users to write
64
63
  *
65
64
  * driver.get('http://www.google.com/ncr');
66
- * driver.findElement({name: 'q'}).sendKeys('webdriver');
67
- * driver.findElement({name: 'btnGn'}).click();
65
+ * driver.findElement({name: 'q'}).sendKeys('webdriver', Key.RETURN);
68
66
  *
69
67
  * instead of
70
68
  *
@@ -73,13 +71,7 @@
73
71
  * return driver.findElement({name: 'q'});
74
72
  * })
75
73
  * .then(function(q) {
76
- * return q.sendKeys('webdriver');
77
- * })
78
- * .then(function() {
79
- * return driver.findElement({name: 'btnG'});
80
- * })
81
- * .then(function(btnG) {
82
- * return btnG.click();
74
+ * return q.sendKeys('webdriver', Key.RETURN);
83
75
  * });
84
76
  *
85
77
  * ## Tasks and Task Queues
@@ -1003,6 +995,9 @@ const PromiseState = {
1003
995
  */
1004
996
  const ON_CANCEL_HANDLER = new WeakMap;
1005
997
 
998
+ const SKIP_LOG = Symbol('skip-log');
999
+ const FLOW_LOG = logging.getLogger('promise.ControlFlow');
1000
+
1006
1001
 
1007
1002
  /**
1008
1003
  * Represents the eventual value of a completed operation. Each promise may be
@@ -1025,14 +1020,29 @@ class ManagedPromise {
1025
1020
  * functions, one for fulfilling the promise and another for rejecting it.
1026
1021
  * @param {ControlFlow=} opt_flow The control flow
1027
1022
  * this instance was created under. Defaults to the currently active flow.
1023
+ * @param {?=} opt_skipLog An internal parameter used to skip logging the
1024
+ * creation of this promise. This parameter has no effect unless it is
1025
+ * strictly equal to an internal symbol. In other words, this parameter
1026
+ * is always ignored for external code.
1028
1027
  */
1029
- constructor(resolver, opt_flow) {
1028
+ constructor(resolver, opt_flow, opt_skipLog) {
1030
1029
  if (!usePromiseManager()) {
1031
1030
  throw TypeError(
1032
1031
  'Unable to create a managed promise instance: the promise manager has'
1033
1032
  + ' been disabled by the SELENIUM_PROMISE_MANAGER environment'
1034
1033
  + ' variable: ' + process.env['SELENIUM_PROMISE_MANAGER']);
1034
+ } else if (opt_skipLog !== SKIP_LOG) {
1035
+ FLOW_LOG.warning(() => {
1036
+ let e =
1037
+ captureStackTrace(
1038
+ 'ManagedPromiseError',
1039
+ 'Creating a new managed Promise. This call will fail when the'
1040
+ + ' promise manager is disabled',
1041
+ ManagedPromise)
1042
+ return e.stack;
1043
+ });
1035
1044
  }
1045
+
1036
1046
  getUid(this);
1037
1047
 
1038
1048
  /** @private {!ControlFlow} */
@@ -1432,15 +1442,19 @@ class Deferred {
1432
1442
  /**
1433
1443
  * @param {ControlFlow=} opt_flow The control flow this instance was
1434
1444
  * created under. This should only be provided during unit tests.
1445
+ * @param {?=} opt_skipLog An internal parameter used to skip logging the
1446
+ * creation of this promise. This parameter has no effect unless it is
1447
+ * strictly equal to an internal symbol. In other words, this parameter
1448
+ * is always ignored for external code.
1435
1449
  */
1436
- constructor(opt_flow) {
1450
+ constructor(opt_flow, opt_skipLog) {
1437
1451
  var fulfill, reject;
1438
1452
 
1439
1453
  /** @type {!ManagedPromise<T>} */
1440
1454
  this.promise = new ManagedPromise(function(f, r) {
1441
1455
  fulfill = f;
1442
1456
  reject = r;
1443
- }, opt_flow);
1457
+ }, opt_flow, opt_skipLog);
1444
1458
 
1445
1459
  var self = this;
1446
1460
  var checkNotSelf = function(value) {
@@ -1563,9 +1577,17 @@ function defer() {
1563
1577
  function fulfilled(opt_value) {
1564
1578
  let ctor = usePromiseManager() ? ManagedPromise : NativePromise;
1565
1579
  if (opt_value instanceof ctor) {
1566
- return /** @type {!Thenable<T>} */(opt_value);
1580
+ return /** @type {!Thenable} */(opt_value);
1581
+ }
1582
+
1583
+ if (usePromiseManager()) {
1584
+ // We can skip logging warnings about creating a managed promise because
1585
+ // this function will automatically switch to use a native promise when
1586
+ // the promise manager is disabled.
1587
+ return new ManagedPromise(
1588
+ resolve => resolve(opt_value), undefined, SKIP_LOG);
1567
1589
  }
1568
- return ctor.resolve(opt_value);
1590
+ return NativePromise.resolve(opt_value);
1569
1591
  }
1570
1592
 
1571
1593
 
@@ -1582,7 +1604,11 @@ function fulfilled(opt_value) {
1582
1604
  */
1583
1605
  function rejected(opt_reason) {
1584
1606
  if (usePromiseManager()) {
1585
- return ManagedPromise.reject(opt_reason);
1607
+ // We can skip logging warnings about creating a managed promise because
1608
+ // this function will automatically switch to use a native promise when
1609
+ // the promise manager is disabled.
1610
+ return new ManagedPromise(
1611
+ (_, reject) => reject(opt_reason), undefined, SKIP_LOG);
1586
1612
  }
1587
1613
  return NativePromise.reject(opt_reason);
1588
1614
  }
@@ -2444,15 +2470,31 @@ class ControlFlow extends events.EventEmitter {
2444
2470
  }
2445
2471
 
2446
2472
  if (!this.hold_) {
2447
- var holdIntervalMs = 2147483647; // 2^31-1; max timer length for Node.js
2473
+ let holdIntervalMs = 2147483647; // 2^31-1; max timer length for Node.js
2448
2474
  this.hold_ = setInterval(function() {}, holdIntervalMs);
2449
2475
  }
2450
2476
 
2451
- var task = new Task(
2477
+ let task = new Task(
2452
2478
  this, fn, opt_description || '<anonymous>',
2453
- {name: 'Task', top: ControlFlow.prototype.execute});
2479
+ {name: 'Task', top: ControlFlow.prototype.execute},
2480
+ true);
2481
+
2482
+ let q = this.getActiveQueue_();
2483
+
2484
+ for (let i = q.tasks_.length; i > 0; i--) {
2485
+ let previousTask = q.tasks_[i - 1];
2486
+ if (previousTask.userTask_) {
2487
+ FLOW_LOG.warning(() => {
2488
+ return `Detected scheduling of an unchained task.
2489
+ When the promise manager is disabled, unchained tasks will not wait for
2490
+ previously scheduled tasks to finish before starting to execute.
2491
+ New task: ${task.promise.stack_.stack}
2492
+ Previous task: ${previousTask.promise.stack_.stack}`.split(/\n/).join('\n ');
2493
+ });
2494
+ break;
2495
+ }
2496
+ }
2454
2497
 
2455
- var q = this.getActiveQueue_();
2456
2498
  q.enqueue(task);
2457
2499
  this.emit(ControlFlow.EventType.SCHEDULE_TASK, task.description);
2458
2500
  return task.promise;
@@ -2460,7 +2502,7 @@ class ControlFlow extends events.EventEmitter {
2460
2502
 
2461
2503
  /** @override */
2462
2504
  promise(resolver) {
2463
- return new ManagedPromise(resolver, this);
2505
+ return new ManagedPromise(resolver, this, SKIP_LOG);
2464
2506
  }
2465
2507
 
2466
2508
  /** @override */
@@ -2729,9 +2771,11 @@ class Task extends Deferred {
2729
2771
  * @param {string} description A description of the task for debugging.
2730
2772
  * @param {{name: string, top: !Function}=} opt_stackOptions Options to use
2731
2773
  * when capturing the stacktrace for when this task was created.
2774
+ * @param {boolean=} opt_isUserTask Whether this task was explicitly scheduled
2775
+ * by the use of the promise manager.
2732
2776
  */
2733
- constructor(flow, fn, description, opt_stackOptions) {
2734
- super(flow);
2777
+ constructor(flow, fn, description, opt_stackOptions, opt_isUserTask) {
2778
+ super(flow, SKIP_LOG);
2735
2779
  getUid(this);
2736
2780
 
2737
2781
  /** @type {function(): (T|!ManagedPromise<T>)} */
@@ -2743,6 +2787,9 @@ class Task extends Deferred {
2743
2787
  /** @type {TaskQueue} */
2744
2788
  this.queue = null;
2745
2789
 
2790
+ /** @private @const {boolean} */
2791
+ this.userTask_ = !!opt_isUserTask;
2792
+
2746
2793
  /**
2747
2794
  * Whether this task is considered block. A blocked task may be registered
2748
2795
  * in a task queue, but will be dropped if it is still blocked when it
@@ -0,0 +1,12 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Clicking on a disabled element</title>
5
+ </head>
6
+ <body>
7
+ <h1>See below</h1>
8
+ <form action="POST">
9
+ <input name="disabled" disabled="disabled" />
10
+ </form>
11
+ </body>
12
+ </html>
@@ -0,0 +1,10 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Testing content editable</title>
5
+ </head>
6
+ <body>
7
+ <div id="editable" contenteditable="true">
8
+ Why <span>hello</span></div>
9
+ </body>
10
+ </html>