diff options
Diffstat (limited to 'node_modules/selenium-webdriver/test/lib')
14 files changed, 8983 insertions, 0 deletions
diff --git a/node_modules/selenium-webdriver/test/lib/by_test.js b/node_modules/selenium-webdriver/test/lib/by_test.js new file mode 100644 index 000000000..1669ec917 --- /dev/null +++ b/node_modules/selenium-webdriver/test/lib/by_test.js @@ -0,0 +1,139 @@ +// 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. + +'use strict'; + +var assert = require('assert'); +var by = require('../../lib/by'); + +describe('by', function() { + describe('By', function() { + describe('className', function() { + it('delegates to By.css', function() { + let locator = by.By.className('foo'); + assert.equal('css selector', locator.using); + assert.equal('.foo', locator.value); + }); + + it('escapes class name', function() { + let locator = by.By.className('foo#bar'); + assert.equal('css selector', locator.using); + assert.equal('.foo\\#bar', locator.value); + }); + + it('translates compound class names', function() { + let locator = by.By.className('a b'); + assert.equal('css selector', locator.using); + assert.equal('.a.b', locator.value); + + locator = by.By.className(' x y z-1 "g" '); + assert.equal('css selector', locator.using); + assert.equal('.x.y.z-1.\\"g\\"', locator.value); + }); + }); + + describe('id', function() { + it('delegates to By.css', function() { + let locator = by.By.id('foo'); + assert.equal('css selector', locator.using); + assert.equal('*[id="foo"]', locator.value); + }); + + it('escapes the ID', function() { + let locator = by.By.id('foo#bar'); + assert.equal('css selector', locator.using); + assert.equal('*[id="foo\\#bar"]', locator.value); + }); + }); + + describe('name', function() { + it('delegates to By.css', function() { + let locator = by.By.name('foo') + assert.equal('css selector', locator.using); + assert.equal('*[name="foo"]', locator.value); + }); + + it('escapes the name', function() { + let locator = by.By.name('foo"bar"') + assert.equal('css selector', locator.using); + assert.equal('*[name="foo\\"bar\\""]', locator.value); + }); + + it('escapes the name when it starts with a number', function() { + let locator = by.By.name('123foo"bar"') + assert.equal('css selector', locator.using); + assert.equal('*[name="\\31 23foo\\"bar\\""]', locator.value); + }); + + it('escapes the name when it starts with a negative number', function() { + let locator = by.By.name('-123foo"bar"') + assert.equal('css selector', locator.using); + assert.equal('*[name="-\\31 23foo\\"bar\\""]', locator.value); + }); + }); + }); + + describe('checkedLocator', function() { + it('accepts class name', function() { + let locator = by.checkedLocator({className: 'foo'}); + assert.equal('css selector', locator.using); + assert.equal('.foo', locator.value); + }); + + it('accepts css', function() { + let locator = by.checkedLocator({css: 'a > b'}); + assert.equal('css selector', locator.using); + assert.equal('a > b', locator.value); + }); + + it('accepts id', function() { + let locator = by.checkedLocator({id: 'foobar'}); + assert.equal('css selector', locator.using); + assert.equal('*[id="foobar"]', locator.value); + }); + + it('accepts linkText', function() { + let locator = by.checkedLocator({linkText: 'hello'}); + assert.equal('link text', locator.using); + assert.equal('hello', locator.value); + }); + + it('accepts name', function() { + let locator = by.checkedLocator({name: 'foobar'}); + assert.equal('css selector', locator.using); + assert.equal('*[name="foobar"]', locator.value); + }); + + it('accepts partialLinkText', function() { + let locator = by.checkedLocator({partialLinkText: 'hello'}); + assert.equal('partial link text', locator.using); + assert.equal('hello', locator.value); + }); + + it('accepts tagName', function() { + let locator = by.checkedLocator({tagName: 'div'}); + assert.equal('css selector', locator.using); + assert.equal('div', locator.value); + }); + + it('accepts xpath', function() { + let locator = by.checkedLocator({xpath: '//div[1]'}); + assert.equal('xpath', locator.using); + assert.equal('//div[1]', locator.value); + }); + }); +}); diff --git a/node_modules/selenium-webdriver/test/lib/capabilities_test.js b/node_modules/selenium-webdriver/test/lib/capabilities_test.js new file mode 100644 index 000000000..0d0c67c9a --- /dev/null +++ b/node_modules/selenium-webdriver/test/lib/capabilities_test.js @@ -0,0 +1,111 @@ +// 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. + +'use strict'; + +const Capabilities = require('../../lib/capabilities').Capabilities; +const Symbols = require('../../lib/symbols'); + +const assert = require('assert'); + +describe('Capabilities', function() { + it('can set and unset a capability', function() { + let caps = new Capabilities(); + assert.equal(undefined, caps.get('foo')); + assert.ok(!caps.has('foo')); + + caps.set('foo', 'bar'); + assert.equal('bar', caps.get('foo')); + assert.ok(caps.has('foo')); + + caps.set('foo', null); + assert.equal(null, caps.get('foo')); + assert.ok(caps.has('foo')); + }); + + it('requires string capability keys', function() { + let caps = new Capabilities(); + assert.throws(() => caps.set({}, 'hi')); + }); + + it('can merge capabilities', function() { + let caps1 = new Capabilities() + .set('foo', 'bar') + .set('color', 'red'); + + let caps2 = new Capabilities() + .set('color', 'green'); + + assert.equal('bar', caps1.get('foo')); + assert.equal('red', caps1.get('color')); + assert.equal('green', caps2.get('color')); + assert.equal(undefined, caps2.get('foo')); + + caps2.merge(caps1); + assert.equal('bar', caps1.get('foo')); + assert.equal('red', caps1.get('color')); + assert.equal('red', caps2.get('color')); + assert.equal('bar', caps2.get('foo')); + }); + + it('can be initialized from a hash object', function() { + let caps = new Capabilities({'one': 123, 'abc': 'def'}); + assert.equal(123, caps.get('one')); + assert.equal('def', caps.get('abc')); + }); + + it('can be initialized from a map', function() { + let m = new Map([['one', 123], ['abc', 'def']]); + + let caps = new Capabilities(m); + assert.equal(123, caps.get('one')); + assert.equal('def', caps.get('abc')); + }); + + describe('serialize', function() { + it('works for simple capabilities', function() { + let m = new Map([['one', 123], ['abc', 'def']]); + let caps = new Capabilities(m); + assert.deepEqual({one: 123, abc: 'def'}, caps[Symbols.serialize]()); + }); + + it('does not omit capabilities set to a false-like value', function() { + let caps = new Capabilities; + caps.set('bool', false); + caps.set('number', 0); + caps.set('string', ''); + + assert.deepEqual( + {bool: false, number: 0, string: ''}, + caps[Symbols.serialize]()); + }); + + it('omits capabilities with a null value', function() { + let caps = new Capabilities; + caps.set('foo', null); + caps.set('bar', 123); + assert.deepEqual({bar: 123}, caps[Symbols.serialize]()); + }); + + it('omits capabilities with an undefined value', function() { + let caps = new Capabilities; + caps.set('foo', undefined); + caps.set('bar', 123); + assert.deepEqual({bar: 123}, caps[Symbols.serialize]()); + }); + }); +}); diff --git a/node_modules/selenium-webdriver/test/lib/error_test.js b/node_modules/selenium-webdriver/test/lib/error_test.js new file mode 100644 index 000000000..a9fc87193 --- /dev/null +++ b/node_modules/selenium-webdriver/test/lib/error_test.js @@ -0,0 +1,286 @@ +// 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. + +'use strict'; + +describe('error', function() { + let assert = require('assert'); + let error = require('../../lib/error'); + + describe('checkResponse', function() { + it('defaults to WebDriverError if type is unrecognized', function() { + assert.throws( + () => error.checkResponse({error: 'foo', message: 'hi there'}), + (e) => { + assert.equal(e.constructor, error.WebDriverError); + return true; + }); + }); + + it('does not throw if error property is not a string', function() { + let resp = {error: 123, message: 'abc123'}; + let out = error.checkResponse(resp); + assert.strictEqual(out, resp); + }); + + test('unknown error', error.WebDriverError); + test('element not selectable', error.ElementNotSelectableError); + test('element not visible', error.ElementNotVisibleError); + test('invalid argument', error.InvalidArgumentError); + test('invalid cookie domain', error.InvalidCookieDomainError); + test('invalid element coordinates', error.InvalidElementCoordinatesError); + test('invalid element state', error.InvalidElementStateError); + test('invalid selector', error.InvalidSelectorError); + test('invalid session id', error.NoSuchSessionError); + test('javascript error', error.JavascriptError); + test('move target out of bounds', error.MoveTargetOutOfBoundsError); + test('no such alert', error.NoSuchAlertError); + test('no such element', error.NoSuchElementError); + test('no such frame', error.NoSuchFrameError); + test('no such window', error.NoSuchWindowError); + test('script timeout', error.ScriptTimeoutError); + test('session not created', error.SessionNotCreatedError); + test('stale element reference', error.StaleElementReferenceError); + test('timeout', error.TimeoutError); + test('unable to set cookie', error.UnableToSetCookieError); + test('unable to capture screen', error.UnableToCaptureScreenError); + test('unexpected alert open', error.UnexpectedAlertOpenError); + test('unknown command', error.UnknownCommandError); + test('unknown method', error.UnknownMethodError); + test('unsupported operation', error.UnsupportedOperationError); + + function test(status, expectedType) { + it(`"${status}" => ${expectedType.name}`, function() { + assert.throws( + () => error.checkResponse({error: status, message: 'oops'}), + (e) => { + assert.equal(expectedType, e.constructor); + assert.equal(e.message, 'oops'); + return true; + }); + }); + } + }); + + describe('encodeError', function() { + describe('defaults to an unknown error', function() { + it('for a generic error value', function() { + runTest('hi', 'unknown error', 'hi'); + runTest(1, 'unknown error', '1'); + runTest({}, 'unknown error', '[object Object]'); + }); + + it('for a generic Error object', function() { + runTest(Error('oops'), 'unknown error', 'oops'); + runTest(TypeError('bad value'), 'unknown error', 'bad value'); + }); + }); + + test(error.WebDriverError, 'unknown error'); + test(error.ElementNotSelectableError, 'element not selectable'); + test(error.ElementNotVisibleError, 'element not visible'); + test(error.InvalidArgumentError, 'invalid argument'); + test(error.InvalidCookieDomainError, 'invalid cookie domain'); + test(error.InvalidElementStateError, 'invalid element state'); + test(error.InvalidSelectorError, 'invalid selector'); + test(error.NoSuchSessionError, 'invalid session id'); + test(error.JavascriptError, 'javascript error'); + test(error.MoveTargetOutOfBoundsError, 'move target out of bounds'); + test(error.NoSuchAlertError, 'no such alert'); + test(error.NoSuchElementError, 'no such element'); + test(error.NoSuchFrameError, 'no such frame'); + test(error.NoSuchWindowError, 'no such window'); + test(error.ScriptTimeoutError, 'script timeout'); + test(error.SessionNotCreatedError, 'session not created'); + test(error.StaleElementReferenceError, 'stale element reference'); + test(error.TimeoutError, 'timeout'); + test(error.UnableToSetCookieError, 'unable to set cookie'); + test(error.UnableToCaptureScreenError, 'unable to capture screen'); + test(error.UnexpectedAlertOpenError, 'unexpected alert open'); + test(error.UnknownCommandError, 'unknown command'); + test(error.UnknownMethodError, 'unknown method'); + test(error.UnsupportedOperationError, 'unsupported operation'); + + function test(ctor, code) { + it(`${ctor.name} => "${code}"`, () => { + runTest(new ctor('oops'), code, 'oops'); + }); + } + + function runTest(err, code, message) { + let obj = error.encodeError(err); + assert.strictEqual(obj['error'], code); + assert.strictEqual(obj['message'], message); + } + }); + + describe('throwDecodedError', function() { + it('defaults to WebDriverError if type is unrecognized', function() { + assert.throws( + () => error.throwDecodedError({error: 'foo', message: 'hi there'}), + (e) => { + assert.equal(e.constructor, error.WebDriverError); + return true; + }); + }); + + it('throws generic error if encoded data is not valid', function() { + assert.throws( + () => error.throwDecodedError({error: 123, message: 'abc123'}), + (e) => { + assert.strictEqual(e.constructor, error.WebDriverError); + return true; + }); + + assert.throws( + () => error.throwDecodedError('null'), + (e) => { + assert.strictEqual(e.constructor, error.WebDriverError); + return true; + }); + + assert.throws( + () => error.throwDecodedError(''), + (e) => { + assert.strictEqual(e.constructor, error.WebDriverError); + return true; + }); + }); + + test('unknown error', error.WebDriverError); + test('element not selectable', error.ElementNotSelectableError); + test('element not visible', error.ElementNotVisibleError); + test('invalid argument', error.InvalidArgumentError); + test('invalid cookie domain', error.InvalidCookieDomainError); + test('invalid element coordinates', error.InvalidElementCoordinatesError); + test('invalid element state', error.InvalidElementStateError); + test('invalid selector', error.InvalidSelectorError); + test('invalid session id', error.NoSuchSessionError); + test('javascript error', error.JavascriptError); + test('move target out of bounds', error.MoveTargetOutOfBoundsError); + test('no such alert', error.NoSuchAlertError); + test('no such element', error.NoSuchElementError); + test('no such frame', error.NoSuchFrameError); + test('no such window', error.NoSuchWindowError); + test('script timeout', error.ScriptTimeoutError); + test('session not created', error.SessionNotCreatedError); + test('stale element reference', error.StaleElementReferenceError); + test('timeout', error.TimeoutError); + test('unable to set cookie', error.UnableToSetCookieError); + test('unable to capture screen', error.UnableToCaptureScreenError); + test('unexpected alert open', error.UnexpectedAlertOpenError); + test('unknown command', error.UnknownCommandError); + test('unknown method', error.UnknownMethodError); + test('unsupported operation', error.UnsupportedOperationError); + + function test(status, expectedType) { + it(`"${status}" => ${expectedType.name}`, function() { + assert.throws( + () => error.throwDecodedError({error: status, message: 'oops'}), + (e) => { + assert.strictEqual(e.constructor, expectedType); + assert.strictEqual(e.message, 'oops'); + return true; + }); + }); + } + }); + + describe('checkLegacyResponse', function() { + it('does not throw for success', function() { + let resp = {status: error.ErrorCode.SUCCESS}; + assert.strictEqual(resp, error.checkLegacyResponse(resp)); + }); + + test('NO_SUCH_ELEMENT', error.NoSuchElementError); + test('NO_SUCH_FRAME', error.NoSuchFrameError); + test('UNKNOWN_COMMAND', error.UnsupportedOperationError); + test('UNSUPPORTED_OPERATION', error.UnsupportedOperationError); + test('STALE_ELEMENT_REFERENCE', error.StaleElementReferenceError); + test('ELEMENT_NOT_VISIBLE', error.ElementNotVisibleError); + test('INVALID_ELEMENT_STATE', error.InvalidElementStateError); + test('UNKNOWN_ERROR', error.WebDriverError); + test('ELEMENT_NOT_SELECTABLE', error.ElementNotSelectableError); + test('JAVASCRIPT_ERROR', error.JavascriptError); + test('XPATH_LOOKUP_ERROR', error.InvalidSelectorError); + test('TIMEOUT', error.TimeoutError); + test('NO_SUCH_WINDOW', error.NoSuchWindowError); + test('INVALID_COOKIE_DOMAIN', error.InvalidCookieDomainError); + test('UNABLE_TO_SET_COOKIE', error.UnableToSetCookieError); + test('UNEXPECTED_ALERT_OPEN', error.UnexpectedAlertOpenError); + test('NO_SUCH_ALERT', error.NoSuchAlertError); + test('SCRIPT_TIMEOUT', error.ScriptTimeoutError); + test('INVALID_ELEMENT_COORDINATES', error.InvalidElementCoordinatesError); + test('INVALID_SELECTOR_ERROR', error.InvalidSelectorError); + test('SESSION_NOT_CREATED', error.SessionNotCreatedError); + test('MOVE_TARGET_OUT_OF_BOUNDS', error.MoveTargetOutOfBoundsError); + test('INVALID_XPATH_SELECTOR', error.InvalidSelectorError); + test('INVALID_XPATH_SELECTOR_RETURN_TYPE', error.InvalidSelectorError); + test('METHOD_NOT_ALLOWED', error.UnsupportedOperationError); + + describe('UnexpectedAlertOpenError', function() { + it('includes alert text from the response object', function() { + let response = { + status: error.ErrorCode.UNEXPECTED_ALERT_OPEN, + value: { + message: 'hi', + alert: {text: 'alert text here'} + } + }; + assert.throws( + () => error.checkLegacyResponse(response), + (e) => { + assert.equal(error.UnexpectedAlertOpenError, e.constructor); + assert.equal(e.message, 'hi'); + assert.equal(e.getAlertText(), 'alert text here'); + return true; + }); + }); + + it('uses an empty string if alert text omitted', function() { + let response = { + status: error.ErrorCode.UNEXPECTED_ALERT_OPEN, + value: { + message: 'hi' + } + }; + assert.throws( + () => error.checkLegacyResponse(response), + (e) => { + assert.equal(error.UnexpectedAlertOpenError, e.constructor); + assert.equal(e.message, 'hi'); + assert.equal(e.getAlertText(), ''); + return true; + }); + }); + }); + + function test(codeKey, expectedType) { + it(`${codeKey} => ${expectedType.name}`, function() { + let code = error.ErrorCode[codeKey]; + let resp = {status: code, value: {message: 'hi'}}; + assert.throws( + () => error.checkLegacyResponse(resp), + (e) => { + assert.equal(expectedType, e.constructor); + assert.equal(e.message, 'hi'); + return true; + }); + }); + } + }); +}); diff --git a/node_modules/selenium-webdriver/test/lib/events_test.js b/node_modules/selenium-webdriver/test/lib/events_test.js new file mode 100644 index 000000000..a02da9747 --- /dev/null +++ b/node_modules/selenium-webdriver/test/lib/events_test.js @@ -0,0 +1,177 @@ +// 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. + +'use strict'; + +const EventEmitter = require('../../lib/events').EventEmitter; + +const assert = require('assert'); +const sinon = require('sinon'); + +describe('EventEmitter', function() { + describe('#emit()', function() { + it('can emit events when nothing is registered', function() { + let emitter = new EventEmitter; + emitter.emit('foo'); + // Ok if no errors are thrown. + }); + + it('can pass args to listeners on emit', function() { + let emitter = new EventEmitter; + let now = Date.now(); + + let messages = []; + emitter.on('foo', (arg) => messages.push(arg)); + + emitter.emit('foo', now); + assert.deepEqual([now], messages); + + emitter.emit('foo', now + 15); + assert.deepEqual([now, now + 15], messages); + }); + }); + + describe('#addListener()', function() { + it('can add multiple listeners for one event', function() { + let emitter = new EventEmitter; + let count = 0; + emitter.addListener('foo', () => count++); + emitter.addListener('foo', () => count++); + emitter.addListener('foo', () => count++); + emitter.emit('foo'); + assert.equal(3, count); + }); + + it('only registers each listener function once', function() { + let emitter = new EventEmitter; + let count = 0; + let onFoo = () => count++; + emitter.addListener('foo', onFoo); + emitter.addListener('foo', onFoo); + emitter.addListener('foo', onFoo); + + emitter.emit('foo'); + assert.equal(1, count); + + emitter.emit('foo'); + assert.equal(2, count); + }); + + it('allows users to specify a custom scope', function() { + let obj = { + count: 0, + inc: function() { + this.count++; + } + }; + let emitter = new EventEmitter; + emitter.addListener('foo', obj.inc, obj); + + emitter.emit('foo'); + assert.equal(1, obj.count); + + emitter.emit('foo'); + assert.equal(2, obj.count); + }); + }); + + describe('#once()', function() { + it('only calls registered callback once', function() { + let emitter = new EventEmitter; + let count = 0; + emitter.once('foo', () => count++); + emitter.once('foo', () => count++); + emitter.once('foo', () => count++); + + emitter.emit('foo'); + assert.equal(3, count); + + emitter.emit('foo'); + assert.equal(3, count); + + emitter.emit('foo'); + assert.equal(3, count); + }); + }); + + describe('#removeListeners()', function() { + it('only removes the given listener function', function() { + let emitter = new EventEmitter; + let count = 0; + emitter.addListener('foo', () => count++); + emitter.addListener('foo', () => count++); + + let toRemove = () => count++; + emitter.addListener('foo', toRemove); + + emitter.emit('foo'); + assert.equal(3, count); + + emitter.removeListener('foo', toRemove); + emitter.emit('foo'); + assert.equal(5, count); + }); + }); + + describe('#removeAllListeners()', function() { + it('only removes listeners for type if specified', function() { + let emitter = new EventEmitter; + let count = 0; + emitter.addListener('foo', () => count++); + emitter.addListener('foo', () => count++); + emitter.addListener('foo', () => count++); + emitter.addListener('bar', () => count++); + + emitter.emit('foo'); + assert.equal(3, count); + + emitter.removeAllListeners('foo'); + + emitter.emit('foo'); + assert.equal(3, count); + + emitter.emit('bar'); + assert.equal(4, count); + }); + + it('removes absolutely all listeners if no type specified', function() { + let emitter = new EventEmitter; + let count = 0; + emitter.addListener('foo', () => count++); + emitter.addListener('bar', () => count++); + emitter.addListener('baz', () => count++); + emitter.addListener('baz', () => count++); + + emitter.emit('foo'); + assert.equal(1, count); + + emitter.emit('baz'); + assert.equal(3, count); + + emitter.removeAllListeners(); + + emitter.emit('foo'); + assert.equal(3, count); + + emitter.emit('bar'); + assert.equal(3, count); + + emitter.emit('baz'); + assert.equal(3, count); + }); + }); +}); diff --git a/node_modules/selenium-webdriver/test/lib/http_test.js b/node_modules/selenium-webdriver/test/lib/http_test.js new file mode 100644 index 000000000..343a04800 --- /dev/null +++ b/node_modules/selenium-webdriver/test/lib/http_test.js @@ -0,0 +1,655 @@ +// 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. + +'use strict'; + +var assert = require('assert'), + sinon = require('sinon'); + +var Capabilities = require('../../lib/capabilities').Capabilities, + Command = require('../../lib/command').Command, + CommandName = require('../../lib/command').Name, + error = require('../../lib/error'), + http = require('../../lib/http'), + Session = require('../../lib/session').Session, + promise = require('../../lib/promise'), + WebElement = require('../../lib/webdriver').WebElement; + +describe('http', function() { + describe('buildPath', function() { + it('properly replaces path segments with command parameters', function() { + var parameters = {'sessionId':'foo', 'url':'http://www.google.com'}; + var finalPath = http.buildPath('/session/:sessionId/url', parameters); + assert.equal(finalPath, '/session/foo/url'); + assert.deepEqual(parameters, {'url':'http://www.google.com'}); + }); + + it('handles web element references', function() { + var parameters = {'sessionId':'foo', 'id': WebElement.buildId('bar')}; + + var finalPath = http.buildPath( + '/session/:sessionId/element/:id/click', parameters); + assert.equal(finalPath, '/session/foo/element/bar/click'); + assert.deepEqual(parameters, {}); + }); + + it('throws if missing a parameter', function() { + assert.throws( + () => http.buildPath('/session/:sessionId', {}), + function(err) { + return err instanceof error.InvalidArgumentError + && 'Missing required parameter: sessionId' === err.message; + }); + + assert.throws( + () => http.buildPath( + '/session/:sessionId/element/:id', {'sessionId': 'foo'}), + function(err) { + return err instanceof error.InvalidArgumentError + && 'Missing required parameter: id' === err.message; + }); + }); + + it('does not match on segments that do not start with a colon', function() { + assert.equal( + http.buildPath('/session/foo:bar/baz', {}), + '/session/foo:bar/baz'); + }); + }); + + describe('Executor', function() { + let executor; + let client; + let send; + + beforeEach(function setUp() { + client = new http.Client; + send = sinon.stub(client, 'send'); + executor = new http.Executor(client); + }); + + describe('command routing', function() { + it('rejects unrecognized commands', function() { + assert.throws( + () => executor.execute(new Command('fake-name')), + function (err) { + return err instanceof error.UnknownCommandError + && 'Unrecognized command: fake-name' === err.message; + }); + }); + + it('rejects promise if client fails to send request', function() { + let error = new Error('boom'); + send.returns(Promise.reject(error)); + return assertFailsToSend(new Command(CommandName.NEW_SESSION)) + .then(function(e) { + assert.strictEqual(error, e); + assertSent( + 'POST', '/session', {}, + [['Accept', 'application/json; charset=utf-8']]); + }); + }); + + it('can execute commands with no URL parameters', function() { + var resp = JSON.stringify({sessionId: 'abc123'}); + send.returns(Promise.resolve(new http.Response(200, {}, resp))); + + let command = new Command(CommandName.NEW_SESSION); + return assertSendsSuccessfully(command).then(function(response) { + assertSent( + 'POST', '/session', {}, + [['Accept', 'application/json; charset=utf-8']]); + }); + }); + + it('rejects commands missing URL parameters', function() { + let command = + new Command(CommandName.FIND_CHILD_ELEMENT). + setParameter('sessionId', 's123'). + // Let this be missing: setParameter('id', {'ELEMENT': 'e456'}). + setParameter('using', 'id'). + setParameter('value', 'foo'); + + assert.throws( + () => executor.execute(command), + function(err) { + return err instanceof error.InvalidArgumentError + && 'Missing required parameter: id' === err.message; + }); + assert.ok(!send.called); + }); + + it('replaces URL parameters with command parameters', function() { + var command = new Command(CommandName.GET). + setParameter('sessionId', 's123'). + setParameter('url', 'http://www.google.com'); + + send.returns(Promise.resolve(new http.Response(200, {}, ''))); + + return assertSendsSuccessfully(command).then(function(response) { + assertSent( + 'POST', '/session/s123/url', {'url': 'http://www.google.com'}, + [['Accept', 'application/json; charset=utf-8']]); + }); + }); + + describe('uses correct URL', function() { + beforeEach(() => executor = new http.Executor(client)); + + describe('in legacy mode', function() { + test(CommandName.GET_WINDOW_SIZE, {sessionId:'s123'}, false, + 'GET', '/session/s123/window/current/size'); + + test(CommandName.SET_WINDOW_SIZE, + {sessionId:'s123', width: 1, height: 1}, false, + 'POST', '/session/s123/window/current/size', + {width: 1, height: 1}); + + test(CommandName.MAXIMIZE_WINDOW, {sessionId:'s123'}, false, + 'POST', '/session/s123/window/current/maximize'); + + // This is consistent b/w legacy and W3C, just making sure. + test(CommandName.GET, + {sessionId:'s123', url: 'http://www.example.com'}, false, + 'POST', '/session/s123/url', {url: 'http://www.example.com'}); + }); + + describe('in W3C mode', function() { + test(CommandName.GET_WINDOW_SIZE, + {sessionId:'s123'}, true, + 'GET', '/session/s123/window/size'); + + test(CommandName.SET_WINDOW_SIZE, + {sessionId:'s123', width: 1, height: 1}, true, + 'POST', '/session/s123/window/size', {width: 1, height: 1}); + + test(CommandName.MAXIMIZE_WINDOW, {sessionId:'s123'}, true, + 'POST', '/session/s123/window/maximize'); + + // This is consistent b/w legacy and W3C, just making sure. + test(CommandName.GET, + {sessionId:'s123', url: 'http://www.example.com'}, true, + 'POST', '/session/s123/url', {url: 'http://www.example.com'}); + }); + + function test(command, parameters, w3c, + expectedMethod, expectedUrl, opt_expectedParams) { + it(`command=${command}`, function() { + var resp = JSON.stringify({sessionId: 'abc123'}); + send.returns(Promise.resolve(new http.Response(200, {}, resp))); + + let cmd = new Command(command).setParameters(parameters); + executor.w3c = w3c; + return executor.execute(cmd).then(function() { + assertSent( + expectedMethod, expectedUrl, opt_expectedParams || {}, + [['Accept', 'application/json; charset=utf-8']]); + }); + }); + } + }); + }); + + describe('response parsing', function() { + it('extracts value from JSON response', function() { + var responseObj = { + 'status': error.ErrorCode.SUCCESS, + 'value': 'http://www.google.com' + }; + + var command = new Command(CommandName.GET_CURRENT_URL) + .setParameter('sessionId', 's123'); + + send.returns(Promise.resolve( + new http.Response(200, {}, JSON.stringify(responseObj)))); + + return executor.execute(command).then(function(response) { + assertSent('GET', '/session/s123/url', {}, + [['Accept', 'application/json; charset=utf-8']]); + assert.strictEqual(response, 'http://www.google.com'); + }); + }); + + describe('extracts Session from NEW_SESSION response', function() { + beforeEach(() => executor = new http.Executor(client)); + + const command = new Command(CommandName.NEW_SESSION); + + describe('fails if server returns invalid response', function() { + describe('(empty response)', function() { + test(true); + test(false); + + function test(w3c) { + it('w3c === ' + w3c, function() { + send.returns(Promise.resolve(new http.Response(200, {}, ''))); + executor.w3c = w3c; + return executor.execute(command).then( + () => assert.fail('expected to fail'), + (e) => { + if (!e.message.startsWith('Unable to parse')) { + throw e; + } + }); + }); + } + }); + + describe('(no session ID)', function() { + test(true); + test(false); + + function test(w3c) { + it('w3c === ' + w3c, function() { + let resp = {value:{name: 'Bob'}}; + send.returns(Promise.resolve( + new http.Response(200, {}, JSON.stringify(resp)))); + executor.w3c = w3c; + return executor.execute(command).then( + () => assert.fail('expected to fail'), + (e) => { + if (!e.message.startsWith('Unable to parse')) { + throw e; + } + }); + }); + } + }); + }); + + it('handles legacy response', function() { + var rawResponse = {sessionId: 's123', status: 0, value: {name: 'Bob'}}; + + send.returns(Promise.resolve( + new http.Response(200, {}, JSON.stringify(rawResponse)))); + + assert.ok(!executor.w3c); + return executor.execute(command).then(function(response) { + assert.ok(response instanceof Session); + assert.equal(response.getId(), 's123'); + + let caps = response.getCapabilities(); + assert.ok(caps instanceof Capabilities); + assert.equal(caps.get('name'), 'Bob'); + + assert.ok(!executor.w3c); + }); + }); + + it('auto-upgrades on W3C response', function() { + var rawResponse = {sessionId: 's123', value: {name: 'Bob'}}; + + send.returns(Promise.resolve( + new http.Response(200, {}, JSON.stringify(rawResponse)))); + + assert.ok(!executor.w3c); + return executor.execute(command).then(function(response) { + assert.ok(response instanceof Session); + assert.equal(response.getId(), 's123'); + + let caps = response.getCapabilities(); + assert.ok(caps instanceof Capabilities); + assert.equal(caps.get('name'), 'Bob'); + + assert.ok(executor.w3c); + }); + }); + + it('if w3c, does not downgrade on legacy response', function() { + var rawResponse = {sessionId: 's123', status: 0, value: null}; + + send.returns(Promise.resolve( + new http.Response(200, {}, JSON.stringify(rawResponse)))); + + executor.w3c = true; + return executor.execute(command).then(function(response) { + assert.ok(response instanceof Session); + assert.equal(response.getId(), 's123'); + assert.equal(response.getCapabilities().size, 0); + assert.ok(executor.w3c, 'should never downgrade'); + }); + }); + }); + + describe('extracts Session from DESCRIBE_SESSION response', function() { + let command; + + beforeEach(function() { + executor = new http.Executor(client); + command = new Command(CommandName.DESCRIBE_SESSION) + .setParameter('sessionId', 'foo'); + }); + + describe('fails if server returns invalid response', function() { + describe('(empty response)', function() { + test(true); + test(false); + + function test(w3c) { + it('w3c === ' + w3c, function() { + send.returns(Promise.resolve(new http.Response(200, {}, ''))); + executor.w3c = w3c; + return executor.execute(command).then( + () => assert.fail('expected to fail'), + (e) => { + if (!e.message.startsWith('Unable to parse')) { + throw e; + } + }); + }); + } + }); + + describe('(no session ID)', function() { + test(true); + test(false); + + function test(w3c) { + it('w3c === ' + w3c, function() { + let resp = {value:{name: 'Bob'}}; + send.returns(Promise.resolve( + new http.Response(200, {}, JSON.stringify(resp)))); + executor.w3c = w3c; + return executor.execute(command).then( + () => assert.fail('expected to fail'), + (e) => { + if (!e.message.startsWith('Unable to parse')) { + throw e; + } + }); + }); + } + }); + }); + + it('handles legacy response', function() { + var rawResponse = {sessionId: 's123', status: 0, value: {name: 'Bob'}}; + + send.returns(Promise.resolve( + new http.Response(200, {}, JSON.stringify(rawResponse)))); + + assert.ok(!executor.w3c); + return executor.execute(command).then(function(response) { + assert.ok(response instanceof Session); + assert.equal(response.getId(), 's123'); + + let caps = response.getCapabilities(); + assert.ok(caps instanceof Capabilities); + assert.equal(caps.get('name'), 'Bob'); + + assert.ok(!executor.w3c); + }); + }); + + it('does not auto-upgrade on W3C response', function() { + var rawResponse = {sessionId: 's123', value: {name: 'Bob'}}; + + send.returns(Promise.resolve( + new http.Response(200, {}, JSON.stringify(rawResponse)))); + + assert.ok(!executor.w3c); + return executor.execute(command).then(function(response) { + assert.ok(response instanceof Session); + assert.equal(response.getId(), 's123'); + + let caps = response.getCapabilities(); + assert.ok(caps instanceof Capabilities); + assert.equal(caps.get('name'), 'Bob'); + + assert.ok(!executor.w3c); + }); + }); + + it('if w3c, does not downgrade on legacy response', function() { + var rawResponse = {sessionId: 's123', status: 0, value: null}; + + send.returns(Promise.resolve( + new http.Response(200, {}, JSON.stringify(rawResponse)))); + + executor.w3c = true; + return executor.execute(command).then(function(response) { + assert.ok(response instanceof Session); + assert.equal(response.getId(), 's123'); + assert.equal(response.getCapabilities().size, 0); + assert.ok(executor.w3c, 'should never downgrade'); + }); + }); + }); + + it('handles JSON null', function() { + var command = new Command(CommandName.GET_CURRENT_URL) + .setParameter('sessionId', 's123'); + + send.returns(Promise.resolve(new http.Response(200, {}, 'null'))); + + return executor.execute(command).then(function(response) { + assertSent('GET', '/session/s123/url', {}, + [['Accept', 'application/json; charset=utf-8']]); + assert.strictEqual(response, null); + }); + }); + + describe('falsy values', function() { + test(0); + test(false); + test(''); + + function test(value) { + it(`value=${value}`, function() { + var command = new Command(CommandName.GET_CURRENT_URL) + .setParameter('sessionId', 's123'); + + send.returns(Promise.resolve( + new http.Response(200, {}, + JSON.stringify({status: 0, value: value})))); + + return executor.execute(command).then(function(response) { + assertSent('GET', '/session/s123/url', {}, + [['Accept', 'application/json; charset=utf-8']]); + assert.strictEqual(response, value); + }); + }); + } + }); + + it('handles non-object JSON', function() { + var command = new Command(CommandName.GET_CURRENT_URL) + .setParameter('sessionId', 's123'); + + send.returns(Promise.resolve(new http.Response(200, {}, '123'))); + + return executor.execute(command).then(function(response) { + assertSent('GET', '/session/s123/url', {}, + [['Accept', 'application/json; charset=utf-8']]); + assert.strictEqual(response, 123); + }); + }); + + it('returns body text when 2xx but not JSON', function() { + var command = new Command(CommandName.GET_CURRENT_URL) + .setParameter('sessionId', 's123'); + + send.returns(Promise.resolve( + new http.Response(200, {}, 'hello, world\r\ngoodbye, world!'))); + + return executor.execute(command).then(function(response) { + assertSent('GET', '/session/s123/url', {}, + [['Accept', 'application/json; charset=utf-8']]); + assert.strictEqual(response, 'hello, world\ngoodbye, world!'); + }); + }); + + it('returns body text when 2xx but invalid JSON', function() { + var command = new Command(CommandName.GET_CURRENT_URL) + .setParameter('sessionId', 's123'); + + send.returns(Promise.resolve( + new http.Response(200, {}, '['))); + + return executor.execute(command).then(function(response) { + assertSent('GET', '/session/s123/url', {}, + [['Accept', 'application/json; charset=utf-8']]); + assert.strictEqual(response, '['); + }); + }); + + it('returns null if no body text and 2xx', function() { + var command = new Command(CommandName.GET_CURRENT_URL) + .setParameter('sessionId', 's123'); + + send.returns(Promise.resolve(new http.Response(200, {}, ''))); + + return executor.execute(command).then(function(response) { + assertSent('GET', '/session/s123/url', {}, + [['Accept', 'application/json; charset=utf-8']]); + assert.strictEqual(response, null); + }); + }); + + it('returns normalized body text when 2xx but not JSON', function() { + var command = new Command(CommandName.GET_CURRENT_URL) + .setParameter('sessionId', 's123'); + + send.returns(Promise.resolve(new http.Response(200, {}, '\r\n\n\n\r\n'))); + + return executor.execute(command).then(function(response) { + assertSent('GET', '/session/s123/url', {}, + [['Accept', 'application/json; charset=utf-8']]); + assert.strictEqual(response, '\n\n\n\n'); + }); + }); + + it('throws UnsupportedOperationError for 404 and body not JSON', + function() { + var command = new Command(CommandName.GET_CURRENT_URL) + .setParameter('sessionId', 's123'); + + send.returns(Promise.resolve( + new http.Response(404, {}, 'hello, world\r\ngoodbye, world!'))); + + return executor.execute(command) + .then( + () => assert.fail('should have failed'), + checkError( + error.UnsupportedOperationError, + 'hello, world\ngoodbye, world!')); + }); + + it('throws WebDriverError for generic 4xx when body not JSON', + function() { + var command = new Command(CommandName.GET_CURRENT_URL) + .setParameter('sessionId', 's123'); + + send.returns(Promise.resolve( + new http.Response(500, {}, 'hello, world\r\ngoodbye, world!'))); + + return executor.execute(command) + .then( + () => assert.fail('should have failed'), + checkError( + error.WebDriverError, + 'hello, world\ngoodbye, world!')) + .then(function() { + assertSent('GET', '/session/s123/url', {}, + [['Accept', 'application/json; charset=utf-8']]); + }); + }); + }); + + it('canDefineNewCommands', function() { + executor.defineCommand('greet', 'GET', '/person/:name'); + + var command = new Command('greet'). + setParameter('name', 'Bob'); + + send.returns(Promise.resolve(new http.Response(200, {}, ''))); + + return assertSendsSuccessfully(command).then(function(response) { + assertSent('GET', '/person/Bob', {}, + [['Accept', 'application/json; charset=utf-8']]); + }); + }); + + it('canRedefineStandardCommands', function() { + executor.defineCommand(CommandName.GO_BACK, 'POST', '/custom/back'); + + var command = new Command(CommandName.GO_BACK). + setParameter('times', 3); + + send.returns(Promise.resolve(new http.Response(200, {}, ''))); + + return assertSendsSuccessfully(command).then(function(response) { + assertSent('POST', '/custom/back', {'times': 3}, + [['Accept', 'application/json; charset=utf-8']]); + }); + }); + + it('accepts promised http clients', function() { + executor = new http.Executor(Promise.resolve(client)); + + var resp = JSON.stringify({sessionId: 'abc123'}); + send.returns(Promise.resolve(new http.Response(200, {}, resp))); + + let command = new Command(CommandName.NEW_SESSION); + return executor.execute(command).then(response => { + assertSent( + 'POST', '/session', {}, + [['Accept', 'application/json; charset=utf-8']]); + }); + }); + + function entries(map) { + let entries = []; + for (let e of map.entries()) { + entries.push(e); + } + return entries; + } + + function checkError(type, message) { + return function(e) { + if (e instanceof type) { + assert.strictEqual(e.message, message); + } else { + throw e; + } + }; + } + + function assertSent(method, path, data, headers) { + assert.ok(send.calledWith(sinon.match(function(value) { + assert.equal(value.method, method); + assert.equal(value.path, path); + assert.deepEqual(value.data, data); + assert.deepEqual(entries(value.headers), headers); + return true; + }))); + } + + function assertSendsSuccessfully(command) { + return executor.execute(command).then(function(response) { + return response; + }); + } + + function assertFailsToSend(command, opt_onError) { + return executor.execute(command).then( + () => {throw Error('should have failed')}, + (e) => {return e}); + } + }); +}); diff --git a/node_modules/selenium-webdriver/test/lib/logging_test.js b/node_modules/selenium-webdriver/test/lib/logging_test.js new file mode 100644 index 000000000..29d2af40f --- /dev/null +++ b/node_modules/selenium-webdriver/test/lib/logging_test.js @@ -0,0 +1,272 @@ +// 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. + +'use strict'; + +const assert = require('assert'); +const sinon = require('sinon'); +const logging = require('../../lib/logging'); + +describe('logging', function() { + let mgr, root, clock; + + beforeEach(function setUp() { + mgr = new logging.LogManager; + root = mgr.getLogger(''); + + clock = sinon.useFakeTimers(); + }); + + afterEach(function tearDown() { + clock.restore(); + }); + + describe('LogManager', function() { + describe('getLogger()', function() { + it('handles falsey input', function() { + assert.strictEqual(root, mgr.getLogger()); + assert.strictEqual(root, mgr.getLogger('')); + assert.strictEqual(root, mgr.getLogger(null)); + assert.strictEqual(root, mgr.getLogger(0)); + }); + + it('creates parent loggers', function() { + let logger = mgr.getLogger('foo.bar.baz'); + assert.strictEqual(logger.parent_, mgr.getLogger('foo.bar')); + + logger = logger.parent_; + assert.strictEqual(logger.parent_, mgr.getLogger('foo')); + + logger = logger.parent_; + assert.strictEqual(logger.parent_, mgr.getLogger('')); + + assert.strictEqual(logger.parent_.parent_, null); + }); + }); + }); + + describe('Logger', function() { + describe('getEffectiveLevel()', function() { + it('defaults to OFF', function() { + assert.strictEqual(root.getLevel(), logging.Level.OFF); + assert.strictEqual(root.getEffectiveLevel(), logging.Level.OFF); + + root.setLevel(null); + assert.strictEqual(root.getLevel(), null); + assert.strictEqual(root.getEffectiveLevel(), logging.Level.OFF); + }); + + it('uses own level if set', function() { + let logger = mgr.getLogger('foo.bar.baz'); + assert.strictEqual(logger.getLevel(), null); + assert.strictEqual(logger.getEffectiveLevel(), logging.Level.OFF); + + logger.setLevel(logging.Level.INFO); + assert.strictEqual(logger.getLevel(), logging.Level.INFO); + assert.strictEqual(logger.getEffectiveLevel(), logging.Level.INFO); + }); + + it('uses level from set on nearest parent', function() { + let ancestor = mgr.getLogger('foo'); + ancestor.setLevel(logging.Level.SEVERE); + + let logger = mgr.getLogger('foo.bar.baz'); + assert.strictEqual(logger.getLevel(), null); + assert.strictEqual(logger.getEffectiveLevel(), logging.Level.SEVERE); + }); + }); + + describe('isLoggable()', function() { + it('compares level against logger\'s effective level', function() { + const log1 = mgr.getLogger('foo'); + log1.setLevel(logging.Level.WARNING); + + const log2 = mgr.getLogger('foo.bar.baz'); + + assert(!log2.isLoggable(logging.Level.FINEST)); + assert(!log2.isLoggable(logging.Level.INFO)); + assert(log2.isLoggable(logging.Level.WARNING)); + assert(log2.isLoggable(logging.Level.SEVERE)); + + log2.setLevel(logging.Level.INFO); + + assert(!log2.isLoggable(logging.Level.FINEST)); + assert(log2.isLoggable(logging.Level.INFO)); + assert(log2.isLoggable(logging.Level.WARNING)); + assert(log2.isLoggable(logging.Level.SEVERE)); + + log2.setLevel(logging.Level.ALL); + + assert(log2.isLoggable(logging.Level.FINEST)); + assert(log2.isLoggable(logging.Level.INFO)); + assert(log2.isLoggable(logging.Level.WARNING)); + assert(log2.isLoggable(logging.Level.SEVERE)); + }); + + it('Level.OFF is never loggable', function() { + function test(level) { + root.setLevel(level); + assert(!root.isLoggable(logging.Level.OFF), + 'OFF should not be loggable at ' + level); + } + + test(logging.Level.ALL); + test(logging.Level.INFO); + test(logging.Level.OFF); + }); + }); + + describe('log()', function() { + it('does not invoke loggable if message is not loggable', function() { + const log = mgr.getLogger('foo'); + log.setLevel(logging.Level.OFF); + + let callback = sinon.spy(); + log.addHandler(callback); + root.addHandler(callback); + + assert(!callback.called); + }); + + it('invokes handlers for each parent logger', function() { + const cb1 = sinon.spy(); + const cb2 = sinon.spy(); + const cb3 = sinon.spy(); + const cb4 = sinon.spy(); + + const log1 = mgr.getLogger('foo'); + const log2 = mgr.getLogger('foo.bar'); + const log3 = mgr.getLogger('foo.bar.baz'); + const log4 = mgr.getLogger('foo.bar.baz.quot'); + + log1.addHandler(cb1); + log1.setLevel(logging.Level.INFO); + + log2.addHandler(cb2); + log2.setLevel(logging.Level.WARNING); + + log3.addHandler(cb3); + log3.setLevel(logging.Level.FINER); + + clock.tick(123456); + + log4.finest('this is the finest message'); + log4.finer('this is a finer message'); + log4.info('this is an info message'); + log4.warning('this is a warning message'); + log4.severe('this is a severe message'); + + assert.equal(4, cb1.callCount); + assert.equal(4, cb2.callCount); + assert.equal(4, cb3.callCount); + + const entry1 = new logging.Entry( + logging.Level.FINER, + '[foo.bar.baz.quot] this is a finer message', + 123456); + const entry2 = new logging.Entry( + logging.Level.INFO, + '[foo.bar.baz.quot] this is an info message', + 123456); + const entry3 = new logging.Entry( + logging.Level.WARNING, + '[foo.bar.baz.quot] this is a warning message', + 123456); + const entry4 = new logging.Entry( + logging.Level.SEVERE, + '[foo.bar.baz.quot] this is a severe message', + 123456); + + check(cb1.getCall(0).args[0], entry1); + check(cb1.getCall(1).args[0], entry2); + check(cb1.getCall(2).args[0], entry3); + check(cb1.getCall(3).args[0], entry4); + + check(cb2.getCall(0).args[0], entry1); + check(cb2.getCall(1).args[0], entry2); + check(cb2.getCall(2).args[0], entry3); + check(cb2.getCall(3).args[0], entry4); + + check(cb3.getCall(0).args[0], entry1); + check(cb3.getCall(1).args[0], entry2); + check(cb3.getCall(2).args[0], entry3); + check(cb3.getCall(3).args[0], entry4); + + function check(entry, expected) { + assert.equal(entry.level, expected.level, 'wrong level'); + assert.equal(entry.message, expected.message, 'wrong message'); + assert.equal(entry.timestamp, expected.timestamp, 'wrong time'); + } + }); + + it('does not invoke removed handler', function() { + root.setLevel(logging.Level.INFO); + const cb = sinon.spy(); + + root.addHandler(cb); + root.info('hi'); + assert.equal(1, cb.callCount); + + assert(root.removeHandler(cb)); + root.info('bye'); + assert.equal(1, cb.callCount); + + assert(!root.removeHandler(cb)); + }); + }); + }); + + describe('getLevel()', function() { + it('converts named levels', function() { + assert.strictEqual(logging.Level.DEBUG, logging.getLevel('DEBUG')); + assert.strictEqual(logging.Level.ALL, logging.getLevel('FAKE')); + }); + + it('converts numeric levels', function() { + assert.strictEqual( + logging.Level.DEBUG, + logging.getLevel(logging.Level.DEBUG.value)); + }); + + it('normalizes numeric levels', function() { + assert.strictEqual( + logging.Level.OFF, + logging.getLevel(logging.Level.OFF.value * 2)); + + let diff = logging.Level.SEVERE.value - logging.Level.WARNING.value; + assert.strictEqual( + logging.Level.WARNING, + logging.getLevel(logging.Level.WARNING.value + (diff * .5))); + + assert.strictEqual(logging.Level.ALL, logging.getLevel(0)); + assert.strictEqual(logging.Level.ALL, logging.getLevel(-1)); + }); + }); + + describe('Preferences', function() { + it('can be converted to JSON', function() { + let prefs = new logging.Preferences; + assert.equal('{}', JSON.stringify(prefs)); + + prefs.setLevel('foo', logging.Level.DEBUG); + assert.equal('{"foo":"DEBUG"}', JSON.stringify(prefs)); + + prefs.setLevel(logging.Type.BROWSER, logging.Level.FINE); + assert.equal('{"foo":"DEBUG","browser":"FINE"}', JSON.stringify(prefs)); + }); + }); +}); diff --git a/node_modules/selenium-webdriver/test/lib/promise_aplus_test.js b/node_modules/selenium-webdriver/test/lib/promise_aplus_test.js new file mode 100644 index 000000000..a89391590 --- /dev/null +++ b/node_modules/selenium-webdriver/test/lib/promise_aplus_test.js @@ -0,0 +1,74 @@ +// 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. + +'use strict'; + +const promise = require('../../lib/promise'); + +describe('Promises/A+ Compliance Tests', function() { + // The promise spec does not define behavior for unhandled rejections and + // assumes they are effectively swallowed. This is not the case with our + // implementation, so we have to disable error propagation to test that the + // rest of our behavior is compliant. + // We run the tests with a separate instance of the control flow to ensure + // disablign error propagation does not impact other tests. + var flow = new promise.ControlFlow(); + flow.setPropagateUnhandledRejections(false); + + // Skip the tests in 2.2.6.1/2. We are not compliant in these scenarios. + var realDescribe = global.describe; + global.describe = function(name, fn) { + realDescribe(name, function() { + var prefix = 'Promises/A+ Compliance Tests 2.2.6: ' + + '`then` may be called multiple times on the same promise.'; + var suffix = 'even when one handler is added inside another handler'; + if (this.fullTitle().startsWith(prefix) + && this.fullTitle().endsWith(suffix)) { + var realSpecify = global.specify; + try { + global.specify = function(name) { + realSpecify(name); + }; + fn(); + } finally { + global.specify = realSpecify; + } + } else { + fn(); + } + }); + }; + + require('promises-aplus-tests').mocha({ + resolved: function(value) { + return new promise.Promise((fulfill) => fulfill(value), flow); + }, + rejected: function(error) { + return new promise.Promise((_, reject) => reject(error), flow); + }, + deferred: function() { + var d = new promise.Deferred(flow); + return { + resolve: d.fulfill, + reject: d.reject, + promise: d.promise + }; + } + }); + + global.describe = realDescribe; +}); diff --git a/node_modules/selenium-webdriver/test/lib/promise_error_test.js b/node_modules/selenium-webdriver/test/lib/promise_error_test.js new file mode 100644 index 000000000..e6de3cb92 --- /dev/null +++ b/node_modules/selenium-webdriver/test/lib/promise_error_test.js @@ -0,0 +1,880 @@ +// 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. + +/** + * @fileoverview Contains tests against promise error handling. Many tests use + * NativePromise to control test termination independent of promise + * (and notably promise.ControlFlow). + */ + +'use strict'; + +const testutil = require('./testutil'); + +const assert = require('assert'); +const promise = require('../../lib/promise'); + +const NativePromise = Promise; +const StubError = testutil.StubError; +const throwStubError = testutil.throwStubError; +const assertIsStubError = testutil.assertIsStubError; + +describe('promise error handling', function() { + var flow, uncaughtExceptions; + + beforeEach(function setUp() { + flow = promise.controlFlow(); + uncaughtExceptions = []; + flow.on('uncaughtException', onUncaughtException); + }); + + afterEach(function tearDown() { + return waitForIdle(flow).then(function() { + assert.deepEqual( + [], uncaughtExceptions, 'There were uncaught exceptions'); + flow.reset(); + }); + }); + + function onUncaughtException(e) { + uncaughtExceptions.push(e); + } + + function waitForAbort(opt_flow, opt_n) { + var n = opt_n || 1; + var theFlow = opt_flow || flow; + theFlow.removeAllListeners( + promise.ControlFlow.EventType.UNCAUGHT_EXCEPTION); + return new NativePromise(function(fulfill, reject) { + theFlow.once('idle', function() { + reject(Error('expected flow to report an unhandled error')); + }); + + var errors = []; + theFlow.on('uncaughtException', onError); + function onError(e) { + errors.push(e); + if (errors.length === n) { + theFlow.removeListener('uncaughtException', onError); + fulfill(n === 1 ? errors[0] : errors); + } + } + }); + } + + function waitForIdle(opt_flow) { + var theFlow = opt_flow || flow; + return new NativePromise(function(fulfill, reject) { + if (theFlow.isIdle()) { + fulfill(); + return; + } + theFlow.once('idle', fulfill); + theFlow.once('uncaughtException', reject); + }); + } + + it('testRejectedPromiseTriggersErrorCallback', function() { + return promise.rejected(new StubError). + then(assert.fail, assertIsStubError); + }); + + describe('callback throws trigger subsequent error callback', function() { + it('fulfilled promise', function() { + return promise.fulfilled(). + then(throwStubError). + then(assert.fail, assertIsStubError); + }); + + it('rejected promise', function() { + var e = Error('not the droids you are looking for'); + return promise.rejected(e). + then(assert.fail, throwStubError). + then(assert.fail, assertIsStubError); + }); + }); + + describe('callback returns rejected promise triggers subsequent errback', function() { + it('from fulfilled callback', function() { + return promise.fulfilled().then(function() { + return promise.rejected(new StubError); + }).then(assert.fail, assertIsStubError); + }); + + it('from rejected callback', function() { + var e = Error('not the droids you are looking for'); + return promise.rejected(e). + then(assert.fail, function() { + return promise.rejected(new StubError); + }). + then(assert.fail, assertIsStubError); + }); + }); + + it('testReportsUnhandledRejectionsThroughTheControlFlow', function() { + promise.rejected(new StubError); + return waitForAbort().then(assertIsStubError); + }); + + describe('multiple unhandled rejections outside a task', function() { + it('are reported in order they occurred', function() { + var e1 = Error('error 1'); + var e2 = Error('error 2'); + + promise.rejected(e1); + promise.rejected(e2); + + return waitForAbort(flow).then(function(error) { + assert.ok( + error instanceof promise.MultipleUnhandledRejectionError); + // TODO: switch to Array.from when we drop node 0.12.x + var errors = []; + for (var e of error.errors) { + errors.push(e); + } + assert.deepEqual([e1, e2], errors); + }); + }); + }); + + describe('does not report unhandled rejection when', function() { + it('handler added before next tick', function() { + promise.rejected(new StubError).then(assert.fail, assertIsStubError); + return waitForIdle(); + }); + + it('added async but before next tick', function() { + var called = false; + return new NativePromise(function(fulfill, reject) { + var aPromise; + NativePromise.resolve().then(function() { + aPromise.then(assert.fail, function(e) { + called = true; + assertIsStubError(e); + }); + waitForIdle().then(fulfill, reject); + }); + aPromise = promise.rejected(new StubError); + }).then(function() { + assert.ok(called); + }) + }); + }); + + it('testTaskThrows', function() { + return flow.execute(throwStubError).then(assert.fail, assertIsStubError); + }); + + it('testTaskReturnsRejectedPromise', function() { + return flow.execute(function() { + return promise.rejected(new StubError) + }).then(assert.fail, assertIsStubError); + }); + + it('testTaskHasUnhandledRejection', function() { + return flow.execute(function() { + promise.rejected(new StubError) + }).then(assert.fail, assertIsStubError); + }); + + it('testTaskfails_returnedPromiseIsUnhandled', function() { + flow.execute(throwStubError); + return waitForAbort().then(assertIsStubError); + }); + + it('testTaskHasUnhandledRejection_cancelsRemainingSubTasks', function() { + var seen = []; + flow.execute(function() { + promise.rejected(new StubError); + + flow.execute(() => seen.push('a')) + .then(() => seen.push('b'), (e) => seen.push(e)); + flow.execute(() => seen.push('c')) + .then(() => seen.push('b'), (e) => seen.push(e)); + }); + + return waitForAbort() + .then(assertIsStubError) + .then(() => assert.deepEqual([], seen)); + }); + + describe('nested task failures', function() { + it('returns up to paren', function() { + return flow.execute(function() { + return flow.execute(function() { + return flow.execute(throwStubError); + }); + }).then(assert.fail, assertIsStubError); + }); + + it('task throws; uncaught error bubbles up', function() { + flow.execute(function() { + flow.execute(function() { + flow.execute(throwStubError); + }); + }); + return waitForAbort().then(assertIsStubError); + }); + + it('task throws; uncaught error bubbles up; is caught at root', function() { + flow.execute(function() { + flow.execute(function() { + flow.execute(throwStubError); + }); + }).then(assert.fail, assertIsStubError); + return waitForIdle(); + }); + + it('unhandled rejection bubbles up', function() { + flow.execute(function() { + flow.execute(function() { + flow.execute(function() { + promise.rejected(new StubError); + }); + }); + }); + return waitForAbort().then(assertIsStubError); + }); + + it('unhandled rejection bubbles up; caught at root', function() { + flow.execute(function() { + flow.execute(function() { + promise.rejected(new StubError); + }); + }).then(assert.fail, assertIsStubError); + return waitForIdle(); + }); + + it('mixtureof hanging and free subtasks', function() { + flow.execute(function() { + return flow.execute(function() { + flow.execute(throwStubError); + }); + }); + return waitForAbort().then(assertIsStubError); + }); + + it('cancels remaining tasks', function() { + var seen = []; + flow.execute(function() { + flow.execute(() => promise.rejected(new StubError)); + flow.execute(() => seen.push('a')) + .then(() => seen.push('b'), (e) => seen.push(e)); + flow.execute(() => seen.push('c')) + .then(() => seen.push('b'), (e) => seen.push(e)); + }); + + return waitForAbort() + .then(assertIsStubError) + .then(() => assert.deepEqual([], seen)); + }); + }); + + it('testTaskReturnsPromiseLikeObjectThatInvokesErrback', function() { + return flow.execute(function() { + return { + 'then': function(_, errback) { + errback('abc123'); + } + }; + }).then(assert.fail, function(value) { + assert.equal('abc123', value); + }); + }); + + describe('ControlFlow#wait();', function() { + describe('condition throws;', function() { + it('failure is caught', function() { + return flow.wait(throwStubError, 50).then(assert.fail, assertIsStubError); + }); + + it('failure is not caught', function() { + flow.wait(throwStubError, 50); + return waitForAbort().then(assertIsStubError); + }); + }); + + describe('condition returns promise', function() { + it('failure is caught', function() { + return flow.wait(function() { + return promise.rejected(new StubError); + }, 50).then(assert.fail, assertIsStubError); + }); + + it('failure is not caught', function() { + flow.wait(function() { + return promise.rejected(new StubError); + }, 50); + return waitForAbort().then(assertIsStubError); + }); + }); + + describe('condition has unhandled promise rejection', function() { + it('failure is caught', function() { + return flow.wait(function() { + promise.rejected(new StubError); + }, 50).then(assert.fail, assertIsStubError); + }); + + it('failure is not caught', function() { + flow.wait(function() { + promise.rejected(new StubError); + }, 50); + return waitForAbort().then(assertIsStubError); + }); + }); + + describe('condition has subtask failure', function() { + it('failure is caught', function() { + return flow.wait(function() { + flow.execute(function() { + flow.execute(throwStubError); + }); + }, 50).then(assert.fail, assertIsStubError); + }); + + it('failure is not caught', function() { + flow.wait(function() { + flow.execute(function() { + flow.execute(throwStubError); + }); + }, 50); + return waitForAbort().then(assertIsStubError); + }); + }); + }); + + describe('errback throws a new error', function() { + it('start with normal promise', function() { + var error = Error('an error'); + return promise.rejected(error). + catch(function(e) { + assert.equal(e, error); + throw new StubError; + }). + catch(assertIsStubError); + }); + + it('start with task result', function() { + var error = Error('an error'); + return flow.execute(function() { + throw error; + }). + catch(function(e) { + assert.equal(e, error); + throw new StubError; + }). + catch(assertIsStubError); + }); + + it('start with normal promise; uncaught error', function() { + var error = Error('an error'); + promise.rejected(error). + catch(function(e) { + assert.equal(e, error); + throw new StubError; + }); + return waitForAbort().then(assertIsStubError); + }); + + it('start with task result; uncaught error', function() { + var error = Error('an error'); + flow.execute(function() { + throw error; + }). + catch(function(e) { + assert.equal(e, error); + throw new StubError; + }); + return waitForAbort().then(assertIsStubError); + }); + }); + + it('thrownPromiseCausesCallbackRejection', function() { + let p = promise.fulfilled(1234); + return promise.fulfilled().then(function() { + throw p; + }).then(assert.fail, function(value) { + assert.strictEqual(p, value); + }); + }); + + describe('task throws promise', function() { + it('promise was fulfilled', function() { + var toThrow = promise.fulfilled(1234); + flow.execute(function() { + throw toThrow; + }).then(assert.fail, function(value) { + assert.equal(toThrow, value); + return toThrow; + }).then(function(value) { + assert.equal(1234, value); + }); + return waitForIdle(); + }); + + it('promise was rejected', function() { + var toThrow = promise.rejected(new StubError); + toThrow.catch(function() {}); // For tearDown. + flow.execute(function() { + throw toThrow; + }).then(assert.fail, function(e) { + assert.equal(toThrow, e); + return e; + }).then(assert.fail, assertIsStubError); + return waitForIdle(); + }); + }); + + it('testFailsTaskIfThereIsAnUnhandledErrorWhileWaitingOnTaskResult', function() { + var d = promise.defer(); + flow.execute(function() { + promise.rejected(new StubError); + return d.promise; + }).then(assert.fail, assertIsStubError); + + return waitForIdle().then(function() { + return d.promise; + }).then(assert.fail, function(e) { + assert.equal('CancellationError: StubError', e.toString()); + }); + }); + + it('testFailsParentTaskIfAsyncScheduledTaskFails', function() { + var d = promise.defer(); + flow.execute(function() { + flow.execute(throwStubError); + return d.promise; + }).then(assert.fail, assertIsStubError); + + return waitForIdle().then(function() { + return d.promise; + }).then(assert.fail, function(e) { + assert.equal('CancellationError: StubError', e.toString()); + }); + }); + + describe('long stack traces', function() { + afterEach(() => promise.LONG_STACK_TRACES = false); + + it('always includes task stacks in failures', function() { + promise.LONG_STACK_TRACES = false; + flow.execute(function() { + flow.execute(function() { + flow.execute(throwStubError, 'throw error'); + }, 'two'); + }, 'three'). + then(assert.fail, function(e) { + assertIsStubError(e); + if (typeof e.stack !== 'string') { + return; + } + var messages = e.stack.split(/\n/).filter(function(line, index) { + return /^From: /.test(line); + }); + assert.deepEqual([ + 'From: Task: throw error', + 'From: Task: two', + 'From: Task: three' + ], messages); + }); + return waitForIdle(); + }); + + it('does not include completed tasks', function () { + flow.execute(function() {}, 'succeeds'); + flow.execute(throwStubError, 'kaboom').then(assert.fail, function(e) { + assertIsStubError(e); + if (typeof e.stack !== 'string') { + return; + } + var messages = e.stack.split(/\n/).filter(function(line, index) { + return /^From: /.test(line); + }); + assert.deepEqual(['From: Task: kaboom'], messages); + }); + return waitForIdle(); + }); + + it('does not include promise chain when disabled', function() { + promise.LONG_STACK_TRACES = false; + flow.execute(function() { + flow.execute(function() { + return promise.fulfilled(). + then(function() {}). + then(function() {}). + then(throwStubError); + }, 'eventually assert.fails'); + }, 'start'). + then(assert.fail, function(e) { + assertIsStubError(e); + if (typeof e.stack !== 'string') { + return; + } + var messages = e.stack.split(/\n/).filter(function(line, index) { + return /^From: /.test(line); + }); + assert.deepEqual([ + 'From: Task: eventually assert.fails', + 'From: Task: start' + ], messages); + }); + return waitForIdle(); + }); + + it('includes promise chain when enabled', function() { + promise.LONG_STACK_TRACES = true; + flow.execute(function() { + flow.execute(function() { + return promise.fulfilled(). + then(function() {}). + then(function() {}). + then(throwStubError); + }, 'eventually assert.fails'); + }, 'start'). + then(assert.fail, function(e) { + assertIsStubError(e); + if (typeof e.stack !== 'string') { + return; + } + var messages = e.stack.split(/\n/).filter(function(line, index) { + return /^From: /.test(line); + }); + assert.deepEqual([ + 'From: Promise: then', + 'From: Task: eventually assert.fails', + 'From: Task: start' + ], messages); + }); + return waitForIdle(); + }); + }); + + describe('frame cancels remaining tasks', function() { + it('on unhandled task failure', function() { + var run = false; + return flow.execute(function() { + flow.execute(throwStubError); + flow.execute(function() { run = true; }); + }).then(assert.fail, function(e) { + assertIsStubError(e); + assert.ok(!run); + }); + }); + + it('on unhandled promise rejection', function() { + var run = false; + return flow.execute(function() { + promise.rejected(new StubError); + flow.execute(function() { run = true; }); + }).then(assert.fail, function(e) { + assertIsStubError(e); + assert.ok(!run); + }); + }); + + it('if task throws', function() { + var run = false; + return flow.execute(function() { + flow.execute(function() { run = true; }); + throw new StubError; + }).then(assert.fail, function(e) { + assertIsStubError(e); + assert.ok(!run); + }); + }); + + describe('task callbacks scheduled in another frame', function() { + flow = promise.controlFlow(); + function noop() {} + + let subTask; + + before(function() { + flow.execute(function() { + // This task will be discarded and never run because of the error below. + subTask = flow.execute(() => 'abc'); + throw new StubError('stub'); + }).catch(noop); + }); + + function assertCancellation(e) { + assert.ok(e instanceof promise.CancellationError); + assert.equal( + 'Task was discarded due to a previous failure: stub', e.message); + } + + it('are rejected with cancellation error', function() { + let result; + return Promise.resolve().then(function() { + return flow.execute(function() { + result = subTask.then(assert.fail); + }); + }) + .then(() => result) + .then(assert.fail, assertCancellation); + }); + + it('cancellation errors propagate through callbacks (1)', function() { + let result; + return Promise.resolve().then(function() { + return flow.execute(function() { + result = subTask + .then(assert.fail, assertCancellation) + .then(() => 'abc123'); + }); + }) + .then(() => result) + .then(value => assert.equal('abc123', value)); + }); + + it('cancellation errors propagate through callbacks (2)', function() { + let result; + return Promise.resolve().then(function() { + return flow.execute(function() { + result = subTask.then(assert.fail) + .then(noop, assertCancellation) + .then(() => 'fin'); + }); + }) + // Verify result actually computed successfully all the way through. + .then(() => result) + .then(value => assert.equal('fin', value)); + }); + }); + }); + + it('testRegisteredTaskCallbacksAreDroppedWhenTaskIsCancelled_return', function() { + var seen = []; + return flow.execute(function() { + flow.execute(throwStubError); + + flow.execute(function() { + seen.push(1); + }).then(function() { + seen.push(2); + }, function() { + seen.push(3); + }); + }).then(assert.fail, function(e) { + assertIsStubError(e); + assert.deepEqual([], seen); + }); + }); + + it('testRegisteredTaskCallbacksAreDroppedWhenTaskIsCancelled_withReturn', function() { + var seen = []; + return flow.execute(function() { + flow.execute(throwStubError); + + return flow.execute(function() { + seen.push(1); + }).then(function() { + seen.push(2); + }, function() { + seen.push(3); + }); + }).then(assert.fail, function(e) { + assertIsStubError(e); + assert.deepEqual([], seen); + }); + }); + + it('testTasksWithinACallbackAreDroppedIfContainingTaskIsAborted', function() { + var seen = []; + return flow.execute(function() { + flow.execute(throwStubError); + + // None of the callbacks on this promise should execute because the + // task assert.failure above is never handled, causing the containing task to + // abort. + promise.fulfilled().then(function() { + seen.push(1); + return flow.execute(function() { + seen.push(2); + }); + }).finally(function() { + seen.push(3); + }); + + }).then(assert.fail, function(e) { + assertIsStubError(e); + assert.deepEqual([], seen); + }); + }); + + it('testTaskIsCancelledAfterWaitTimeout', function() { + var seen = []; + return flow.execute(function() { + flow.wait(function() { + return promise.delayed(50); + }, 5); + + return flow.execute(function() { + seen.push(1); + }).then(function() { + seen.push(2); + }, function() { + seen.push(3); + }); + }).then(assert.fail, function() { + assert.deepEqual([], seen); + }); + }); + + describe('task callbacks get cancellation error if registered after task was cancelled', function() { + it('(a)', function() { + var task; + flow.execute(function() { + flow.execute(throwStubError); + task = flow.execute(function() {}); + }).then(assert.fail, assertIsStubError); + return waitForIdle().then(function() { + return task.then(assert.fail, function(e) { + assert.ok(e instanceof promise.CancellationError); + }); + }); + }); + + it('(b)', function() { + var seen = []; + + var task; + flow.execute(function() { + flow.execute(throwStubError); + task = flow.execute(function() {}); + + task.then(() => seen.push(1)) + .then(() => seen.push(2)); + task.then(() => seen.push(3)) + .then(() => seen.push(4)); + + }).then(assert.fail, assertIsStubError); + + return waitForIdle().then(function() { + return task.then(assert.fail, function(e) { + seen.push(5); + assert.ok(e instanceof promise.CancellationError); + }); + }).then(() => assert.deepEqual([5], seen)); + }); + }); + + it('unhandledRejectionInParallelTaskQueue', function() { + var seen = []; + function schedule(name) { + return flow.execute(() => seen.push(name), name); + } + + flow.async(function() { + schedule('a.1'); + flow.execute(throwStubError, 'a.2 (throws)'); + }); + + var b3; + flow.async(function() { + schedule('b.1'); + schedule('b.2'); + b3 = schedule('b.3'); + }); + + var c3; + flow.async(function() { + schedule('c.1'); + schedule('c.2'); + c3 = schedule('c.3'); + }); + + function assertWasCancelled(p) { + return p.then(assert.fail, function(e) { + assert.ok(e instanceof promise.CancellationError); + }); + } + + return waitForAbort() + .then(function() { + assert.deepEqual(['a.1', 'b.1', 'c.1', 'b.2', 'c.2'], seen); + assert.ok(!b3.isPending()); + assert.ok(!c3.isPending()); + }) + .then(() => assertWasCancelled(b3)) + .then(() => assertWasCancelled(c3)); + }); + + it('errorsInAsyncFunctionsAreReportedAsUnhandledRejection', function() { + flow.removeAllListeners(); // For tearDown. + + var task; + return new Promise(function(fulfill) { + flow.once('uncaughtException', fulfill); + flow.async(function() { + task = flow.execute(function() {}); + throw Error('boom'); + }); + }).then(function(error) { + assert.ok(error instanceof promise.CancellationError); + assert.ok(!task.isPending()); + return task.catch(function(error) { + assert.ok(error instanceof promise.CancellationError); + }); + }); + }); + + describe('does not wait for values thrown from callbacks to be resolved', function() { + it('(a)', function() { + var p1 = promise.fulfilled(); + var reason = promise.fulfilled('should not see me'); + return p1.then(function() { + throw reason; + }).then(assert.fail, function(e) { + assert.equal(reason, e); + }); + }); + + it('(b)', function() { + var p1 = promise.fulfilled(); + var reason = promise.rejected('should not see me'); + reason.catch(function() {}); // For tearDown. + return p1.then(function() { + throw reason; + }).then(assert.fail, function(e) { + assert.equal(reason, e); + }); + }); + + it('(c)', function() { + var p1 = promise.fulfilled(); + var reason = promise.defer(); + setTimeout(() => reason.fulfill('should not see me'), 100); + return p1.then(function() { + throw reason.promise; + }).then(assert.fail, function(e) { + assert.equal(reason.promise, e); + }); + }); + + it('(d)', function() { + var p1 = promise.fulfilled(); + var reason = {then: function() {}}; // A thenable like object. + return p1.then(function() { + throw reason; + }).then(assert.fail, function(e) { + assert.equal(reason, e); + }); + }); + }); +}); diff --git a/node_modules/selenium-webdriver/test/lib/promise_flow_test.js b/node_modules/selenium-webdriver/test/lib/promise_flow_test.js new file mode 100644 index 000000000..b42ac5209 --- /dev/null +++ b/node_modules/selenium-webdriver/test/lib/promise_flow_test.js @@ -0,0 +1,2284 @@ +// 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. + +'use strict'; + +const assert = require('assert'); +const fail = assert.fail; +const sinon = require('sinon'); + +const testutil = require('./testutil'); +const promise = require('../../lib/promise'); + +const NativePromise = Promise; + +// Aliases for readability. +const StubError = testutil.StubError; +const assertIsStubError = testutil.assertIsStubError; +const callbackPair = testutil.callbackPair; +const throwStubError = testutil.throwStubError; + +describe('promise control flow', function() { + let flow, flowHistory, messages, uncaughtExceptions; + + beforeEach(function setUp() { + promise.LONG_STACK_TRACES = false; + flow = new promise.ControlFlow(); + promise.setDefaultFlow(flow); + messages = []; + flowHistory = []; + + uncaughtExceptions = []; + flow.on(promise.ControlFlow.EventType.UNCAUGHT_EXCEPTION, + onUncaughtException); + }); + + afterEach(function tearDown() { + flow.removeAllListeners( + promise.ControlFlow.EventType.UNCAUGHT_EXCEPTION); + assert.deepEqual([], uncaughtExceptions, + 'There were uncaught exceptions'); + flow.reset(); + promise.LONG_STACK_TRACES = false; + }); + + function onUncaughtException(e) { + uncaughtExceptions.push(e); + } + + function waitForAbort(opt_flow) { + var theFlow = opt_flow || flow; + theFlow.removeAllListeners( + promise.ControlFlow.EventType.UNCAUGHT_EXCEPTION); + return new NativePromise(function(fulfill, reject) { + theFlow.once(promise.ControlFlow.EventType.IDLE, function() { + reject(Error('expected flow to report an unhandled error')); + }); + theFlow.once( + promise.ControlFlow.EventType.UNCAUGHT_EXCEPTION, + fulfill); + }); + } + + function waitForIdle(opt_flow) { + var theFlow = opt_flow || flow; + return new NativePromise(function(fulfill, reject) { + theFlow.once(promise.ControlFlow.EventType.IDLE, fulfill); + theFlow.once( + promise.ControlFlow.EventType.UNCAUGHT_EXCEPTION, reject); + }); + } + + function timeout(ms) { + return new NativePromise(function(fulfill) { + setTimeout(fulfill, ms); + }); + } + + + function schedule(msg, opt_return) { + return scheduleAction(msg, function() { + return opt_return; + }); + } + + /** + * @param {string} value The value to push. + * @param {promise.Promise=} opt_taskPromise Promise to return from + * the task. + * @return {!promise.Promise} The result. + */ + function schedulePush(value, opt_taskPromise) { + return scheduleAction(value, function() { + messages.push(value); + return opt_taskPromise; + }); + } + + /** + * @param {string} msg Debug message. + * @param {!Function} actionFn The function. + * @return {!promise.Promise} The function result. + */ + function scheduleAction(msg, actionFn) { + return promise.controlFlow().execute(function() { + flowHistory.push(msg); + return actionFn(); + }, msg); + } + + /** + * @param {!Function} condition The condition function. + * @param {number=} opt_timeout The timeout. + * @param {string=} opt_message Optional message. + * @return {!promise.Promise} The wait result. + */ + function scheduleWait(condition, opt_timeout, opt_message) { + var msg = opt_message || ''; + // It's not possible to hook into when the wait itself is scheduled, so + // we record each iteration of the wait loop. + var count = 0; + return promise.controlFlow().wait(function() { + flowHistory.push((count++) + ': ' + msg); + return condition(); + }, opt_timeout, msg); + } + + function asyncRun(fn, opt_self) { + NativePromise.resolve().then(() => fn.call(opt_self)); + } + + function assertFlowHistory(var_args) { + var expected = Array.prototype.slice.call(arguments, 0); + assert.deepEqual(expected, flowHistory); + } + + function assertMessages(var_args) { + var expected = Array.prototype.slice.call(arguments, 0); + assert.deepEqual(expected, messages); + } + + function assertingMessages(var_args) { + var args = Array.prototype.slice.call(arguments, 0); + return () => assertMessages.apply(null, args); + } + + function assertFlowIs(flow) { + assert.equal(flow, promise.controlFlow()); + } + + describe('testScheduling', function() { + it('aSimpleFunction', function() { + schedule('go'); + return waitForIdle().then(function() { + assertFlowHistory('go'); + }); + }); + + it('aSimpleFunctionWithANonPromiseReturnValue', function() { + schedule('go', 123).then(function(value) { + assert.equal(123, value); + }); + return waitForIdle().then(function() { + assertFlowHistory('go'); + }); + }); + + it('aSimpleSequence', function() { + schedule('a'); + schedule('b'); + schedule('c'); + return waitForIdle().then(function() { + assertFlowHistory('a', 'b', 'c'); + }); + }); + + it('invokesCallbacksWhenTaskIsDone', function() { + var d = new promise.Deferred(); + var called = false; + var done = schedule('a', d.promise).then(function(value) { + called = true; + assert.equal(123, value); + }); + return timeout(5).then(function() { + assert.ok(!called); + d.fulfill(123); + return done; + }). + then(function() { + assertFlowHistory('a'); + }); + }); + + it('blocksUntilPromiseReturnedByTaskIsResolved', function() { + var done = promise.defer(); + schedulePush('a', done.promise); + schedulePush('b'); + setTimeout(function() { + done.fulfill(); + messages.push('c'); + }, 25); + return waitForIdle().then(assertingMessages('a', 'c', 'b')); + }); + + it('waitsForReturnedPromisesToResolve', function() { + var d1 = new promise.Deferred(); + var d2 = new promise.Deferred(); + + var callback = sinon.spy(); + schedule('a', d1.promise).then(callback); + + return timeout(5).then(function() { + assert(!callback.called); + d1.fulfill(d2.promise); + return timeout(5); + }).then(function() { + assert(!callback.called); + d2.fulfill('fluffy bunny'); + return waitForIdle(); + }).then(function() { + assert(callback.called); + assert.equal('fluffy bunny', callback.getCall(0).args[0]); + assertFlowHistory('a'); + }); + }); + + it('executesTasksInAFutureTurnAfterTheyAreScheduled', function() { + var count = 0; + function incr() { count++; } + + scheduleAction('', incr); + assert.equal(0, count); + return waitForIdle().then(function() { + assert.equal(1, count); + }); + }); + + it('executesOneTaskPerTurnOfTheEventLoop', function() { + var order = []; + function go() { + order.push(order.length / 2); + asyncRun(function() { + order.push('-'); + }); + } + + scheduleAction('', go); + scheduleAction('', go); + return waitForIdle().then(function() { + assert.deepEqual([0, '-', 1, '-'], order); + }) + }); + + it('firstScheduledTaskIsWithinACallback', function() { + promise.fulfilled().then(function() { + schedule('a'); + schedule('b'); + schedule('c'); + }).then(function() { + assertFlowHistory('a', 'b', 'c'); + }); + return waitForIdle(); + }); + + it('newTasksAddedWhileWaitingOnTaskReturnedPromise1', function() { + scheduleAction('a', function() { + var d = promise.defer(); + setTimeout(function() { + schedule('c'); + d.fulfill(); + }, 10); + return d.promise; + }); + schedule('b'); + return waitForIdle().then(function() { + assertFlowHistory('a', 'b', 'c'); + }); + }); + + it('newTasksAddedWhileWaitingOnTaskReturnedPromise2', function() { + scheduleAction('a', function() { + var d = promise.defer(); + setTimeout(function() { + schedule('c'); + asyncRun(d.fulfill); + }, 10); + return d.promise; + }); + schedule('b'); + return waitForIdle().then(function() { + assertFlowHistory('a', 'c', 'b'); + }); + }); + }); + + describe('testFraming', function() { + it('callbacksRunInANewFrame', function() { + schedule('a').then(function() { + schedule('c'); + }); + schedule('b'); + return waitForIdle().then(function() { + assertFlowHistory('a', 'c', 'b'); + }); + }); + + it('lotsOfNesting', function() { + schedule('a').then(function() { + schedule('c').then(function() { + schedule('e').then(function() { + schedule('g'); + }); + schedule('f'); + }); + schedule('d'); + }); + schedule('b'); + + return waitForIdle().then(function() { + assertFlowHistory('a', 'c', 'e', 'g', 'f', 'd', 'b'); + }); + }); + + it('callbackReturnsPromiseThatDependsOnATask_1', function() { + schedule('a').then(function() { + schedule('b'); + return promise.delayed(5).then(function() { + return schedule('c'); + }); + }); + schedule('d'); + + return waitForIdle().then(function() { + assertFlowHistory('a', 'b', 'c', 'd'); + }); + }); + + it('callbackReturnsPromiseThatDependsOnATask_2', function() { + schedule('a').then(function() { + schedule('b'); + return promise.delayed(5). + then(function() { return promise.delayed(5) }). + then(function() { return promise.delayed(5) }). + then(function() { return promise.delayed(5) }). + then(function() { return schedule('c'); }); + }); + schedule('d'); + + return waitForIdle().then(function() { + assertFlowHistory('a', 'b', 'c', 'd'); + }); + }); + + it('eachCallbackWaitsForAllScheduledTasksToComplete', function() { + schedule('a'). + then(function() { + schedule('b'); + schedule('c'); + }). + then(function() { + schedule('d'); + }); + schedule('e'); + + return waitForIdle().then(function() { + assertFlowHistory('a', 'b', 'c', 'd', 'e'); + }); + }); + + it('eachCallbackWaitsForReturnTasksToComplete', function() { + schedule('a'). + then(function() { + schedule('b'); + return schedule('c'); + }). + then(function() { + schedule('d'); + }); + schedule('e'); + + return waitForIdle().then(function() { + assertFlowHistory('a', 'b', 'c', 'd', 'e'); + }); + }); + + it('callbacksOnAResolvedPromiseInsertIntoTheCurrentFlow', function() { + promise.fulfilled().then(function() { + schedule('b'); + }); + schedule('a'); + + return waitForIdle().then(function() { + assertFlowHistory('b', 'a'); + }); + }); + + it('callbacksInterruptTheFlowWhenPromiseIsResolved', function() { + schedule('a').then(function() { + schedule('c'); + }); + schedule('b'); + + return waitForIdle().then(function() { + assertFlowHistory('a', 'c', 'b'); + }); + }); + + it('allCallbacksInAFrameAreScheduledWhenPromiseIsResolved', function() { + var a = schedule('a'); + a.then(function() { schedule('b'); }); + schedule('c'); + a.then(function() { schedule('d'); }); + schedule('e'); + + return waitForIdle().then(function() { + assertFlowHistory('a', 'b', 'c', 'd', 'e'); + }); + }); + + it('tasksScheduledInInActiveFrameDoNotGetPrecedence', function() { + var d = promise.fulfilled(); + schedule('a'); + schedule('b'); + d.then(function() { schedule('c'); }); + + return waitForIdle().then(function() { + assertFlowHistory('a', 'b', 'c'); + }); + }); + + it('tasksScheduledInAFrameGetPrecedence_1', function() { + var a = schedule('a'); + schedule('b').then(function() { + a.then(function() { + schedule('c'); + schedule('d'); + }); + var e = schedule('e'); + a.then(function() { + schedule('f'); + e.then(function() { + schedule('g'); + }); + schedule('h'); + }); + schedule('i'); + }); + schedule('j'); + + return waitForIdle().then(function() { + assertFlowHistory('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'); + }); + }); + }); + + describe('testErrorHandling', function() { + it('thrownErrorsArePassedToTaskErrback', function() { + scheduleAction('function that throws', throwStubError). + then(fail, assertIsStubError); + return waitForIdle(); + }); + + it('thrownErrorsPropagateThroughPromiseChain', function() { + scheduleAction('function that throws', throwStubError). + then(fail). + then(fail, assertIsStubError); + return waitForIdle(); + }); + + it('catchesErrorsFromFailedTasksInAFrame', function() { + schedule('a').then(function() { + schedule('b'); + scheduleAction('function that throws', throwStubError); + }). + then(fail, assertIsStubError); + return waitForIdle(); + }); + + it('abortsIfOnlyTaskReturnsAnUnhandledRejection', function() { + scheduleAction('function that returns rejected promise', function() { + return promise.rejected(new StubError); + }); + return waitForAbort().then(assertIsStubError); + }); + + it('abortsIfThereIsAnUnhandledRejection', function() { + promise.rejected(new StubError); + schedule('this should not run'); + return waitForAbort(). + then(assertIsStubError). + then(function() { + assertFlowHistory(/* none */); + }); + }); + + it('abortsSequenceIfATaskFails', function() { + schedule('a'); + schedule('b'); + scheduleAction('c', throwStubError); + schedule('d'); // Should never execute. + + return waitForAbort(). + then(assertIsStubError). + then(function() { + assertFlowHistory('a', 'b', 'c'); + }); + }); + + it('abortsFromUnhandledFramedTaskFailures_1', function() { + schedule('outer task').then(function() { + scheduleAction('inner task', throwStubError); + }); + schedule('this should not run'); + return waitForAbort(). + then(assertIsStubError). + then(function() { + assertFlowHistory('outer task', 'inner task'); + }); + }); + + it('abortsFromUnhandledFramedTaskFailures_2', function() { + schedule('a').then(function() { + schedule('b').then(function() { + scheduleAction('c', throwStubError); + // This should not execute. + schedule('d'); + }); + }); + + return waitForAbort(). + then(assertIsStubError). + then(function() { + assertFlowHistory('a', 'b', 'c'); + }); + }); + + it('abortsWhenErrorBubblesUpFromFullyResolvingAnObject', function() { + var callback = sinon.spy(); + + scheduleAction('', function() { + var obj = {'foo': promise.rejected(new StubError)}; + return promise.fullyResolved(obj).then(callback); + }); + + return waitForAbort(). + then(assertIsStubError). + then(() => assert(!callback.called)); + }); + + it('abortsWhenErrorBubblesUpFromFullyResolvingAnObject_withCallback', function() { + var callback1 = sinon.spy(); + var callback2 = sinon.spy(); + + scheduleAction('', function() { + var obj = {'foo': promise.rejected(new StubError)}; + return promise.fullyResolved(obj).then(callback1); + }).then(callback2); + + return waitForAbort(). + then(assertIsStubError). + then(() => assert(!callback1.called)). + then(() => assert(!callback2.called)); + }); + + it('canCatchErrorsFromNestedTasks', function() { + var errback = sinon.spy(); + schedule('a'). + then(function() { + return scheduleAction('b', throwStubError); + }). + catch(errback); + return waitForIdle().then(function() { + assert(errback.called); + assertIsStubError(errback.getCall(0).args[0]); + }); + }); + + it('nestedCommandFailuresCanBeCaughtAndSuppressed', function() { + var errback = sinon.spy(); + schedule('a').then(function() { + return schedule('b').then(function() { + return schedule('c').then(function() { + throw new StubError; + }); + }); + }).catch(errback); + schedule('d'); + return waitForIdle(). + then(function() { + assert(errback.called); + assertIsStubError(errback.getCall(0).args[0]); + assertFlowHistory('a', 'b', 'c', 'd'); + }); + }); + + it('aTaskWithAnUnhandledPromiseRejection', function() { + schedule('a'); + scheduleAction('sub-tasks', function() { + promise.rejected(new StubError); + }); + schedule('should never run'); + + return waitForAbort(). + then(assertIsStubError). + then(function() { + assertFlowHistory('a', 'sub-tasks'); + }); + }); + + it('aTaskThatReutrnsARejectedPromise', function() { + schedule('a'); + scheduleAction('sub-tasks', function() { + return promise.rejected(new StubError); + }); + schedule('should never run'); + + return waitForAbort(). + then(assertIsStubError). + then(function() { + assertFlowHistory('a', 'sub-tasks'); + }); + }); + + it('discardsSubtasksIfTaskThrows', function() { + var pair = callbackPair(null, assertIsStubError); + scheduleAction('a', function() { + schedule('b'); + schedule('c'); + throwStubError(); + }).then(pair.callback, pair.errback); + schedule('d'); + + return waitForIdle(). + then(pair.assertErrback). + then(function() { + assertFlowHistory('a', 'd'); + }); + }); + + it('discardsRemainingSubtasksIfASubtaskFails', function() { + var pair = callbackPair(null, assertIsStubError); + scheduleAction('a', function() { + schedule('b'); + scheduleAction('c', throwStubError); + schedule('d'); + }).then(pair.callback, pair.errback); + schedule('e'); + + return waitForIdle(). + then(pair.assertErrback). + then(function() { + assertFlowHistory('a', 'b', 'c', 'e'); + }); + }); + }); + + describe('testTryModelingFinally', function() { + it('happyPath', function() { + /* Model: + try { + doFoo(); + doBar(); + } finally { + doBaz(); + } + */ + schedulePush('foo'). + then(() => schedulePush('bar')). + finally(() => schedulePush('baz')); + return waitForIdle().then(assertingMessages('foo', 'bar', 'baz')); + }); + + it('firstTryFails', function() { + /* Model: + try { + doFoo(); + doBar(); + } finally { + doBaz(); + } + */ + + scheduleAction('doFoo and throw', function() { + messages.push('foo'); + throw new StubError; + }). + then(function() { schedulePush('bar'); }). + finally(function() { schedulePush('baz'); }); + + return waitForAbort(). + then(assertIsStubError). + then(assertingMessages('foo', 'baz')); + }); + + it('secondTryFails', function() { + /* Model: + try { + doFoo(); + doBar(); + } finally { + doBaz(); + } + */ + + schedulePush('foo'). + then(function() { + return scheduleAction('doBar and throw', function() { + messages.push('bar'); + throw new StubError; + }); + }). + finally(function() { + return schedulePush('baz'); + }); + return waitForAbort(). + then(assertIsStubError). + then(assertingMessages('foo', 'bar', 'baz')); + }); + }); + + describe('testTaskCallbacksInterruptFlow', function() { + it('(base case)', function() { + schedule('a').then(function() { + schedule('b'); + }); + schedule('c'); + return waitForIdle().then(function() { + assertFlowHistory('a', 'b', 'c'); + }); + }); + + it('taskDependsOnImmediatelyFulfilledPromise', function() { + scheduleAction('a', function() { + return promise.fulfilled(); + }).then(function() { + schedule('b'); + }); + schedule('c'); + return waitForIdle().then(function() { + assertFlowHistory('a', 'b', 'c'); + }); + }); + + it('taskDependsOnPreviouslyFulfilledPromise', function() { + var aPromise = promise.fulfilled(123); + scheduleAction('a', function() { + return aPromise; + }).then(function(value) { + assert.equal(123, value); + schedule('b'); + }); + schedule('c'); + return waitForIdle().then(function() { + assertFlowHistory('a', 'b', 'c'); + }); + }); + + it('taskDependsOnAsyncPromise', function() { + scheduleAction('a', function() { + return promise.delayed(25); + }).then(function() { + schedule('b'); + }); + schedule('c'); + return waitForIdle().then(function() { + assertFlowHistory('a', 'b', 'c'); + }); + }); + + it('promiseChainedToTaskInterruptFlow', function() { + schedule('a').then(function() { + return promise.fulfilled(); + }).then(function() { + return promise.fulfilled(); + }).then(function() { + schedule('b'); + }); + schedule('c'); + return waitForIdle().then(function() { + assertFlowHistory('a', 'b', 'c'); + }); + }); + + it('nestedTaskCallbacksInterruptFlowWhenResolved', function() { + schedule('a').then(function() { + schedule('b').then(function() { + schedule('c'); + }); + }); + schedule('d'); + return waitForIdle().then(function() { + assertFlowHistory('a', 'b', 'c', 'd'); + }); + }); + }); + + describe('testDelayedNesting', function() { + + it('1', function() { + var a = schedule('a'); + schedule('b').then(function() { + a.then(function() { schedule('c'); }); + schedule('d'); + }); + schedule('e'); + + return waitForIdle().then(function() { + assertFlowHistory('a', 'b', 'c', 'd', 'e'); + }); + }); + + it('2', function() { + var a = schedule('a'); + schedule('b').then(function() { + a.then(function() { schedule('c'); }); + schedule('d'); + a.then(function() { schedule('e'); }); + }); + schedule('f'); + + return waitForIdle().then(function() { + assertFlowHistory('a', 'b', 'c', 'd', 'e', 'f'); + }); + }); + + it('3', function() { + var a = schedule('a'); + schedule('b').then(function() { + a.then(function() { schedule('c'); }); + a.then(function() { schedule('d'); }); + }); + schedule('e'); + + return waitForIdle().then(function() { + assertFlowHistory('a', 'b', 'c', 'd', 'e'); + }); + }); + + it('4', function() { + var a = schedule('a'); + schedule('b').then(function() { + a.then(function() { schedule('c'); }).then(function() { + schedule('d'); + }); + a.then(function() { schedule('e'); }); + }); + schedule('f'); + + return waitForIdle().then(function() { + assertFlowHistory('a', 'b', 'c', 'd', 'e', 'f'); + }); + }); + + it('5', function() { + var a = schedule('a'); + schedule('b').then(function() { + var c; + a.then(function() { c = schedule('c'); }).then(function() { + schedule('d'); + a.then(function() { schedule('e'); }); + c.then(function() { schedule('f'); }); + schedule('g'); + }); + a.then(function() { schedule('h'); }); + }); + schedule('i'); + + return waitForIdle().then(function() { + assertFlowHistory('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'); + }); + }); + }); + + describe('testWaiting', function() { + it('onAConditionThatIsAlwaysTrue', function() { + scheduleWait(function() { return true;}, 0, 'waiting on true'); + return waitForIdle().then(function() { + assertFlowHistory('0: waiting on true'); + }); + }); + + it('aSimpleCountingCondition', function() { + var count = 0; + scheduleWait(function() { + return ++count == 3; + }, 100, 'counting to 3'); + + return waitForIdle().then(function() { + assert.equal(3, count); + }); + }); + + it('aConditionThatReturnsAPromise', function() { + var d = new promise.Deferred(); + var count = 0; + + scheduleWait(function() { + count += 1; + return d.promise; + }, 0, 'waiting for promise'); + + return timeout(50).then(function() { + assert.equal(1, count); + d.fulfill(123); + return waitForIdle(); + }); + }); + + it('aConditionThatReturnsAPromise_2', function() { + var count = 0; + scheduleWait(function() { + return promise.fulfilled(++count == 3); + }, 100, 'waiting for promise'); + + return waitForIdle().then(function() { + assert.equal(3, count); + }); + }); + + it('aConditionThatReturnsATaskResult', function() { + var count = 0; + scheduleWait(function() { + return scheduleAction('increment count', function() { + return ++count == 3; + }); + }, 100, 'counting to 3'); + schedule('post wait'); + + return waitForIdle().then(function() { + assert.equal(3, count); + assertFlowHistory( + '0: counting to 3', 'increment count', + '1: counting to 3', 'increment count', + '2: counting to 3', 'increment count', + 'post wait'); + }); + }); + + it('conditionContainsASubtask', function() { + var count = 0; + scheduleWait(function() { + schedule('sub task'); + return ++count == 3; + }, 100, 'counting to 3'); + schedule('post wait'); + + return waitForIdle().then(function() { + assert.equal(3, count); + assertFlowHistory( + '0: counting to 3', 'sub task', + '1: counting to 3', 'sub task', + '2: counting to 3', 'sub task', + 'post wait'); + }); + }); + + it('cancelsWaitIfScheduledTaskFails', function() { + var pair = callbackPair(null, assertIsStubError); + scheduleWait(function() { + scheduleAction('boom', throwStubError); + schedule('this should not run'); + return true; + }, 100, 'waiting to go boom').then(pair.callback, pair.errback); + schedule('post wait'); + + return waitForIdle(). + then(pair.assertErrback). + then(function() { + assertFlowHistory( + '0: waiting to go boom', 'boom', + 'post wait'); + }); + }); + + it('failsIfConditionThrows', function() { + var callbacks = callbackPair(null, assertIsStubError); + scheduleWait(throwStubError, 0, 'goes boom'). + then(callbacks.callback, callbacks.errback); + schedule('post wait'); + + return waitForIdle(). + then(callbacks.assertErrback). + then(function() { + assertFlowHistory('0: goes boom', 'post wait'); + }); + }); + + it('failsIfConditionReturnsARejectedPromise', function() { + var callbacks = callbackPair(null, assertIsStubError); + scheduleWait(function() { + return promise.rejected(new StubError); + }, 0, 'goes boom').then(callbacks.callback, callbacks.errback); + schedule('post wait'); + + return waitForIdle(). + then(callbacks.assertErrback). + then(function() { + assertFlowHistory('0: goes boom', 'post wait'); + }); + }); + + it('failsIfConditionHasUnhandledRejection', function() { + var callbacks = callbackPair(null, assertIsStubError); + scheduleWait(function() { + promise.controlFlow().execute(throwStubError); + }, 0, 'goes boom').then(callbacks.callback, callbacks.errback); + schedule('post wait'); + + return waitForIdle(). + then(callbacks.assertErrback). + then(function() { + assertFlowHistory('0: goes boom', 'post wait'); + }); + }); + + it('failsIfConditionHasAFailedSubtask', function() { + var callbacks = callbackPair(null, assertIsStubError); + var count = 0; + scheduleWait(function() { + scheduleAction('maybe throw', function() { + if (++count == 2) { + throw new StubError; + } + }); + }, 100, 'waiting').then(callbacks.callback, callbacks.errback); + schedule('post wait'); + + return waitForIdle().then(function() { + assert.equal(2, count); + assertFlowHistory( + '0: waiting', 'maybe throw', + '1: waiting', 'maybe throw', + 'post wait'); + }); + }); + + it('pollingLoopWaitsForAllScheduledTasksInCondition', function() { + var count = 0; + scheduleWait(function() { + scheduleAction('increment count', function() { ++count; }); + return count >= 3; + }, 100, 'counting to 3'); + schedule('post wait'); + + return waitForIdle().then(function() { + assert.equal(4, count); + assertFlowHistory( + '0: counting to 3', 'increment count', + '1: counting to 3', 'increment count', + '2: counting to 3', 'increment count', + '3: counting to 3', 'increment count', + 'post wait'); + }); + }); + + it('waitsForeverOnAZeroTimeout', function() { + var done = false; + setTimeout(function() { + done = true; + }, 150); + var waitResult = scheduleWait(function() { + return done; + }, 0); + + return timeout(75).then(function() { + assert.ok(!done); + return timeout(100); + }).then(function() { + assert.ok(done); + return waitResult; + }); + }); + + it('waitsForeverIfTimeoutOmitted', function() { + var done = false; + setTimeout(function() { + done = true; + }, 150); + var waitResult = scheduleWait(function() { + return done; + }); + + return timeout(75).then(function() { + assert.ok(!done); + return timeout(100); + }).then(function() { + assert.ok(done); + return waitResult; + }); + }); + + it('timesOut_nonZeroTimeout', function() { + var count = 0; + scheduleWait(function() { + count += 1; + var ms = count === 2 ? 65 : 5; + return promise.delayed(ms).then(function() { + return false; + }); + }, 60, 'counting to 3'); + return waitForAbort().then(function(e) { + switch (count) { + case 1: + assertFlowHistory('0: counting to 3'); + break; + case 2: + assertFlowHistory('0: counting to 3', '1: counting to 3'); + break; + default: + fail('unexpected polling count: ' + count); + } + assert.ok( + /^counting to 3\nWait timed out after \d+ms$/.test(e.message)); + }); + }); + + it('shouldFailIfConditionReturnsARejectedPromise', function() { + scheduleWait(function() { + return promise.rejected(new StubError); + }, 100, 'returns rejected promise on first pass'); + return waitForAbort().then(assertIsStubError); + }); + + it('scheduleWithIntermittentWaits', function() { + schedule('a'); + scheduleWait(function() { return true; }, 0, 'wait 1'); + schedule('b'); + scheduleWait(function() { return true; }, 0, 'wait 2'); + schedule('c'); + scheduleWait(function() { return true; }, 0, 'wait 3'); + + return waitForIdle().then(function() { + assertFlowHistory('a', '0: wait 1', 'b', '0: wait 2', 'c', '0: wait 3'); + }); + }); + + it('scheduleWithIntermittentAndNestedWaits', function() { + schedule('a'); + scheduleWait(function() { return true; }, 0, 'wait 1'). + then(function() { + schedule('d'); + scheduleWait(function() { return true; }, 0, 'wait 2'); + schedule('e'); + }); + schedule('b'); + scheduleWait(function() { return true; }, 0, 'wait 3'); + schedule('c'); + scheduleWait(function() { return true; }, 0, 'wait 4'); + + return waitForIdle().then(function() { + assertFlowHistory( + 'a', '0: wait 1', 'd', '0: wait 2', 'e', 'b', '0: wait 3', 'c', + '0: wait 4'); + }); + }); + + it('requiresConditionToBeAPromiseOrFunction', function() { + assert.throws(function() { + flow.wait(1234, 0); + }); + flow.wait(function() { return true;}, 0); + flow.wait(promise.fulfilled(), 0); + return waitForIdle(); + }); + + it('promiseThatDoesNotResolveBeforeTimeout', function() { + var d = promise.defer(); + flow.wait(d.promise, 5).then(fail, function(e) { + assert.ok( + /Timed out waiting for promise to resolve after \d+ms/ + .test(e.message), + 'unexpected error message: ' + e.message); + }); + return waitForIdle().then(function() { + assert.ok('Promise should not be cancelled', d.promise.isPending()); + }); + }); + + it('unboundedWaitOnPromiseResolution', function() { + var messages = []; + var d = promise.defer(); + var waitResult = flow.wait(d.promise).then(function(value) { + messages.push('b'); + assert.equal(1234, value); + }); + setTimeout(function() { + messages.push('a'); + }, 5); + + timeout(10).then(function() { + assert.deepEqual(['a'], messages); + assert.ok(waitResult.isPending()); + d.fulfill(1234); + return waitResult; + }).then(function() { + assert.deepEqual(['a', 'b'], messages); + }); + + return waitForIdle(); + }); + }); + + describe('testSubtasks', function() { + it('(base case)', function() { + schedule('a'); + scheduleAction('sub-tasks', function() { + schedule('c'); + schedule('d'); + }); + schedule('b'); + + return waitForIdle().then(function() { + assertFlowHistory('a', 'sub-tasks', 'c', 'd', 'b'); + }); + }); + + it('nesting', function() { + schedule('a'); + scheduleAction('sub-tasks', function() { + schedule('b'); + scheduleAction('sub-sub-tasks', function() { + schedule('c'); + schedule('d'); + }); + schedule('e'); + }); + schedule('f'); + + return waitForIdle().then(function() { + assertFlowHistory( + 'a', 'sub-tasks', 'b', 'sub-sub-tasks', 'c', 'd', 'e', 'f'); + }); + }); + + it('taskReturnsSubTaskResult_1', function() { + schedule('a'); + scheduleAction('sub-tasks', function() { + return schedule('c'); + }); + schedule('b'); + + return waitForIdle().then(function() { + assertFlowHistory('a', 'sub-tasks', 'c', 'b'); + }); + }); + + it('taskReturnsSubTaskResult_2', function() { + let pair = callbackPair((value) => assert.equal(123, value)); + schedule('a'); + schedule('sub-tasks', promise.fulfilled(123)).then(pair.callback); + schedule('b'); + + return waitForIdle().then(function() { + assertFlowHistory('a', 'sub-tasks','b'); + pair.assertCallback(); + }); + }); + + it('taskReturnsPromiseThatDependsOnSubtask_1', function() { + scheduleAction('a', function() { + return promise.delayed(10).then(function() { + schedule('b'); + }); + }); + schedule('c'); + return waitForIdle().then(function() { + assertFlowHistory('a', 'b', 'c'); + }); + }); + + it('taskReturnsPromiseThatDependsOnSubtask_2', function() { + scheduleAction('a', function() { + return promise.fulfilled().then(function() { + schedule('b'); + }); + }); + schedule('c'); + return waitForIdle().then(function() { + assertFlowHistory('a', 'b', 'c'); + }); + }); + + it('taskReturnsPromiseThatDependsOnSubtask_3', function() { + scheduleAction('a', function() { + return promise.delayed(10).then(function() { + return schedule('b'); + }); + }); + schedule('c'); + return waitForIdle().then(function() { + assertFlowHistory('a', 'b', 'c'); + }); + }); + + it('taskReturnsPromiseThatDependsOnSubtask_4', function() { + scheduleAction('a', function() { + return promise.delayed(5).then(function() { + return promise.delayed(5).then(function() { + return schedule('b'); + }); + }); + }); + schedule('c'); + return waitForIdle().then(function() { + assertFlowHistory('a', 'b', 'c'); + }); + }); + + it('taskReturnsPromiseThatDependsOnSubtask_5', function() { + scheduleAction('a', function() { + return promise.delayed(5).then(function() { + return promise.delayed(5).then(function() { + return promise.delayed(5).then(function() { + return promise.delayed(5).then(function() { + return schedule('b'); + }); + }); + }); + }); + }); + schedule('c'); + return waitForIdle().then(function() { + assertFlowHistory('a', 'b', 'c'); + }); + }); + + it('taskReturnsPromiseThatDependsOnSubtask_6', function() { + scheduleAction('a', function() { + return promise.delayed(5). + then(function() { return promise.delayed(5) }). + then(function() { return promise.delayed(5) }). + then(function() { return promise.delayed(5) }). + then(function() { return schedule('b'); }); + }); + schedule('c'); + return waitForIdle().then(function() { + assertFlowHistory('a', 'b', 'c'); + }); + }); + + it('subTaskFails_1', function() { + schedule('a'); + scheduleAction('sub-tasks', function() { + scheduleAction('sub-task that fails', throwStubError); + }); + schedule('should never execute'); + + return waitForAbort(). + then(assertIsStubError). + then(function() { + assertFlowHistory('a', 'sub-tasks', 'sub-task that fails'); + }); + }); + + it('subTaskFails_2', function() { + schedule('a'); + scheduleAction('sub-tasks', function() { + return promise.rejected(new StubError); + }); + schedule('should never execute'); + + return waitForAbort(). + then(assertIsStubError). + then(function() { + assertFlowHistory('a', 'sub-tasks'); + }); + }); + + it('subTaskFails_3', function() { + var callbacks = callbackPair(null, assertIsStubError); + + schedule('a'); + scheduleAction('sub-tasks', function() { + return promise.rejected(new StubError); + }).then(callbacks.callback, callbacks.errback); + schedule('b'); + + return waitForIdle(). + then(function() { + assertFlowHistory('a', 'sub-tasks', 'b'); + callbacks.assertErrback(); + }); + }); + }); + + describe('testEventLoopWaitsOnPendingPromiseRejections', function() { + it('oneRejection', function() { + var d = new promise.Deferred; + scheduleAction('one', function() { + return d.promise; + }); + scheduleAction('two', function() {}); + + return timeout(50).then(function() { + assertFlowHistory('one'); + d.reject(new StubError); + return waitForAbort(); + }). + then(assertIsStubError). + then(function() { + assertFlowHistory('one'); + }); + }); + + it('multipleRejections', function() { + var once = Error('once'); + var twice = Error('twice'); + + scheduleAction('one', function() { + promise.rejected(once); + promise.rejected(twice); + }); + var twoResult = scheduleAction('two', function() {}); + + flow.removeAllListeners( + promise.ControlFlow.EventType.UNCAUGHT_EXCEPTION); + return new NativePromise(function(fulfill, reject) { + setTimeout(function() { + reject(Error('Should have reported the two errors by now')); + }, 50); + flow.on( + promise.ControlFlow.EventType.UNCAUGHT_EXCEPTION, + fulfill); + }).then(function(e) { + assert.ok(e instanceof promise.MultipleUnhandledRejectionError, + 'Not a MultipleUnhandledRejectionError'); + let errors = Array.from(e.errors); + assert.deepEqual([once, twice], errors); + assertFlowHistory('one'); + assert.ok(!twoResult.isPending(), 'Did not cancel the second task'); + }); + }); + }); + + describe('testCancelsPromiseReturnedByCallbackIfFrameFails', function() { + it('promiseCallback', function() { + var chainPair = callbackPair(null, assertIsStubError); + var deferredPair = callbackPair(null, function(e) { + assert.equal('CancellationError: StubError', e.toString(), + 'callback result should be cancelled'); + }); + + var d = new promise.Deferred(); + d.promise.then(deferredPair.callback, deferredPair.errback); + + promise.fulfilled(). + then(function() { + scheduleAction('boom', throwStubError); + schedule('this should not run'); + return d.promise; + }). + then(chainPair.callback, chainPair.errback); + + return waitForIdle().then(function() { + assertFlowHistory('boom'); + chainPair.assertErrback('chain errback not invoked'); + deferredPair.assertErrback('deferred errback not invoked'); + }); + }); + + it('taskCallback', function() { + var chainPair = callbackPair(null, assertIsStubError); + var deferredPair = callbackPair(null, function(e) { + assert.equal('CancellationError: StubError', e.toString(), + 'callback result should be cancelled'); + }); + + var d = new promise.Deferred(); + d.promise.then(deferredPair.callback, deferredPair.errback); + + schedule('a'). + then(function() { + scheduleAction('boom', throwStubError); + schedule('this should not run'); + return d.promise; + }). + then(chainPair.callback, chainPair.errback); + + return waitForIdle().then(function() { + assertFlowHistory('a', 'boom'); + chainPair.assertErrback('chain errback not invoked'); + deferredPair.assertErrback('deferred errback not invoked'); + }); + }); + }); + + it('testMaintainsOrderInCallbacksWhenATaskReturnsAPromise', function() { + schedule('__start__', promise.fulfilled()). + then(function() { + messages.push('a'); + schedulePush('b'); + messages.push('c'); + }). + then(function() { + messages.push('d'); + }); + schedulePush('e'); + + return waitForIdle().then(function() { + assertFlowHistory('__start__', 'b', 'e'); + assertMessages('a', 'c', 'b', 'd', 'e'); + }); + }); + + it('testOwningFlowIsActivatedForExecutingTasks', function() { + var defaultFlow = promise.controlFlow(); + var order = []; + + promise.createFlow(function(flow) { + assertFlowIs(flow); + order.push(0); + + defaultFlow.execute(function() { + assertFlowIs(defaultFlow); + order.push(1); + }); + }); + + return waitForIdle().then(function() { + assertFlowIs(defaultFlow); + assert.deepEqual([0, 1], order); + }); + }); + + it('testCreateFlowReturnsPromisePairedWithCreatedFlow', function() { + return new NativePromise(function(fulfill, reject) { + var newFlow; + promise.createFlow(function(flow) { + newFlow = flow; + assertFlowIs(newFlow); + }).then(function() { + assertFlowIs(newFlow); + waitForIdle(newFlow).then(fulfill, reject); + }); + }); + }); + + it('testDeferredFactoriesCreateForActiveFlow_defaultFlow', function() { + var e = Error(); + var defaultFlow = promise.controlFlow(); + promise.fulfilled().then(function() { + assertFlowIs(defaultFlow); + }); + promise.rejected(e).then(null, function(err) { + assert.equal(e, err); + assertFlowIs(defaultFlow); + }); + promise.defer().promise.then(function() { + assertFlowIs(defaultFlow); + }); + + return waitForIdle(); + }); + + it('testDeferredFactoriesCreateForActiveFlow_newFlow', function() { + var e = Error(); + var newFlow = new promise.ControlFlow; + newFlow.execute(function() { + promise.fulfilled().then(function() { + assertFlowIs(newFlow); + }); + + promise.rejected(e).then(null, function(err) { + assert.equal(e, err); + assertFlowIs(newFlow); + }); + + let d = promise.defer(); + d.promise.then(function() { + assertFlowIs(newFlow); + }); + d.fulfill(); + }).then(function() { + assertFlowIs(newFlow); + }); + + return waitForIdle(newFlow); + }); + + it('testFlowsSynchronizeWithThemselvesNotEachOther', function() { + var defaultFlow = promise.controlFlow(); + schedulePush('a', 'a'); + promise.controlFlow().timeout(250); + schedulePush('b', 'b'); + + promise.createFlow(function() { + schedulePush('c', 'c'); + schedulePush('d', 'd'); + }); + + return waitForIdle().then(function() { + assertMessages('a', 'c', 'd', 'b'); + }); + }); + + it('testUnhandledErrorsAreReportedToTheOwningFlow', function() { + var error1 = Error('e1'); + var error2 = Error('e2'); + + var defaultFlow = promise.controlFlow(); + defaultFlow.removeAllListeners('uncaughtException'); + + var flow1Error = NativePromise.defer(); + flow1Error.promise.then(function(value) { + assert.equal(error2, value); + }); + + var flow2Error = NativePromise.defer(); + flow2Error.promise.then(function(value) { + assert.equal(error1, value); + }); + + promise.createFlow(function(flow) { + flow.once('uncaughtException', flow2Error.resolve); + promise.rejected(error1); + + defaultFlow.once('uncaughtException', flow1Error.resolve); + defaultFlow.execute(function() { + promise.rejected(error2); + }); + }); + + return NativePromise.all([flow1Error.promise, flow2Error.promise]); + }); + + it('testCanSynchronizeFlowsByReturningPromiseFromOneToAnother', function() { + var flow1 = new promise.ControlFlow; + var flow1Done = NativePromise.defer(); + flow1.once('idle', flow1Done.resolve); + flow1.once('uncaughtException', flow1Done.reject); + + var flow2 = new promise.ControlFlow; + var flow2Done = NativePromise.defer(); + flow2.once('idle', flow2Done.resolve); + flow2.once('uncaughtException', flow2Done.reject); + + flow1.execute(function() { + schedulePush('a', 'a'); + return promise.delayed(25); + }, 'start flow 1'); + + flow2.execute(function() { + schedulePush('b', 'b'); + schedulePush('c', 'c'); + flow2.execute(function() { + return flow1.execute(function() { + schedulePush('d', 'd'); + }, 'flow 1 task'); + }, 'inject flow1 result into flow2'); + schedulePush('e', 'e'); + }, 'start flow 2'); + + return NativePromise.all([flow1Done.promise, flow2Done.promise]). + then(function() { + assertMessages('a', 'b', 'c', 'd', 'e'); + }); + }); + + it('testFramesWaitToCompleteForPendingRejections', function() { + return new NativePromise(function(fulfill, reject) { + + promise.controlFlow().execute(function() { + promise.rejected(new StubError); + }).then(fulfill, reject); + + }). + then(() => fail('expected to fail'), assertIsStubError); + }); + + it('testSynchronizeErrorsPropagateToOuterFlow', function() { + var outerFlow = new promise.ControlFlow; + var innerFlow = new promise.ControlFlow; + + var block = NativePromise.defer(); + innerFlow.execute(function() { + return block.promise; + }, 'block inner flow'); + + outerFlow.execute(function() { + block.resolve(); + return innerFlow.execute(function() { + promise.rejected(new StubError); + }, 'trigger unhandled rejection error'); + }, 'run test'); + + return NativePromise.all([ + waitForIdle(innerFlow), + waitForAbort(outerFlow).then(assertIsStubError) + ]); + }); + + it('testFailsIfErrbackThrows', function() { + promise.rejected('').then(null, throwStubError); + return waitForAbort().then(assertIsStubError); + }); + + it('testFailsIfCallbackReturnsRejectedPromise', function() { + promise.fulfilled().then(function() { + return promise.rejected(new StubError); + }); + return waitForAbort().then(assertIsStubError); + }); + + it('testAbortsFrameIfTaskFails', function() { + promise.fulfilled().then(function() { + promise.controlFlow().execute(throwStubError); + }); + return waitForAbort().then(assertIsStubError); + }); + + it('testAbortsFramePromisedChainedFromTaskIsNotHandled', function() { + promise.fulfilled().then(function() { + promise.controlFlow().execute(function() {}). + then(throwStubError); + }); + return waitForAbort().then(assertIsStubError); + }); + + it('testTrapsChainedUnhandledRejectionsWithinAFrame', function() { + var pair = callbackPair(null, assertIsStubError); + promise.fulfilled().then(function() { + promise.controlFlow().execute(function() {}). + then(throwStubError); + }).then(pair.callback, pair.errback); + + return waitForIdle().then(pair.assertErrback); + }); + + it('testCancelsRemainingTasksIfFrameThrowsDuringScheduling', function() { + var task1, task2; + var pair = callbackPair(null, assertIsStubError); + var flow = promise.controlFlow(); + flow.execute(function() { + task1 = flow.execute(function() {}); + task2 = flow.execute(function() {}); + throw new StubError; + }).then(pair.callback, pair.errback); + + return waitForIdle(). + then(pair.assertErrback). + then(function() { + assert.ok(!task1.isPending()); + pair = callbackPair(); + return task1.then(pair.callback, pair.errback); + }). + then(function() { + pair.assertErrback(); + assert.ok(!task2.isPending()); + pair = callbackPair(); + return task2.then(pair.callback, pair.errback); + }). + then(function() { + pair.assertErrback(); + }); + }); + + it('testCancelsRemainingTasksInFrameIfATaskFails', function() { + var task; + var pair = callbackPair(null, assertIsStubError); + var flow = promise.controlFlow(); + flow.execute(function() { + flow.execute(throwStubError); + task = flow.execute(function() {}); + }).then(pair.callback, pair.errback); + + return waitForIdle().then(pair.assertErrback).then(function() { + assert.ok(!task.isPending()); + pair = callbackPair(); + task.then(pair.callback, pair.errback); + }).then(function() { + pair.assertErrback(); + }); + }); + + it('testDoesNotModifyRejectionErrorIfPromiseNotInsideAFlow', function() { + var error = Error('original message'); + var originalStack = error.stack; + var originalStr = error.toString(); + + var pair = callbackPair(null, function(e) { + assert.equal(error, e); + assert.equal('original message', e.message); + assert.equal(originalStack, e.stack); + assert.equal(originalStr, e.toString()); + }); + + promise.rejected(error).then(pair.callback, pair.errback); + return waitForIdle().then(pair.assertErrback); + }); + + /** See https://github.com/SeleniumHQ/selenium/issues/444 */ + it('testMaintainsOrderWithPromiseChainsCreatedWithinAForeach_1', function() { + var messages = []; + flow.execute(function() { + return promise.fulfilled(['a', 'b', 'c', 'd']); + }, 'start').then(function(steps) { + steps.forEach(function(step) { + promise.fulfilled(step) + .then(function() { + messages.push(step + '.1'); + }).then(function() { + messages.push(step + '.2'); + }); + }) + }); + return waitForIdle().then(function() { + assert.deepEqual( + ['a.1', 'a.2', 'b.1', 'b.2', 'c.1', 'c.2', 'd.1', 'd.2'], + messages); + }); + }); + + /** See https://github.com/SeleniumHQ/selenium/issues/444 */ + it('testMaintainsOrderWithPromiseChainsCreatedWithinAForeach_2', function() { + var messages = []; + flow.execute(function() { + return promise.fulfilled(['a', 'b', 'c', 'd']); + }, 'start').then(function(steps) { + steps.forEach(function(step) { + promise.fulfilled(step) + .then(function() { + messages.push(step + '.1'); + }).then(function() { + flow.execute(function() {}, step + '.2').then(function() { + messages.push(step + '.2'); + }); + }); + }) + }); + return waitForIdle().then(function() { + assert.deepEqual( + ['a.1', 'a.2', 'b.1', 'b.2', 'c.1', 'c.2', 'd.1', 'd.2'], + messages); + }); + }); + + /** See https://github.com/SeleniumHQ/selenium/issues/444 */ + it('testMaintainsOrderWithPromiseChainsCreatedWithinAForeach_3', function() { + var messages = []; + flow.execute(function() { + return promise.fulfilled(['a', 'b', 'c', 'd']); + }, 'start').then(function(steps) { + steps.forEach(function(step) { + promise.fulfilled(step) + .then(function(){}) + .then(function() { + messages.push(step + '.1'); + return flow.execute(function() {}, step + '.1'); + }).then(function() { + flow.execute(function() {}, step + '.2').then(function(text) { + messages.push(step + '.2'); + }); + }); + }) + }); + return waitForIdle().then(function() { + assert.deepEqual( + ['a.1', 'a.2', 'b.1', 'b.2', 'c.1', 'c.2', 'd.1', 'd.2'], + messages); + }); + }); + + /** See https://github.com/SeleniumHQ/selenium/issues/363 */ + it('testTasksScheduledInASeparateTurnOfTheEventLoopGetASeparateTaskQueue_2', function() { + scheduleAction('a', () => promise.delayed(10)); + schedule('b'); + setTimeout(() => schedule('c'), 0); + + return waitForIdle().then(function() { + assertFlowHistory('a', 'c', 'b'); + }); + }); + + /** See https://github.com/SeleniumHQ/selenium/issues/363 */ + it('testTasksScheduledInASeparateTurnOfTheEventLoopGetASeparateTaskQueue_2', function() { + scheduleAction('a', () => promise.delayed(10)); + schedule('b'); + schedule('c'); + setTimeout(function() { + schedule('d'); + scheduleAction('e', () => promise.delayed(10)); + schedule('f'); + }, 0); + + return waitForIdle().then(function() { + assertFlowHistory('a', 'd', 'e', 'b', 'c', 'f'); + }); + }); + + /** See https://github.com/SeleniumHQ/selenium/issues/363 */ + it('testCanSynchronizeTasksFromAdjacentTaskQueues', function() { + var task1 = scheduleAction('a', () => promise.delayed(10)); + schedule('b'); + setTimeout(function() { + scheduleAction('c', () => task1); + schedule('d'); + }, 0); + + return waitForIdle().then(function() { + assertFlowHistory('a', 'c', 'd', 'b'); + }); + }); + + describe('testCancellingAScheduledTask', function() { + it('1', function() { + var called = false; + var task1 = scheduleAction('a', () => called = true); + task1.cancel('no soup for you'); + + return waitForIdle().then(function() { + assert.ok(!called); + assertFlowHistory(); + return task1.catch(function(e) { + assert.ok(e instanceof promise.CancellationError); + assert.equal('no soup for you', e.message); + }); + }); + }); + + it('2', function() { + schedule('a'); + var called = false; + var task2 = scheduleAction('b', () => called = true); + schedule('c'); + + task2.cancel('no soup for you'); + + return waitForIdle().then(function() { + assert.ok(!called); + assertFlowHistory('a', 'c'); + return task2.catch(function(e) { + assert.ok(e instanceof promise.CancellationError); + assert.equal('no soup for you', e.message); + }); + }); + }); + + it('3', function() { + var called = false; + var task = scheduleAction('a', () => called = true); + task.cancel(new StubError); + + return waitForIdle().then(function() { + assert.ok(!called); + assertFlowHistory(); + return task.catch(function(e) { + assert.ok(e instanceof promise.CancellationError); + }); + }); + }); + + it('4', function() { + var seen = []; + var task = scheduleAction('a', () => seen.push(1)) + .then(() => seen.push(2)) + .then(() => seen.push(3)) + .then(() => seen.push(4)) + .then(() => seen.push(5)); + task.cancel(new StubError); + + return waitForIdle().then(function() { + assert.deepEqual([], seen); + assertFlowHistory(); + return task.catch(function(e) { + assert.ok(e instanceof promise.CancellationError); + }); + }); + }); + + it('fromWithinAnExecutingTask', function() { + var called = false; + var task; + scheduleAction('a', function() { + task.cancel('no soup for you'); + }); + task = scheduleAction('b', () => called = true); + schedule('c'); + + return waitForIdle().then(function() { + assert.ok(!called); + assertFlowHistory('a', 'c'); + return task.catch(function(e) { + assert.ok(e instanceof promise.CancellationError); + assert.equal('no soup for you', e.message); + }); + }); + }); + }); + + it('testCancellingAPendingTask', function() { + var order = []; + var unresolved = promise.defer(); + + var innerTask; + var outerTask = scheduleAction('a', function() { + order.push(1); + + // Schedule a task that will never finish. + innerTask = scheduleAction('a.1', function() { + return unresolved.promise; + }); + + // Since the outerTask is cancelled below, innerTask should be cancelled + // with a DiscardedTaskError, which means its callbacks are silently + // dropped - so this should never execute. + innerTask.catch(function(e) { + order.push(2); + }); + }); + schedule('b'); + + outerTask.catch(function(e) { + order.push(3); + assert.ok(e instanceof promise.CancellationError); + assert.equal('no soup for you', e.message); + }); + + unresolved.promise.catch(function(e) { + order.push(4); + assert.ok(e instanceof promise.CancellationError); + }); + + return timeout(10).then(function() { + assert.deepEqual([1], order); + assert.ok(unresolved.promise.isPending()); + + outerTask.cancel('no soup for you'); + return waitForIdle(); + }).then(function() { + assertFlowHistory('a', 'a.1', 'b'); + assert.deepEqual([1, 3, 4], order); + }); + }); + + it('testCancellingAPendingPromiseCallback', function() { + var called = false; + + var root = promise.fulfilled(); + root.then(function() { + cb2.cancel('no soup for you'); + }); + + var cb2 = root.then(fail, fail); // These callbacks should never be called. + cb2.then(fail, function(e) { + called = true; + assert.ok(e instanceof promise.CancellationError); + assert.equal('no soup for you', e.message); + }); + + return waitForIdle().then(function() { + assert.ok(called); + }); + }); + + describe('testResetFlow', function() { + it('1', function() { + var called = 0; + var task = flow.execute(() => called++); + task.finally(() => called++); + + return new Promise(function(fulfill) { + flow.once('reset', fulfill); + flow.reset(); + + }).then(function() { + assert.equal(0, called); + assert.ok(!task.isPending()); + return task; + + }).then(fail, function(e) { + assert.ok(e instanceof promise.CancellationError); + assert.equal('ControlFlow was reset', e.message); + }); + }); + + it('2', function() { + var called = 0; + var task1 = flow.execute(() => called++); + task1.finally(() => called++); + + var task2 = flow.execute(() => called++); + task2.finally(() => called++); + + var task3 = flow.execute(() => called++); + task3.finally(() => called++); + + return new Promise(function(fulfill) { + flow.once('reset', fulfill); + flow.reset(); + + }).then(function() { + assert.equal(0, called); + assert.ok(!task1.isPending()); + assert.ok(!task2.isPending()); + assert.ok(!task3.isPending()); + }); + }); + }); + + describe('testPromiseFulfilledInsideTask', function() { + it('1', function() { + var order = []; + + flow.execute(function() { + var d = promise.defer(); + + d.promise.then(() => order.push('a')); + d.promise.then(() => order.push('b')); + d.promise.then(() => order.push('c')); + d.fulfill(); + + flow.execute(() => order.push('d')); + + }).then(() => order.push('fin')); + + return waitForIdle().then(function() { + assert.deepEqual(['a', 'b', 'c', 'd', 'fin'], order); + }); + }); + + it('2', function() { + var order = []; + + flow.execute(function() { + flow.execute(() => order.push('a')); + flow.execute(() => order.push('b')); + + var d = promise.defer(); + d.promise.then(() => order.push('c')); + d.promise.then(() => order.push('d')); + d.fulfill(); + + flow.execute(() => order.push('e')); + + }).then(() => order.push('fin')); + + return waitForIdle().then(function() { + assert.deepEqual(['a', 'b', 'c', 'd', 'e', 'fin'], order); + }); + }); + + it('3', function() { + var order = []; + var d = promise.defer(); + d.promise.then(() => order.push('c')); + d.promise.then(() => order.push('d')); + + flow.execute(function() { + flow.execute(() => order.push('a')); + flow.execute(() => order.push('b')); + + d.promise.then(() => order.push('e')); + d.fulfill(); + + flow.execute(() => order.push('f')); + + }).then(() => order.push('fin')); + + return waitForIdle().then(function() { + assert.deepEqual(['c', 'd', 'a', 'b', 'e', 'f', 'fin'], order); + }); + }); + + it('4', function() { + var order = []; + var d = promise.defer(); + d.promise.then(() => order.push('a')); + d.promise.then(() => order.push('b')); + + flow.execute(function() { + flow.execute(function() { + order.push('c'); + flow.execute(() => order.push('d')); + d.promise.then(() => order.push('e')); + }); + flow.execute(() => order.push('f')); + + d.promise.then(() => order.push('g')); + d.fulfill(); + + flow.execute(() => order.push('h')); + + }).then(() => order.push('fin')); + + return waitForIdle().then(function() { + assert.deepEqual(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'fin'], order); + }); + }); + }); + + describe('testSettledPromiseCallbacksInsideATask', function() { + it('1', function() { + var order = []; + var p = promise.fulfilled(); + + flow.execute(function() { + flow.execute(() => order.push('a')); + p.then(() => order.push('b')); + flow.execute(() => order.push('c')); + p.then(() => order.push('d')); + }).then(() => order.push('fin')); + + return waitForIdle().then(function() { + assert.deepEqual(['a', 'b', 'c', 'd', 'fin'], order); + }); + }); + + it('2', function() { + var order = []; + + flow.execute(function() { + flow.execute(() => order.push('a')) + .then( () => order.push('c')); + flow.execute(() => order.push('b')); + }).then(() => order.push('fin')); + + return waitForIdle().then(function() { + assert.deepEqual(['a', 'c', 'b', 'fin'], order); + }); + }); + }); + + it('testTasksDoNotWaitForNewlyCreatedPromises', function() { + var order = []; + + flow.execute(function() { + var d = promise.defer(); + + // This is a normal promise, not a task, so the task for this callback is + // considered volatile. Volatile tasks should be skipped when they reach + // the front of the task queue. + d.promise.then(() => order.push('a')); + + flow.execute(() => order.push('b')); + flow.execute(function() { + flow.execute(() => order.push('c')); + d.promise.then(() => order.push('d')); + d.fulfill(); + }); + flow.execute(() => order.push('e')); + + }).then(() => order.push('fin')); + + return waitForIdle().then(function() { + assert.deepEqual(['b', 'a', 'c', 'd', 'e', 'fin'], order); + }); + }); + + it('testCallbackDependenciesDoNotDeadlock', function() { + var order = []; + var root = promise.defer(); + var dep = promise.fulfilled().then(function() { + order.push('a'); + return root.promise.then(function() { + order.push('b'); + }); + }); + // This callback depends on |dep|, which depends on another callback + // attached to |root| via a chain. + root.promise.then(function() { + order.push('c'); + return dep.then(() => order.push('d')); + }).then(() => order.push('fin')); + + setTimeout(() => root.fulfill(), 20); + + return waitForIdle().then(function() { + assert.deepEqual(['a', 'b', 'c', 'd', 'fin'], order); + }); + }); +}); diff --git a/node_modules/selenium-webdriver/test/lib/promise_generator_test.js b/node_modules/selenium-webdriver/test/lib/promise_generator_test.js new file mode 100644 index 000000000..5fdff9f80 --- /dev/null +++ b/node_modules/selenium-webdriver/test/lib/promise_generator_test.js @@ -0,0 +1,308 @@ +// 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. + +'use strict'; + +const assert = require('assert'); +const promise = require('../../lib/promise'); + +describe('promise.consume()', function() { + it('requires inputs to be generator functions', function() { + assert.throws(function() { + promise.consume(function() {}); + }); + }); + + it('handles a basic generator with no yielded promises', function() { + var values = []; + return promise.consume(function* () { + var i = 0; + while (i < 4) { + i = yield i + 1; + values.push(i); + } + }).then(function() { + assert.deepEqual([1, 2, 3, 4], values); + }); + }); + + it('handles a promise yielding generator', function() { + var values = []; + return promise.consume(function* () { + var i = 0; + while (i < 4) { + // Test that things are actually async here. + setTimeout(function() { + values.push(i * 2); + }, 10); + + yield promise.delayed(10).then(function() { + values.push(i++); + }); + } + }).then(function() { + assert.deepEqual([0, 0, 2, 1, 4, 2, 6, 3], values); + }); + }); + + it('assignemnts to yielded promises get fulfilled value', function() { + return promise.consume(function* () { + var p = promise.fulfilled(2); + var x = yield p; + assert.equal(2, x); + }); + }); + + it('is possible to cancel promise generators', function() { + var values = []; + var p = promise.consume(function* () { + var i = 0; + while (i < 3) { + yield promise.delayed(100).then(function() { + values.push(i++); + }); + } + }); + return promise.delayed(75).then(function() { + p.cancel(); + return p.catch(function() { + return promise.delayed(300); + }); + }).then(function() { + assert.deepEqual([0], values); + }); + }); + + it('uses final return value as fulfillment value', function() { + return promise.consume(function* () { + yield 1; + yield 2; + return 3; + }).then(function(value) { + assert.equal(3, value); + }); + }); + + it('throws rejected promise errors within the generator', function() { + var values = []; + return promise.consume(function* () { + values.push('a'); + var e = Error('stub error'); + try { + yield promise.rejected(e); + values.push('b'); + } catch (ex) { + assert.equal(e, ex); + values.push('c'); + } + values.push('d'); + }).then(function() { + assert.deepEqual(['a', 'c', 'd'], values); + }); + }); + + it('aborts the generator if there is an unhandled rejection', function() { + var values = []; + var e = Error('stub error'); + return promise.consume(function* () { + values.push(1); + yield promise.rejected(e); + values.push(2); + }).catch(function() { + assert.deepEqual([1], values); + }); + }); + + it('yield waits for promises', function() { + var values = []; + var d = promise.defer(); + + setTimeout(function() { + assert.deepEqual([1], values); + d.fulfill(2); + }, 100); + + return promise.consume(function* () { + values.push(1); + values.push((yield d.promise), 3); + }).then(function() { + assert.deepEqual([1, 2, 3], values); + }); + }); + + it('accepts custom scopes', function() { + return promise.consume(function* () { + return this.name; + }, {name: 'Bob'}).then(function(value) { + assert.equal('Bob', value); + }); + }); + + it('accepts initial generator arguments', function() { + return promise.consume(function* (a, b) { + assert.equal('red', a); + assert.equal('apples', b); + }, null, 'red', 'apples'); + }); + + it('executes generator within the control flow', function() { + var promises = [ + promise.defer(), + promise.defer() + ]; + var values = []; + + setTimeout(function() { + assert.deepEqual([], values); + promises[0].fulfill(1); + }, 100); + + setTimeout(function() { + assert.deepEqual([1], values); + promises[1].fulfill(2); + }, 200); + + return promise.controlFlow().execute(function* () { + values.push(yield promises[0].promise); + values.push(yield promises[1].promise); + values.push('fin'); + }).then(function() { + assert.deepEqual([1, 2, 'fin'], values); + }); + }); + + it('handles tasks scheduled in generator', function() { + var flow = promise.controlFlow(); + return flow.execute(function* () { + var x = yield flow.execute(function() { + return promise.delayed(10).then(function() { + return 1; + }); + }); + + var y = yield flow.execute(function() { + return 2; + }); + + return x + y; + }).then(function(value) { + assert.equal(3, value); + }); + }); + + it('blocks the control flow while processing generator', function() { + var values = []; + return promise.controlFlow().wait(function* () { + yield values.push(1); + values.push(yield promise.delayed(10).then(function() { + return 2; + })); + yield values.push(3); + return values.length === 6; + }, 250).then(function() { + assert.deepEqual([1, 2, 3, 1, 2, 3], values); + }); + }); + + it('ControlFlow.wait() will timeout on long generator', function() { + var values = []; + return promise.controlFlow().wait(function* () { + var i = 0; + while (i < 3) { + yield promise.delayed(100).then(function() { + values.push(i++); + }); + } + }, 75).catch(function() { + assert.deepEqual( + [0, 1, 2], values, 'Should complete one loop of wait condition'); + }); + }); + + describe('generators in promise callbacks', function() { + it('works with no initial value', function() { + var promises = [ + promise.defer(), + promise.defer() + ]; + var values = []; + + setTimeout(function() { + promises[0].fulfill(1); + }, 50); + + setTimeout(function() { + promises[1].fulfill(2); + }, 100); + + return promise.fulfilled().then(function*() { + values.push(yield promises[0].promise); + values.push(yield promises[1].promise); + values.push('fin'); + }).then(function() { + assert.deepEqual([1, 2, 'fin'], values); + }); + }); + + it('starts the generator with promised value', function() { + var promises = [ + promise.defer(), + promise.defer() + ]; + var values = []; + + setTimeout(function() { + promises[0].fulfill(1); + }, 50); + + setTimeout(function() { + promises[1].fulfill(2); + }, 100); + + return promise.fulfilled(3).then(function*(value) { + var p1 = yield promises[0].promise; + var p2 = yield promises[1].promise; + values.push(value + p1); + values.push(value + p2); + values.push('fin'); + }).then(function() { + assert.deepEqual([4, 5, 'fin'], values); + }); + }); + + it('throws yielded rejections within the generator callback', function() { + var d = promise.defer(); + var e = Error('stub'); + + setTimeout(function() { + d.reject(e); + }, 50); + + return promise.fulfilled().then(function*() { + var threw = false; + try { + yield d.promise; + } catch (ex) { + threw = true; + assert.equal(e, ex); + } + assert.ok(threw); + }); + }); + }); +}); + diff --git a/node_modules/selenium-webdriver/test/lib/promise_test.js b/node_modules/selenium-webdriver/test/lib/promise_test.js new file mode 100644 index 000000000..51554ecd9 --- /dev/null +++ b/node_modules/selenium-webdriver/test/lib/promise_test.js @@ -0,0 +1,1067 @@ +// 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. + +'use strict'; + +const assert = require('assert'); + +const testutil = require('./testutil'); +const promise = require('../../lib/promise'); + +// Aliases for readability. +const NativePromise = Promise; +const StubError = testutil.StubError; +const assertIsStubError = testutil.assertIsStubError; +const callbackHelper = testutil.callbackHelper; +const callbackPair = testutil.callbackPair; +const throwStubError = testutil.throwStubError; +const fail = () => assert.fail(); + +// Refer to promise_aplus_test for promise compliance with standard behavior. +describe('promise', function() { + var app, uncaughtExceptions; + + beforeEach(function setUp() { + promise.LONG_STACK_TRACES = false; + uncaughtExceptions = []; + + app = promise.controlFlow(); + app.on(promise.ControlFlow.EventType.UNCAUGHT_EXCEPTION, + (e) => uncaughtExceptions.push(e)); + }); + + afterEach(function tearDown() { + app.reset(); + promise.setDefaultFlow(new promise.ControlFlow); + assert.deepEqual([], uncaughtExceptions, + 'Did not expect any uncaught exceptions'); + promise.LONG_STACK_TRACES = false; + }); + + const assertIsPromise = (p) => assert.ok(promise.isPromise(p)); + const assertNotPromise = (v) => assert.ok(!promise.isPromise(v)); + + function createRejectedPromise(reason) { + var p = promise.rejected(reason); + p.catch(function() {}); + return p; + } + + it('testCanDetectPromiseLikeObjects', function() { + assertIsPromise(new promise.Promise(function(fulfill) { + fulfill(); + })); + assertIsPromise(new promise.Deferred().promise); + assertIsPromise({then:function() {}}); + + assertNotPromise(new promise.Deferred()); + assertNotPromise(undefined); + assertNotPromise(null); + assertNotPromise(''); + assertNotPromise(true); + assertNotPromise(false); + assertNotPromise(1); + assertNotPromise({}); + assertNotPromise({then:1}); + assertNotPromise({then:true}); + assertNotPromise({then:''}); + }); + + describe('then', function() { + it('returnsOwnPromiseIfNoCallbacksWereGiven', function() { + var deferred = new promise.Deferred(); + assert.equal(deferred.promise, deferred.promise.then()); + assert.equal(deferred.promise, deferred.promise.catch()); + assert.equal(deferred.promise, promise.when(deferred.promise)); + }); + + it('stillConsideredUnHandledIfNoCallbacksWereGivenOnCallsToThen', function() { + promise.rejected(new StubError).then(); + var handler = callbackHelper(assertIsStubError); + + // so tearDown() doesn't throw + app.removeAllListeners(); + app.on(promise.ControlFlow.EventType.UNCAUGHT_EXCEPTION, handler); + return NativePromise.resolve() + // Macro yield so the uncaught exception has a chance to trigger. + .then(() => new NativePromise(resolve => setTimeout(resolve, 0))) + .then(() => handler.assertCalled()); + }); + }); + + describe('finally', function() { + it('nonFailingCallbackDoesNotSuppressOriginalError', function() { + var done = callbackHelper(assertIsStubError); + return promise.rejected(new StubError). + finally(function() {}). + catch(done). + finally(done.assertCalled); + }); + + it('failingCallbackSuppressesOriginalError', function() { + var done = callbackHelper(assertIsStubError); + return promise.rejected(new Error('original')). + finally(throwStubError). + catch(done). + finally(done.assertCalled); + }); + + it('callbackThrowsAfterFulfilledPromise', function() { + var done = callbackHelper(assertIsStubError); + return promise.fulfilled(). + finally(throwStubError). + catch(done). + finally(done.assertCalled); + }); + + it('callbackReturnsRejectedPromise', function() { + var done = callbackHelper(assertIsStubError); + return promise.fulfilled(). + finally(function() { + return promise.rejected(new StubError); + }). + catch(done). + finally(done.assertCalled); + }); + }); + + describe('cancel', function() { + it('passesTheCancellationReasonToReject', function() { + var d = new promise.Deferred(); + var res = d.promise.then(assert.fail, function(e) { + assert.ok(e instanceof promise.CancellationError); + assert.equal('because i said so', e.message); + }); + d.promise.cancel('because i said so'); + return res; + }); + + describe('can cancel original promise from its child;', function() { + it('child created by then()', function() { + var d = new promise.Deferred(); + var p = d.promise.then(assert.fail, function(e) { + assert.ok(e instanceof promise.CancellationError); + assert.equal('because i said so', e.message); + return 123; + }); + + p.cancel('because i said so'); + return p.then(v => assert.equal(123, v)); + }); + + it('child linked by resolving with parent', function() { + let parent = promise.defer(); + let child = new promise.Promise(resolve => resolve(parent.promise)); + child.cancel('all done'); + + return parent.promise.then( + () => assert.fail('expected a cancellation'), + e => { + assert.ok(e instanceof promise.CancellationError); + assert.equal('all done', e.message); + }); + }); + + it('grand child through thenable chain', function() { + let p = new promise.Promise(function() {/* never resolve*/}); + + let noop = function() {}; + let gc = p.then(noop).then(noop).then(noop); + gc.cancel('stop!'); + + return p.then( + () => assert.fail('expected to be cancelled'), + (e) => { + assert.ok(e instanceof promise.CancellationError); + assert.equal('stop!', e.message); + }); + }); + + it('grand child through thenable chain started at resolve', function() { + function noop() {} + + let parent = promise.defer(); + let child = new promise.Promise(resolve => resolve(parent.promise)); + let grandChild = child.then(noop).then(noop).then(noop); + grandChild.cancel('all done'); + + return parent.promise.then( + () => assert.fail('expected a cancellation'), + e => { + assert.ok(e instanceof promise.CancellationError); + assert.equal('all done', e.message); + }); + }); + + it('"parent" is a Thenable', function() { + function noop() {} + + class FakeThenable { + constructor(p) { + this.promise = p; + } + + cancel(reason) { + this.promise.cancel(reason); + } + + then(cb, eb) { + let result = this.promise.then(cb, eb); + return new FakeThenable(result); + } + } + promise.Thenable.addImplementation(FakeThenable); + + let root = new promise.Promise(noop); + let thenable = new FakeThenable(root); + assert.ok(promise.Thenable.isImplementation(thenable)); + + let child = new promise.Promise(resolve => resolve(thenable)); + assert.ok(child instanceof promise.Promise); + child.cancel('stop!'); + + function assertStopped(p) { + return p.then( + () => assert.fail('not stopped!'), + (e) => { + assert.ok(e instanceof promise.CancellationError); + assert.equal('stop!', e.message); + }); + } + + return assertStopped(child).then(() => assertStopped(root)); + }); + }); + + it('canCancelATimeout', function() { + var p = promise.delayed(25) + .then(assert.fail, (e) => e instanceof promise.CancellationError); + setTimeout(() => p.cancel(), 20); + p.cancel(); + return p; + }); + + it('can cancel timeout from grandchild', function() { + }); + + it('cancelIsANoopOnceAPromiseHasBeenFulfilled', function() { + var p = promise.fulfilled(123); + p.cancel(); + return p.then((v) => assert.equal(123, v)); + }); + + it('cancelIsANoopOnceAPromiseHasBeenRejected', function() { + var p = promise.rejected(new StubError); + p.cancel(); + + var pair = callbackPair(null, assertIsStubError); + return p.then(assert.fail, assertIsStubError); + }); + + it('noopCancelTriggeredOnCallbackOfResolvedPromise', function() { + var d = promise.defer(); + var p = d.promise.then(); + + d.fulfill(); + p.cancel(); // This should not throw. + return p; // This should not trigger a failure. + }); + }); + + describe('when', function() { + it('ReturnsAResolvedPromiseIfGivenANonPromiseValue', function() { + var ret = promise.when('abc'); + assertIsPromise(ret); + return ret.then((value) => assert.equal('abc', value)); + }); + + it('PassesRawErrorsToCallbacks', function() { + var error = new Error('boo!'); + return promise.when(error, function(value) { + assert.equal(error, value); + }); + }); + + it('WaitsForValueToBeResolvedBeforeInvokingCallback', function() { + var d = new promise.Deferred(), callback; + let result = promise.when(d.promise, callback = callbackHelper(function(value) { + assert.equal('hi', value); + })); + callback.assertNotCalled(); + d.fulfill('hi'); + return result.then(callback.assertCalled); + }); + }); + + it('firesUncaughtExceptionEventIfRejectionNeverHandled', function() { + promise.rejected(new StubError); + var handler = callbackHelper(assertIsStubError); + + // so tearDown() doesn't throw + app.removeAllListeners(); + app.on(promise.ControlFlow.EventType.UNCAUGHT_EXCEPTION, handler); + + return NativePromise.resolve() + // Macro yield so the uncaught exception has a chance to trigger. + .then(() => new NativePromise(resolve => setTimeout(resolve, 0))) + .then(handler.assertCalled); + }); + + it('cannotResolveADeferredWithItself', function() { + var deferred = new promise.Deferred(); + assert.throws(() => deferred.fulfill(deferred)); + assert.throws(() => deferred.reject(deferred)); + }); + + describe('fullyResolved', function() { + it('primitives', function() { + function runTest(value) { + var callback, errback; + return promise.fullyResolved(value) + .then((resolved) => assert.equal(value, resolved)); + } + return runTest(true) + .then(() => runTest(function() {})) + .then(() => runTest(null)) + .then(() => runTest(123)) + .then(() => runTest('foo bar')) + .then(() => runTest(undefined)); + }); + + it('arrayOfPrimitives', function() { + var fn = function() {}; + var array = [true, fn, null, 123, '', undefined, 1]; + return promise.fullyResolved(array).then(function(resolved) { + assert.equal(array, resolved); + assert.deepEqual([true, fn, null, 123, '', undefined, 1], + resolved); + }); + }); + + it('nestedArrayOfPrimitives', function() { + var fn = function() {}; + var array = [true, [fn, null, 123], '', undefined]; + return promise.fullyResolved(array) + .then(function(resolved) { + assert.equal(array, resolved); + assert.deepEqual([true, [fn, null, 123], '', undefined], resolved); + assert.deepEqual([fn, null, 123], resolved[1]); + }); + }); + + it('arrayWithPromisedPrimitive', function() { + return promise.fullyResolved([promise.fulfilled(123)]) + .then(function(resolved) { + assert.deepEqual([123], resolved); + }); + }); + + it('promiseResolvesToPrimitive', function() { + return promise.fullyResolved(promise.fulfilled(123)) + .then((resolved) => assert.equal(123, resolved)); + }); + + it('promiseResolvesToArray', function() { + var fn = function() {}; + var array = [true, [fn, null, 123], '', undefined]; + var aPromise = promise.fulfilled(array); + + var result = promise.fullyResolved(aPromise); + return result.then(function(resolved) { + assert.equal(array, resolved); + assert.deepEqual([true, [fn, null, 123], '', undefined], + resolved); + assert.deepEqual([fn, null, 123], resolved[1]); + }); + }); + + it('promiseResolvesToArrayWithPromises', function() { + var nestedPromise = promise.fulfilled(123); + var aPromise = promise.fulfilled([true, nestedPromise]); + return promise.fullyResolved(aPromise) + .then(function(resolved) { + assert.deepEqual([true, 123], resolved); + }); + }); + + it('rejectsIfArrayPromiseRejects', function() { + var nestedPromise = createRejectedPromise(new StubError); + var aPromise = promise.fulfilled([true, nestedPromise]); + + var pair = callbackPair(null, assertIsStubError); + return promise.fullyResolved(aPromise) + .then(assert.fail, assertIsStubError); + }); + + it('rejectsOnFirstArrayRejection', function() { + var e1 = new Error('foo'); + var e2 = new Error('bar'); + var aPromise = promise.fulfilled([ + createRejectedPromise(e1), + createRejectedPromise(e2) + ]); + + return promise.fullyResolved(aPromise) + .then(assert.fail, function(error) { + assert.strictEqual(e1, error); + }); + }); + + it('rejectsIfNestedArrayPromiseRejects', function() { + var aPromise = promise.fulfilled([ + promise.fulfilled([ + createRejectedPromise(new StubError) + ]) + ]); + + return promise.fullyResolved(aPromise) + .then(assert.fail, assertIsStubError); + }); + + it('simpleHash', function() { + var hash = {'a': 123}; + return promise.fullyResolved(hash) + .then(function(resolved) { + assert.strictEqual(hash, resolved); + assert.deepEqual(hash, {'a': 123}); + }); + }); + + it('nestedHash', function() { + var nestedHash = {'foo':'bar'}; + var hash = {'a': 123, 'b': nestedHash}; + + return promise.fullyResolved(hash) + .then(function(resolved) { + assert.strictEqual(hash, resolved); + assert.deepEqual({'a': 123, 'b': {'foo': 'bar'}}, resolved); + assert.strictEqual(nestedHash, resolved['b']); + }); + }); + + it('promiseResolvesToSimpleHash', function() { + var hash = {'a': 123}; + var aPromise = promise.fulfilled(hash); + + return promise.fullyResolved(aPromise) + .then((resolved) => assert.strictEqual(hash, resolved)); + }); + + it('promiseResolvesToNestedHash', function() { + var nestedHash = {'foo':'bar'}; + var hash = {'a': 123, 'b': nestedHash}; + var aPromise = promise.fulfilled(hash); + + return promise.fullyResolved(aPromise) + .then(function(resolved) { + assert.strictEqual(hash, resolved); + assert.strictEqual(nestedHash, resolved['b']); + assert.deepEqual(hash, {'a': 123, 'b': {'foo': 'bar'}}); + }); + }); + + it('promiseResolvesToHashWithPromises', function() { + var aPromise = promise.fulfilled({ + 'a': promise.fulfilled(123) + }); + + return promise.fullyResolved(aPromise) + .then(function(resolved) { + assert.deepEqual({'a': 123}, resolved); + }); + }); + + it('rejectsIfHashPromiseRejects', function() { + var aPromise = promise.fulfilled({ + 'a': createRejectedPromise(new StubError) + }); + + return promise.fullyResolved(aPromise) + .then(assert.fail, assertIsStubError); + }); + + it('rejectsIfNestedHashPromiseRejects', function() { + var aPromise = promise.fulfilled({ + 'a': {'b': createRejectedPromise(new StubError)} + }); + + return promise.fullyResolved(aPromise) + .then(assert.fail, assertIsStubError); + }); + + it('instantiatedObject', function() { + function Foo() { + this.bar = 'baz'; + } + var foo = new Foo; + + return promise.fullyResolved(foo).then(function(resolvedFoo) { + assert.equal(foo, resolvedFoo); + assert.ok(resolvedFoo instanceof Foo); + assert.deepEqual(new Foo, resolvedFoo); + }); + }); + + it('withEmptyArray', function() { + return promise.fullyResolved([]).then(function(resolved) { + assert.deepEqual([], resolved); + }); + }); + + it('withEmptyHash', function() { + return promise.fullyResolved({}).then(function(resolved) { + assert.deepEqual({}, resolved); + }); + }); + + it('arrayWithPromisedHash', function() { + var obj = {'foo': 'bar'}; + var array = [promise.fulfilled(obj)]; + + return promise.fullyResolved(array).then(function(resolved) { + assert.deepEqual(resolved, [obj]); + }); + }); + }); + + describe('checkedNodeCall', function() { + it('functionThrows', function() { + return promise.checkedNodeCall(throwStubError) + .then(assert.fail, assertIsStubError); + }); + + it('functionReturnsAnError', function() { + return promise.checkedNodeCall(function(callback) { + callback(new StubError); + }).then(assert.fail, assertIsStubError); + }); + + it('functionReturnsSuccess', function() { + var success = 'success!'; + return promise.checkedNodeCall(function(callback) { + callback(null, success); + }).then((value) => assert.equal(success, value)); + }); + + it('functionReturnsAndThrows', function() { + var error = new Error('boom'); + var error2 = new Error('boom again'); + return promise.checkedNodeCall(function(callback) { + callback(error); + throw error2; + }).then(assert.fail, (e) => assert.equal(error, e)); + }); + + it('functionThrowsAndReturns', function() { + var error = new Error('boom'); + var error2 = new Error('boom again'); + return promise.checkedNodeCall(function(callback) { + setTimeout(() => callback(error), 10); + throw error2; + }).then(assert.fail, (e) => assert.equal(error2, e)); + }); + }); + + describe('all', function() { + it('(base case)', function() { + let defer = [promise.defer(), promise.defer()]; + var a = [ + 0, 1, + defer[0].promise, + defer[1].promise, + 4, 5, 6 + ]; + delete a[5]; + + var pair = callbackPair(function(value) { + assert.deepEqual([0, 1, 2, 3, 4, undefined, 6], value); + }); + + var result = promise.all(a).then(pair.callback, pair.errback); + pair.assertNeither(); + + defer[0].fulfill(2); + pair.assertNeither(); + + defer[1].fulfill(3); + return result.then(() => pair.assertCallback()); + }); + + it('empty array', function() { + return promise.all([]).then((a) => assert.deepEqual([], a)); + }); + + it('usesFirstRejection', function() { + let defer = [promise.defer(), promise.defer()]; + let a = [defer[0].promise, defer[1].promise]; + + var result = promise.all(a).then(assert.fail, assertIsStubError); + defer[1].reject(new StubError); + setTimeout(() => defer[0].reject(Error('ignored')), 0); + return result; + }); + }); + + describe('map', function() { + it('(base case)', function() { + var a = [1, 2, 3]; + return promise.map(a, function(value, index, a2) { + assert.equal(a, a2); + assert.equal('number', typeof index, 'not a number'); + return value + 1; + }).then(function(value) { + assert.deepEqual([2, 3, 4], value); + }); + }); + + it('omitsDeleted', function() { + var a = [0, 1, 2, 3, 4, 5, 6]; + delete a[1]; + delete a[3]; + delete a[4]; + delete a[6]; + + var expected = [0, 1, 4, 9, 16, 25, 36]; + delete expected[1]; + delete expected[3]; + delete expected[4]; + delete expected[6]; + + return promise.map(a, function(value) { + return value * value; + }).then(function(value) { + assert.deepEqual(expected, value); + }); + }); + + it('emptyArray', function() { + return promise.map([], function(value) { + return value + 1; + }).then(function(value) { + assert.deepEqual([], value); + }); + }); + + it('inputIsPromise', function() { + var input = promise.defer(); + var result = promise.map(input.promise, function(value) { + return value + 1; + }); + + var pair = callbackPair(function(value) { + assert.deepEqual([2, 3, 4], value); + }); + result = result.then(pair.callback, pair.errback); + + setTimeout(function() { + pair.assertNeither(); + input.fulfill([1, 2, 3]); + }, 10); + + return result; + }); + + it('waitsForFunctionResultToResolve', function() { + var innerResults = [ + promise.defer(), + promise.defer() + ]; + + var result = promise.map([1, 2], function(value, index) { + return innerResults[index].promise; + }); + + var pair = callbackPair(function(value) { + assert.deepEqual(['a', 'b'], value); + }); + result = result.then(pair.callback, pair.errback); + + return NativePromise.resolve() + .then(function() { + pair.assertNeither(); + innerResults[0].fulfill('a'); + }) + .then(function() { + pair.assertNeither(); + innerResults[1].fulfill('b'); + return result; + }) + .then(pair.assertCallback); + }); + + it('rejectsPromiseIfFunctionThrows', function() { + return promise.map([1], throwStubError) + .then(assert.fail, assertIsStubError); + }); + + it('rejectsPromiseIfFunctionReturnsRejectedPromise', function() { + return promise.map([1], function() { + return promise.rejected(new StubError); + }).then(assert.fail, assertIsStubError); + }); + + it('stopsCallingFunctionIfPreviousIterationFailed', function() { + var count = 0; + return promise.map([1, 2, 3, 4], function() { + count++; + if (count == 3) { + throw new StubError; + } + }).then(assert.fail, function(e) { + assertIsStubError(e); + assert.equal(3, count); + }); + }); + + it('rejectsWithFirstRejectedPromise', function() { + var innerResult = [ + promise.fulfilled(), + createRejectedPromise(new StubError), + createRejectedPromise(Error('should be ignored')) + ]; + var count = 0; + return promise.map([1, 2, 3, 4], function(value, index) { + count += 1; + return innerResult[index]; + }).then(assert.fail, function(e) { + assertIsStubError(e); + assert.equal(2, count); + }); + }); + + it('preservesOrderWhenMapReturnsPromise', function() { + var deferreds = [ + promise.defer(), + promise.defer(), + promise.defer(), + promise.defer() + ]; + var result = promise.map(deferreds, function(value) { + return value.promise; + }); + + var pair = callbackPair(function(value) { + assert.deepEqual([0, 1, 2, 3], value); + }); + result = result.then(pair.callback, pair.errback); + + return NativePromise.resolve() + .then(function() { + pair.assertNeither(); + for (let i = deferreds.length; i > 0; i -= 1) { + deferreds[i - 1].fulfill(i - 1); + } + return result; + }).then(pair.assertCallback); + }); + }); + + describe('filter', function() { + it('basicFiltering', function() { + var a = [0, 1, 2, 3]; + return promise.filter(a, function(val, index, a2) { + assert.equal(a, a2); + assert.equal('number', typeof index, 'not a number'); + return val > 1; + }).then(function(val) { + assert.deepEqual([2, 3], val); + }); + }); + + it('omitsDeleted', function() { + var a = [0, 1, 2, 3, 4, 5, 6]; + delete a[3]; + delete a[4]; + + return promise.filter(a, function(value) { + return value > 1 && value < 6; + }).then(function(val) { + assert.deepEqual([2, 5], val); + }); + }); + + it('preservesInputs', function() { + var a = [0, 1, 2, 3]; + + return promise.filter(a, function(value, i, a2) { + assert.equal(a, a2); + // Even if a function modifies the input array, the original value + // should be inserted into the new array. + a2[i] = a2[i] - 1; + return a2[i] >= 1; + }).then(function(val) { + assert.deepEqual([2, 3], val); + }); + }); + + it('inputIsPromise', function() { + var input = promise.defer(); + var result = promise.filter(input.promise, function(value) { + return value > 1 && value < 3; + }); + + var pair = callbackPair(function(value) { + assert.deepEqual([2], value); + }); + result = result.then(pair.callback, pair.errback); + return NativePromise.resolve() + .then(function() { + pair.assertNeither(); + input.fulfill([1, 2, 3]); + return result; + }) + .then(pair.assertCallback); + }); + + it('waitsForFunctionResultToResolve', function() { + var innerResults = [ + promise.defer(), + promise.defer() + ]; + + var result = promise.filter([1, 2], function(value, index) { + return innerResults[index].promise; + }); + + var pair = callbackPair(function(value) { + assert.deepEqual([2], value); + }); + result = result.then(pair.callback, pair.errback); + return NativePromise.resolve() + .then(function() { + pair.assertNeither(); + innerResults[0].fulfill(false); + }) + .then(function() { + pair.assertNeither(); + innerResults[1].fulfill(true); + return result; + }) + .then(pair.assertCallback); + }); + + it('rejectsPromiseIfFunctionReturnsRejectedPromise', function() { + return promise.filter([1], function() { + return promise.rejected(new StubError); + }).then(assert.fail, assertIsStubError); + }); + + it('stopsCallingFunctionIfPreviousIterationFailed', function() { + var count = 0; + return promise.filter([1, 2, 3, 4], function() { + count++; + if (count == 3) { + throw new StubError; + } + }).then(assert.fail, function(e) { + assertIsStubError(e); + assert.equal(3, count); + }); + }); + + it('rejectsWithFirstRejectedPromise', function() { + var innerResult = [ + promise.fulfilled(), + createRejectedPromise(new StubError), + createRejectedPromise(Error('should be ignored')) + ]; + + return promise.filter([1, 2, 3, 4], function(value, index) { + assert.ok(index < innerResult.length); + return innerResult[index]; + }).then(assert.fail, assertIsStubError); + }); + + it('preservesOrderWhenFilterReturnsPromise', function() { + var deferreds = [ + promise.defer(), + promise.defer(), + promise.defer(), + promise.defer() + ]; + var result = promise.filter([0, 1, 2, 3], function(value, index) { + return deferreds[index].promise; + }); + + var pair = callbackPair(function(value) { + assert.deepEqual([1, 2], value); + }); + result = result.then(pair.callback, pair.errback); + + return NativePromise.resolve() + .then(function() { + pair.assertNeither(); + for (let i = deferreds.length - 1; i >= 0; i -= 1) { + deferreds[i].fulfill(i > 0 && i < 3); + } + return result; + }).then(pair.assertCallback); + }); + }); + + it('testAddThenableImplementation', function() { + function tmp() {} + assert.ok(!promise.Thenable.isImplementation(new tmp())); + promise.Thenable.addImplementation(tmp); + assert.ok(promise.Thenable.isImplementation(new tmp())); + + class tmpClass {} + assert.ok(!promise.Thenable.isImplementation(new tmpClass())); + promise.Thenable.addImplementation(tmpClass); + assert.ok(promise.Thenable.isImplementation(new tmpClass())); + }); + + describe('testLongStackTraces', function() { + beforeEach(() => promise.LONG_STACK_TRACES = false); + afterEach(() => promise.LONG_STACK_TRACES = false); + + it('doesNotAppendStackIfFeatureDisabled', function() { + promise.LONG_STACK_TRACES = false; + + var error = Error('hello'); + var originalStack = error.stack; + return promise.rejected(error). + then(fail). + then(fail). + then(fail). + then(fail, function(e) { + assert.equal(error, e); + assert.equal(originalStack, e.stack); + }); + }); + + function getStackMessages(error) { + return error.stack.split(/\n/).filter(function(line) { + return /^From: /.test(line); + }); + } + + it('appendsInitialPromiseCreation_resolverThrows', function() { + promise.LONG_STACK_TRACES = true; + + var error = Error('hello'); + var originalStack = '(placeholder; will be overwritten later)'; + + return new promise.Promise(function() { + try { + throw error; + } catch (e) { + originalStack = e.stack; + throw e; + } + }).then(fail, function(e) { + assert.strictEqual(error, e); + if (typeof originalStack !== 'string') { + return; + } + assert.notEqual(originalStack, e.stack); + assert.equal(e.stack.indexOf(originalStack), 0, + 'should start with original stack'); + assert.deepEqual(['From: ManagedPromise: new'], getStackMessages(e)); + }); + }); + + it('appendsInitialPromiseCreation_rejectCalled', function() { + promise.LONG_STACK_TRACES = true; + + var error = Error('hello'); + var originalStack = error.stack; + + return new promise.Promise(function(_, reject) { + reject(error); + }).then(fail, function(e) { + assert.equal(error, e); + if (typeof originalStack !== 'string') { + return; + } + assert.notEqual(originalStack, e.stack); + assert.equal(e.stack.indexOf(originalStack), 0, + 'should start with original stack'); + assert.deepEqual(['From: ManagedPromise: new'], getStackMessages(e)); + }); + }); + + it('appendsEachStepToRejectionError', function() { + promise.LONG_STACK_TRACES = true; + + var error = Error('hello'); + var originalStack = '(placeholder; will be overwritten later)'; + + return new promise.Promise(function() { + try { + throw error; + } catch (e) { + originalStack = e.stack; + throw e; + } + }). + then(fail). + catch(function(e) { throw e; }). + then(fail). + catch(function(e) { throw e; }). + then(fail, function(e) { + assert.equal(error, e); + if (typeof originalStack !== 'string') { + return; + } + assert.notEqual(originalStack, e.stack); + assert.equal(e.stack.indexOf(originalStack), 0, + 'should start with original stack'); + assert.deepEqual([ + 'From: ManagedPromise: new', + 'From: Promise: then', + 'From: Promise: catch', + 'From: Promise: then', + 'From: Promise: catch', + ], getStackMessages(e)); + }); + }); + + it('errorOccursInCallbackChain', function() { + promise.LONG_STACK_TRACES = true; + + var error = Error('hello'); + var originalStack = '(placeholder; will be overwritten later)'; + + return promise.fulfilled(). + then(function() {}). + then(function() {}). + then(function() { + try { + throw error; + } catch (e) { + originalStack = e.stack; + throw e; + } + }). + catch(function(e) { throw e; }). + then(fail, function(e) { + assert.equal(error, e); + if (typeof originalStack !== 'string') { + return; + } + assert.notEqual(originalStack, e.stack); + assert.equal(e.stack.indexOf(originalStack), 0, + 'should start with original stack'); + assert.deepEqual([ + 'From: Promise: then', + 'From: Promise: catch', + ], getStackMessages(e)); + }); + }); + }); +}); diff --git a/node_modules/selenium-webdriver/test/lib/testutil.js b/node_modules/selenium-webdriver/test/lib/testutil.js new file mode 100644 index 000000000..e68ca28ba --- /dev/null +++ b/node_modules/selenium-webdriver/test/lib/testutil.js @@ -0,0 +1,90 @@ +// 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. + +'use strict'; + +const assert = require('assert'); +const sinon = require('sinon'); + + +class StubError extends Error { + constructor(opt_msg) { + super(opt_msg); + this.name = this.constructor.name; + } +} +exports.StubError = StubError; + +exports.throwStubError = function() { + throw new StubError; +}; + +exports.assertIsStubError = function(value) { + assert.ok(value instanceof StubError, value + ' is not a ' + StubError.name); +}; + +exports.assertIsInstance = function(ctor, value) { + assert.ok(value instanceof ctor, 'Not a ' + ctor.name + ': ' + value); +}; + +function callbackPair(cb, eb) { + if (cb && eb) { + throw new TypeError('can only specify one of callback or errback'); + } + + let callback = cb ? sinon.spy(cb) : sinon.spy(); + let errback = eb ? sinon.spy(eb) : sinon.spy(); + + function assertCallback() { + assert.ok(callback.called, 'callback not called'); + assert.ok(!errback.called, 'errback called'); + if (callback.threw()) { + throw callback.exceptions[0]; + } + } + + function assertErrback() { + assert.ok(!callback.called, 'callback called'); + assert.ok(errback.called, 'errback not called'); + if (errback.threw()) { + throw errback.exceptions[0]; + } + } + + function assertNeither() { + assert.ok(!callback.called, 'callback called'); + assert.ok(!errback.called, 'errback called'); + } + + return { + callback, + errback, + assertCallback, + assertErrback, + assertNeither + }; +} +exports.callbackPair = callbackPair; + + +exports.callbackHelper = function(cb) { + let pair = callbackPair(cb); + let wrapped = pair.callback.bind(null); + wrapped.assertCalled = () => pair.assertCallback(); + wrapped.assertNotCalled = () => pair.assertNeither(); + return wrapped; +}; diff --git a/node_modules/selenium-webdriver/test/lib/until_test.js b/node_modules/selenium-webdriver/test/lib/until_test.js new file mode 100644 index 000000000..d3e81ec18 --- /dev/null +++ b/node_modules/selenium-webdriver/test/lib/until_test.js @@ -0,0 +1,463 @@ +// 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. + +'use strict'; + +const assert = require('assert'); + +const By = require('../../lib/by').By; +const CommandName = require('../../lib/command').Name; +const error = require('../../lib/error'); +const promise = require('../../lib/promise'); +const until = require('../../lib/until'); +const webdriver = require('../../lib/webdriver'), + WebElement = webdriver.WebElement; + +describe('until', function() { + let driver, executor; + + class TestExecutor { + constructor() { + this.handlers_ = {}; + } + + on(cmd, handler) { + this.handlers_[cmd] = handler; + return this; + } + + execute(cmd) { + let self = this; + return Promise.resolve().then(function() { + if (!self.handlers_[cmd.getName()]) { + throw new error.UnknownCommandError(cmd.getName()); + } + return self.handlers_[cmd.getName()](cmd); + }); + } + } + + function fail(opt_msg) { + throw new assert.AssertionError({message: opt_msg}); + } + + beforeEach(function setUp() { + executor = new TestExecutor(); + driver = new webdriver.WebDriver('session-id', executor); + }); + + describe('ableToSwitchToFrame', function() { + it('failsFastForNonSwitchErrors', function() { + let e = Error('boom'); + executor.on(CommandName.SWITCH_TO_FRAME, function() { + throw e; + }); + return driver.wait(until.ableToSwitchToFrame(0), 100) + .then(fail, (e2) => assert.strictEqual(e2, e)); + }); + + it('byIndex', function() { + executor.on(CommandName.SWITCH_TO_FRAME, () => true); + return driver.wait(until.ableToSwitchToFrame(0), 100); + }); + + it('byWebElement', function() { + executor.on(CommandName.SWITCH_TO_FRAME, () => true); + var el = new webdriver.WebElement(driver, {ELEMENT: 1234}); + return driver.wait(until.ableToSwitchToFrame(el), 100); + }); + + it('byWebElementPromise', function() { + executor.on(CommandName.SWITCH_TO_FRAME, () => true); + var el = new webdriver.WebElementPromise(driver, + promise.fulfilled(new webdriver.WebElement(driver, {ELEMENT: 1234}))); + return driver.wait(until.ableToSwitchToFrame(el), 100); + }); + + it('byLocator', function() { + executor.on(CommandName.FIND_ELEMENTS, () => [WebElement.buildId(1234)]); + executor.on(CommandName.SWITCH_TO_FRAME, () => true); + return driver.wait(until.ableToSwitchToFrame(By.id('foo')), 100); + }); + + it('byLocator_elementNotInitiallyFound', function() { + var foundResponses = [[], [], [WebElement.buildId(1234)]]; + executor.on(CommandName.FIND_ELEMENTS, () => foundResponses.shift()); + executor.on(CommandName.SWITCH_TO_FRAME, () => true); + + return driver.wait(until.ableToSwitchToFrame(By.id('foo')), 2000) + .then(function() { + assert.equal(foundResponses.length, 0); + }); + }); + + it('timesOutIfNeverAbletoSwitchFrames', function() { + var count = 0; + executor.on(CommandName.SWITCH_TO_FRAME, function() { + count += 1; + throw new error.NoSuchFrameError; + }); + + return driver.wait(until.ableToSwitchToFrame(0), 100) + .then(fail, function(e) { + assert.ok(count > 0); + assert.ok( + e.message.startsWith('Waiting to be able to switch to frame'), + 'Wrong message: ' + e.message); + }); + }); + }); + + describe('alertIsPresent', function() { + it('failsFastForNonAlertSwitchErrors', function() { + return driver.wait(until.alertIsPresent(), 100).then(fail, function(e) { + assert.ok(e instanceof error.UnknownCommandError); + assert.equal(e.message, CommandName.GET_ALERT_TEXT); + }); + }); + + it('waitsForAlert', function() { + var count = 0; + executor.on(CommandName.GET_ALERT_TEXT, function() { + if (count++ < 3) { + throw new error.NoSuchAlertError; + } else { + return true; + } + }).on(CommandName.DISMISS_ALERT, () => true); + + return driver.wait(until.alertIsPresent(), 1000).then(function(alert) { + assert.equal(count, 4); + return alert.dismiss(); + }); + }); + + // TODO: Remove once GeckoDriver doesn't throw this unwanted error. + // See https://github.com/SeleniumHQ/selenium/pull/2137 + describe('workaround for GeckoDriver', function() { + it('doesNotFailWhenCannotConvertNullToObject', function() { + var count = 0; + executor.on(CommandName.GET_ALERT_TEXT, function() { + if (count++ < 3) { + throw new error.WebDriverError(`can't convert null to object`); + } else { + return true; + } + }).on(CommandName.DISMISS_ALERT, () => true); + + return driver.wait(until.alertIsPresent(), 1000).then(function(alert) { + assert.equal(count, 4); + return alert.dismiss(); + }); + }); + + it('keepsRaisingRegularWebdriverError', function() { + var webDriverError = new error.WebDriverError; + + executor.on(CommandName.GET_ALERT_TEXT, function() { + throw webDriverError; + }); + + return driver.wait(until.alertIsPresent(), 1000).then(function() { + throw new Error('driver did not fail against WebDriverError'); + }, function(error) { + assert.equal(error, webDriverError); + }); + }) + }); + }); + + it('testUntilTitleIs', function() { + var titles = ['foo', 'bar', 'baz']; + executor.on(CommandName.GET_TITLE, () => titles.shift()); + + return driver.wait(until.titleIs('bar'), 3000).then(function() { + assert.deepStrictEqual(titles, ['baz']); + }); + }); + + it('testUntilTitleContains', function() { + var titles = ['foo', 'froogle', 'google']; + executor.on(CommandName.GET_TITLE, () => titles.shift()); + + return driver.wait(until.titleContains('oogle'), 3000).then(function() { + assert.deepStrictEqual(titles, ['google']); + }); + }); + + it('testUntilTitleMatches', function() { + var titles = ['foo', 'froogle', 'aaaabc', 'aabbbc', 'google']; + executor.on(CommandName.GET_TITLE, () => titles.shift()); + + return driver.wait(until.titleMatches(/^a{2,3}b+c$/), 3000) + .then(function() { + assert.deepStrictEqual(titles, ['google']); + }); + }); + + it('testUntilUrlIs', function() { + var urls = ['http://www.foo.com', 'https://boo.com', 'http://docs.yes.com']; + executor.on(CommandName.GET_CURRENT_URL, () => urls.shift()); + + return driver.wait(until.urlIs('https://boo.com'), 3000).then(function() { + assert.deepStrictEqual(urls, ['http://docs.yes.com']); + }); + }); + + it('testUntilUrlContains', function() { + var urls = + ['http://foo.com', 'https://groups.froogle.com', 'http://google.com']; + executor.on(CommandName.GET_CURRENT_URL, () => urls.shift()); + + return driver.wait(until.urlContains('oogle.com'), 3000).then(function() { + assert.deepStrictEqual(urls, ['http://google.com']); + }); + }); + + it('testUntilUrlMatches', function() { + var urls = ['foo', 'froogle', 'aaaabc', 'aabbbc', 'google']; + executor.on(CommandName.GET_CURRENT_URL, () => urls.shift()); + + return driver.wait(until.urlMatches(/^a{2,3}b+c$/), 3000) + .then(function() { + assert.deepStrictEqual(urls, ['google']); + }); + }); + + it('testUntilElementLocated', function() { + var responses = [ + [], + [WebElement.buildId('abc123'), WebElement.buildId('foo')], + ['end'] + ]; + executor.on(CommandName.FIND_ELEMENTS, () => responses.shift()); + + let element = driver.wait(until.elementLocated(By.id('quux')), 2000); + assert.ok(element instanceof webdriver.WebElementPromise); + return element.getId().then(function(id) { + assert.deepStrictEqual(responses, [['end']]); + assert.equal(id, 'abc123'); + }); + }); + + describe('untilElementLocated, elementNeverFound', function() { + function runNoElementFoundTest(locator, locatorStr) { + executor.on(CommandName.FIND_ELEMENTS, () => []); + + function expectedFailure() { + fail('expected condition to timeout'); + } + + return driver.wait(until.elementLocated(locator), 100) + .then(expectedFailure, function(error) { + var expected = 'Waiting for element to be located ' + locatorStr; + var lines = error.message.split(/\n/, 2); + assert.equal(lines[0], expected); + + let regex = /^Wait timed out after \d+ms$/; + assert.ok(regex.test(lines[1]), + `Lines <${lines[1]}> does not match ${regex}`); + }); + } + + it('byLocator', function() { + return runNoElementFoundTest( + By.id('quux'), 'By(css selector, *[id="quux"])'); + }); + + it('byHash', function() { + return runNoElementFoundTest( + {id: 'quux'}, 'By(css selector, *[id="quux"])'); + }); + + it('byFunction', function() { + return runNoElementFoundTest(function() {}, 'by function()'); + }); + }); + + it('testUntilElementsLocated', function() { + var responses = [ + [], + [WebElement.buildId('abc123'), WebElement.buildId('foo')], + ['end'] + ]; + executor.on(CommandName.FIND_ELEMENTS, () => responses.shift()); + + return driver.wait(until.elementsLocated(By.id('quux')), 2000) + .then(function(els) { + return Promise.all(els.map(e => e.getId())); + }).then(function(ids) { + assert.deepStrictEqual(responses, [['end']]); + assert.equal(ids.length, 2); + assert.equal(ids[0], 'abc123'); + assert.equal(ids[1], 'foo'); + }); + }); + + describe('untilElementsLocated, noElementsFound', function() { + function runNoElementsFoundTest(locator, locatorStr) { + executor.on(CommandName.FIND_ELEMENTS, () => []); + + function expectedFailure() { + fail('expected condition to timeout'); + } + + return driver.wait(until.elementsLocated(locator), 100) + .then(expectedFailure, function(error) { + var expected = + 'Waiting for at least one element to be located ' + locatorStr; + var lines = error.message.split(/\n/, 2); + assert.equal(lines[0], expected); + + let regex = /^Wait timed out after \d+ms$/; + assert.ok(regex.test(lines[1]), + `Lines <${lines[1]}> does not match ${regex}`); + }); + } + + it('byLocator', function() { + return runNoElementsFoundTest( + By.id('quux'), 'By(css selector, *[id="quux"])'); + }); + + it('byHash', function() { + return runNoElementsFoundTest( + {id: 'quux'}, 'By(css selector, *[id="quux"])'); + }); + + it('byFunction', function() { + return runNoElementsFoundTest(function() {}, 'by function()'); + }); + }); + + it('testUntilStalenessOf', function() { + let count = 0; + executor.on(CommandName.GET_ELEMENT_TAG_NAME, function() { + while (count < 3) { + count += 1; + return 'body'; + } + throw new error.StaleElementReferenceError('now stale'); + }); + + var el = new webdriver.WebElement(driver, {ELEMENT: 'foo'}); + return driver.wait(until.stalenessOf(el), 2000) + .then(() => assert.equal(count, 3)); + }); + + describe('element state conditions', function() { + function runElementStateTest(predicate, command, responses, var_args) { + let original = new webdriver.WebElement(driver, 'foo'); + let predicateArgs = [original]; + if (arguments.length > 3) { + predicateArgs = predicateArgs.concat(arguments[1]); + command = arguments[2]; + responses = arguments[3]; + } + + assert.ok(responses.length > 1); + + responses = responses.concat(['end']); + executor.on(command, () => responses.shift()); + + let result = driver.wait(predicate.apply(null, predicateArgs), 2000); + assert.ok(result instanceof webdriver.WebElementPromise); + return result.then(function(value) { + assert.ok(value instanceof webdriver.WebElement); + assert.ok(!(value instanceof webdriver.WebElementPromise)); + return value.getId(); + }).then(function(id) { + assert.equal('foo', id); + assert.deepStrictEqual(responses, ['end']); + }); + } + + it('elementIsVisible', function() { + return runElementStateTest( + until.elementIsVisible, + CommandName.IS_ELEMENT_DISPLAYED, [false, false, true]); + }); + + it('elementIsNotVisible', function() { + return runElementStateTest( + until.elementIsNotVisible, + CommandName.IS_ELEMENT_DISPLAYED, [true, true, false]); + }); + + it('elementIsEnabled', function() { + return runElementStateTest( + until.elementIsEnabled, + CommandName.IS_ELEMENT_ENABLED, [false, false, true]); + }); + + it('elementIsDisabled', function() { + return runElementStateTest( + until.elementIsDisabled, + CommandName.IS_ELEMENT_ENABLED, [true, true, false]); + }); + + it('elementIsSelected', function() { + return runElementStateTest( + until.elementIsSelected, + CommandName.IS_ELEMENT_SELECTED, [false, false, true]); + }); + + it('elementIsNotSelected', function() { + return runElementStateTest( + until.elementIsNotSelected, + CommandName.IS_ELEMENT_SELECTED, [true, true, false]); + }); + + it('elementTextIs', function() { + return runElementStateTest( + until.elementTextIs, 'foobar', + CommandName.GET_ELEMENT_TEXT, + ['foo', 'fooba', 'foobar']); + }); + + it('elementTextContains', function() { + return runElementStateTest( + until.elementTextContains, 'bar', + CommandName.GET_ELEMENT_TEXT, + ['foo', 'foobaz', 'foobarbaz']); + }); + + it('elementTextMatches', function() { + return runElementStateTest( + until.elementTextMatches, /fo+bar{3}/, + CommandName.GET_ELEMENT_TEXT, + ['foo', 'foobar', 'fooobarrr']); + }); + }); + + describe('WebElementCondition', function() { + it('fails if wait completes with a non-WebElement value', function() { + let result = driver.wait( + new webdriver.WebElementCondition('testing', () => 123), 1000); + + return result.then( + () => assert.fail('expected to fail'), + function(e) { + assert.ok(e instanceof TypeError); + assert.equal( + 'WebElementCondition did not resolve to a WebElement: ' + + '[object Number]', + e.message); + }); + }); + }); +}); diff --git a/node_modules/selenium-webdriver/test/lib/webdriver_test.js b/node_modules/selenium-webdriver/test/lib/webdriver_test.js new file mode 100644 index 000000000..d2d87ca3e --- /dev/null +++ b/node_modules/selenium-webdriver/test/lib/webdriver_test.js @@ -0,0 +1,2177 @@ +// 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. + +'use strict'; + +const testutil = require('./testutil'); + +const By = require('../../lib/by').By; +const Capabilities = require('../../lib/capabilities').Capabilities; +const Executor = require('../../lib/command').Executor; +const CName = require('../../lib/command').Name; +const error = require('../../lib/error'); +const Button = require('../../lib/input').Button; +const Key = require('../../lib/input').Key; +const logging = require('../../lib/logging'); +const Session = require('../../lib/session').Session; +const promise = require('../../lib/promise'); +const Alert = require('../../lib/webdriver').Alert; +const AlertPromise = require('../../lib/webdriver').AlertPromise; +const UnhandledAlertError = require('../../lib/webdriver').UnhandledAlertError; +const WebDriver = require('../../lib/webdriver').WebDriver; +const WebElement = require('../../lib/webdriver').WebElement; +const WebElementPromise = require('../../lib/webdriver').WebElementPromise; + +const assert = require('assert'); +const sinon = require('sinon'); + +const SESSION_ID = 'test_session_id'; + +// Aliases for readability. +const NativePromise = Promise; +const StubError = testutil.StubError; +const assertIsInstance = testutil.assertIsInstance; +const assertIsStubError = testutil.assertIsStubError; +const throwStubError = testutil.throwStubError; +const fail = (msg) => assert.fail(msg); + +describe('WebDriver', function() { + const LOG = logging.getLogger('webdriver.test'); + + // before(function() { + // logging.getLogger('webdriver').setLevel(logging.Level.ALL); + // logging.installConsoleHandler(); + // }); + + // after(function() { + // logging.getLogger('webdriver').setLevel(null); + // logging.removeConsoleHandler(); + // }); + + var driver; + var flow; + var uncaughtExceptions; + + beforeEach(function setUp() { + flow = promise.controlFlow(); + uncaughtExceptions = []; + flow.on('uncaughtException', onUncaughtException); + }); + + afterEach(function tearDown() { + return waitForIdle(flow).then(function() { + assert.deepEqual([], uncaughtExceptions); + flow.reset(); + }); + }); + + function onUncaughtException(e) { + uncaughtExceptions.push(e); + } + + function waitForIdle(opt_flow) { + var theFlow = opt_flow || flow; + return new Promise(function(fulfill, reject) { + if (theFlow.isIdle()) { + fulfill(); + return; + } + theFlow.once('idle', fulfill); + theFlow.once('uncaughtException', reject); + }); + } + + function waitForAbort(opt_flow, opt_n) { + var n = opt_n || 1; + var theFlow = opt_flow || flow; + theFlow.removeAllListeners( + promise.ControlFlow.EventType.UNCAUGHT_EXCEPTION); + return new Promise(function(fulfill, reject) { + theFlow.once('idle', function() { + reject(Error('expected flow to report an unhandled error')); + }); + + var errors = []; + theFlow.on('uncaughtException', onError); + function onError(e) { + errors.push(e); + if (errors.length === n) { + theFlow.removeListener('uncaughtException', onError); + fulfill(n === 1 ? errors[0] : errors); + } + } + }); + } + + function expectedError(ctor, message) { + return function(e) { + assertIsInstance(ctor, e); + assert.equal(message, e.message); + }; + } + + class Expectation { + constructor(executor, name, opt_parameters) { + this.executor_ = executor; + this.name_ = name; + this.times_ = 1; + this.sessionId_ = SESSION_ID; + this.check_ = null; + this.toDo_ = null; + this.withParameters(opt_parameters || {}); + } + + anyTimes() { + this.times_ = Infinity; + return this; + } + + times(n) { + this.times_ = n; + return this; + } + + withParameters(parameters) { + this.parameters_ = parameters; + if (this.name_ !== CName.NEW_SESSION) { + this.parameters_['sessionId'] = this.sessionId_; + } + return this; + } + + andReturn(code, opt_value) { + this.toDo_ = function(command) { + LOG.info('executing ' + command.getName() + '; returning ' + code); + return Promise.resolve(opt_value !== void(0) ? opt_value : null); + }; + return this; + } + + andReturnSuccess(opt_value) { + this.toDo_ = function(command) { + LOG.info('executing ' + command.getName() + '; returning success'); + return Promise.resolve(opt_value !== void(0) ? opt_value : null); + }; + return this; + } + + andReturnError(error) { + if (typeof error === 'number') { + throw Error('need error type'); + } + this.toDo_ = function(command) { + LOG.info('executing ' + command.getName() + '; returning failure'); + return Promise.reject(error); + }; + return this; + } + + expect(name, opt_parameters) { + this.end(); + return this.executor_.expect(name, opt_parameters); + } + + end() { + if (!this.toDo_) { + this.andReturnSuccess(null); + } + return this.executor_; + } + + execute(command) { + assert.deepEqual(this.parameters_, command.getParameters()); + return this.toDo_(command); + } + } + + class FakeExecutor { + constructor() { + this.commands_ = new Map; + } + + execute(command) { + let expectations = this.commands_.get(command.getName()); + if (!expectations || !expectations.length) { + assert.fail('unexpected command: ' + command.getName()); + return; + } + + let next = expectations[0]; + let result = next.execute(command); + if (next.times_ != Infinity) { + next.times_ -= 1; + if (!next.times_) { + expectations.shift(); + } + } + return result; + } + + expect(commandName, opt_parameters) { + if (!this.commands_.has(commandName)) { + this.commands_.set(commandName, []); + } + let e = new Expectation(this, commandName, opt_parameters); + this.commands_.get(commandName).push(e); + return e; + } + + createDriver(opt_session) { + let session = opt_session || new Session(SESSION_ID, {}); + return new WebDriver(session, this); + } + } + + + ///////////////////////////////////////////////////////////////////////////// + // + // Tests + // + ///////////////////////////////////////////////////////////////////////////// + + + describe('testAttachToSession', function() { + it('sessionIsAvailable', function() { + let aSession = new Session(SESSION_ID, {'browserName': 'firefox'}); + let executor = new FakeExecutor(). + expect(CName.DESCRIBE_SESSION). + withParameters({'sessionId': SESSION_ID}). + andReturnSuccess(aSession). + end(); + + let driver = WebDriver.attachToSession(executor, SESSION_ID); + return driver.getSession().then(v => assert.strictEqual(v, aSession)); + }); + + it('failsToGetSessionInfo', function() { + let e = new Error('boom'); + let executor = new FakeExecutor(). + expect(CName.DESCRIBE_SESSION). + withParameters({'sessionId': SESSION_ID}). + andReturnError(e). + end(); + + let driver = WebDriver.attachToSession(executor, SESSION_ID); + return driver.getSession() + .then(() => assert.fail('should have failed!'), + (actual) => assert.strictEqual(actual, e)); + }); + + it('remote end does not recognize DESCRIBE_SESSION command', function() { + let e = new error.UnknownCommandError; + let executor = new FakeExecutor(). + expect(CName.DESCRIBE_SESSION). + withParameters({'sessionId': SESSION_ID}). + andReturnError(e). + end(); + + let driver = WebDriver.attachToSession(executor, SESSION_ID); + return driver.getSession().then(session => { + assert.ok(session instanceof Session); + assert.strictEqual(session.getId(), SESSION_ID); + assert.equal(session.getCapabilities().size, 0); + }); + }); + + it('usesActiveFlowByDefault', function() { + let executor = new FakeExecutor(). + expect(CName.DESCRIBE_SESSION). + withParameters({'sessionId': SESSION_ID}). + andReturnSuccess({}). + end(); + + var driver = WebDriver.attachToSession(executor, SESSION_ID); + assert.equal(driver.controlFlow(), promise.controlFlow()); + + return waitForIdle(driver.controlFlow()); + }); + + it('canAttachInCustomFlow', function() { + let executor = new FakeExecutor(). + expect(CName.DESCRIBE_SESSION). + withParameters({'sessionId': SESSION_ID}). + andReturnSuccess({}). + end(); + + var otherFlow = new promise.ControlFlow(); + var driver = WebDriver.attachToSession(executor, SESSION_ID, otherFlow); + assert.equal(otherFlow, driver.controlFlow()); + assert.notEqual(otherFlow, promise.controlFlow()); + + return waitForIdle(otherFlow); + }); + }); + + describe('testCreateSession', function() { + it('happyPathWithCapabilitiesHashObject', function() { + let aSession = new Session(SESSION_ID, {'browserName': 'firefox'}); + let executor = new FakeExecutor(). + expect(CName.NEW_SESSION). + withParameters({ + 'desiredCapabilities': {'browserName': 'firefox'} + }). + andReturnSuccess(aSession). + end(); + + var driver = WebDriver.createSession(executor, { + 'browserName': 'firefox' + }); + return driver.getSession().then(v => assert.strictEqual(v, aSession)); + }); + + it('happyPathWithCapabilitiesInstance', function() { + let aSession = new Session(SESSION_ID, {'browserName': 'firefox'}); + let executor = new FakeExecutor(). + expect(CName.NEW_SESSION). + withParameters({'desiredCapabilities': {'browserName': 'firefox'}}). + andReturnSuccess(aSession). + end(); + + var driver = WebDriver.createSession(executor, Capabilities.firefox()); + return driver.getSession().then(v => assert.strictEqual(v, aSession)); + }); + + it('handles desired and requried capabilities', function() { + let aSession = new Session(SESSION_ID, {'browserName': 'firefox'}); + let executor = new FakeExecutor(). + expect(CName.NEW_SESSION). + withParameters({ + 'desiredCapabilities': {'foo': 'bar'}, + 'requiredCapabilities': {'bim': 'baz'} + }). + andReturnSuccess(aSession). + end(); + + let desired = new Capabilities().set('foo', 'bar'); + let required = new Capabilities().set('bim', 'baz'); + var driver = WebDriver.createSession(executor, {desired, required}); + return driver.getSession().then(v => assert.strictEqual(v, aSession)); + }); + + it('failsToCreateSession', function() { + let executor = new FakeExecutor(). + expect(CName.NEW_SESSION). + withParameters({'desiredCapabilities': {'browserName': 'firefox'}}). + andReturnError(new StubError()). + end(); + + var driver = + WebDriver.createSession(executor, {'browserName': 'firefox'}); + return driver.getSession().then(fail, assertIsStubError); + }); + + it('usesActiveFlowByDefault', function() { + let executor = new FakeExecutor(). + expect(CName.NEW_SESSION). + withParameters({'desiredCapabilities': {}}). + andReturnSuccess(new Session(SESSION_ID)). + end(); + + var driver = WebDriver.createSession(executor, {}); + assert.equal(promise.controlFlow(), driver.controlFlow()); + + return waitForIdle(driver.controlFlow()); + }); + + it('canCreateInCustomFlow', function() { + let executor = new FakeExecutor(). + expect(CName.NEW_SESSION). + withParameters({'desiredCapabilities': {}}). + andReturnSuccess({}). + end(); + + var otherFlow = new promise.ControlFlow(); + var driver = WebDriver.createSession(executor, {}, otherFlow); + assert.equal(otherFlow, driver.controlFlow()); + assert.notEqual(otherFlow, promise.controlFlow()); + + return waitForIdle(otherFlow); + }); + }); + + it('testDoesNotExecuteCommandIfSessionDoesNotResolve', function() { + var session = Promise.reject(new StubError); + new FakeExecutor().createDriver(session).getTitle(); + return waitForAbort().then(assertIsStubError); + }); + + it('testCommandReturnValuesArePassedToFirstCallback', function() { + let executor = new FakeExecutor(). + expect(CName.GET_TITLE).andReturnSuccess('Google Search'). + end(); + + var driver = executor.createDriver(); + return driver.getTitle().then(function(title) { + assert.equal('Google Search', title); + }); + }); + + it('testStopsCommandExecutionWhenAnErrorOccurs', function() { + let e = new error.NoSuchWindowError('window not found'); + let executor = new FakeExecutor(). + expect(CName.SWITCH_TO_WINDOW). + withParameters({ + 'name': 'foo', + 'handle': 'foo' + }). + andReturnError(e). + end(); + + var driver = executor.createDriver(); + driver.switchTo().window('foo'); + driver.getTitle(); // mock should blow if this gets executed + + return waitForAbort().then(v => assert.strictEqual(v, e)); + }); + + it('testCanSuppressCommandFailures', function() { + let e = new error.NoSuchWindowError('window not found'); + let executor = new FakeExecutor(). + expect(CName.SWITCH_TO_WINDOW). + withParameters({ + 'name': 'foo', + 'handle': 'foo' + }). + andReturnError(e). + expect(CName.GET_TITLE). + andReturnSuccess('Google Search'). + end(); + + var driver = executor.createDriver(); + driver.switchTo().window('foo') + .catch(v => assert.strictEqual(v, e)); + driver.getTitle(); + return waitForIdle(); + }); + + it('testErrorsPropagateUpToTheRunningApplication', function() { + let e = new error.NoSuchWindowError('window not found'); + let executor = new FakeExecutor(). + expect(CName.SWITCH_TO_WINDOW). + withParameters({ + 'name': 'foo', + 'handle': 'foo' + }). + andReturnError(e). + end(); + + executor.createDriver().switchTo().window('foo'); + return waitForAbort().then(v => assert.strictEqual(v, e)); + }); + + it('testErrbacksThatReturnErrorsStillSwitchToCallbackChain', function() { + let executor = new FakeExecutor(). + expect(CName.SWITCH_TO_WINDOW). + withParameters({ + 'name': 'foo', + 'handle': 'foo' + }). + andReturnError(new error.NoSuchWindowError('window not found')). + end(); + + var driver = executor.createDriver(); + return driver.switchTo().window('foo'). + catch(function() { return new StubError; }); + then(assertIsStubError); + }); + + it('testErrbacksThrownCanOverrideOriginalError', function() { + let executor = new FakeExecutor(). + expect(CName.SWITCH_TO_WINDOW, { + 'name': 'foo', + 'handle': 'foo' + }). + andReturnError(new error.NoSuchWindowError('window not found')). + end(); + + var driver = executor.createDriver(); + driver.switchTo().window('foo').catch(throwStubError); + + return waitForAbort().then(assertIsStubError); + }); + + it('testCannotScheduleCommandsIfTheSessionIdHasBeenDeleted', function() { + var driver = new FakeExecutor().createDriver(); + delete driver.session_; + assert.throws(() => driver.get('http://www.google.com')); + }); + + it('testDeletesSessionIdAfterQuitting', function() { + var driver; + let executor = new FakeExecutor(). + expect(CName.QUIT). + end(); + + driver = executor.createDriver(); + return driver.quit().then(function() { + assert.equal(void 0, driver.session_); + }); + }); + + it('testReportsErrorWhenExecutingCommandsAfterExecutingAQuit', function() { + let executor = new FakeExecutor(). + expect(CName.QUIT). + end(); + + var driver = executor.createDriver(); + driver.quit(); + driver.get('http://www.google.com'); + return waitForAbort(). + then(expectedError( + error.NoSuchSessionError, + 'This driver instance does not have a valid session ID ' + + '(did you call WebDriver.quit()?) and may no longer be used.')); + }); + + it('testCallbackCommandsExecuteBeforeNextCommand', function() { + let executor = new FakeExecutor(). + expect(CName.GET_CURRENT_URL). + expect(CName.GET, {'url': 'http://www.google.com'}). + expect(CName.CLOSE). + expect(CName.GET_TITLE). + end(); + + var driver = executor.createDriver(); + driver.getCurrentUrl().then(function() { + driver.get('http://www.google.com').then(function() { + driver.close(); + }); + }); + driver.getTitle(); + + return waitForIdle(); + }); + + it('testEachCallbackFrameRunsToCompletionBeforeTheNext', function() { + let executor = new FakeExecutor(). + expect(CName.GET_TITLE). + expect(CName.GET_CURRENT_URL). + expect(CName.GET_CURRENT_WINDOW_HANDLE). + expect(CName.CLOSE). + expect(CName.QUIT). + end(); + + var driver = executor.createDriver(); + driver.getTitle(). + // Everything in this callback... + then(function() { + driver.getCurrentUrl(); + driver.getWindowHandle(); + }). + // ...should execute before everything in this callback. + then(function() { + driver.close(); + }); + // This should execute after everything above + driver.quit(); + + return waitForIdle(); + }); + + describe('nestedCommandFailures', function() { + it('bubbleUpToGlobalHandlerIfUnsuppressed', function() { + let e = new error.NoSuchWindowError('window not found'); + let executor = new FakeExecutor(). + expect(CName.GET_TITLE). + expect(CName.SWITCH_TO_WINDOW, { + 'name': 'foo', + 'handle': 'foo' + }). + andReturnError(e). + end(); + + var driver = executor.createDriver(); + driver.getTitle().then(function() { + driver.switchTo().window('foo'); + }); + + return waitForAbort().then(v => assert.strictEqual(v, e)); + }); + + it('canBeSuppressWhenTheyOccur', function() { + let executor = new FakeExecutor(). + expect(CName.GET_TITLE). + expect(CName.SWITCH_TO_WINDOW, { + 'name': 'foo', + 'handle': 'foo' + }). + andReturnError(new error.NoSuchWindowError('window not found')). + expect(CName.CLOSE). + end(); + + var driver = executor.createDriver(); + driver.getTitle().then(function() { + driver.switchTo().window('foo').catch(function() {}); + }); + driver.close(); + + return waitForIdle(); + }); + + it('bubbleUpThroughTheFrameStack', function() { + let e = new error.NoSuchWindowError('window not found'); + let executor = new FakeExecutor(). + expect(CName.GET_TITLE). + expect(CName.SWITCH_TO_WINDOW, { + 'name': 'foo', + 'handle': 'foo' + }). + andReturnError(e). + end(); + + var driver = executor.createDriver(); + driver.getTitle(). + then(function() { + return driver.switchTo().window('foo'); + }). + catch(v => assert.strictEqual(v, e)); + + return waitForIdle(); + }); + + it('canBeCaughtAndSuppressed', function() { + let executor = new FakeExecutor(). + expect(CName.GET_TITLE). + expect(CName.GET_CURRENT_URL). + expect(CName.SWITCH_TO_WINDOW, { + 'name': 'foo', + 'handle': 'foo' + }). + andReturnError(new StubError()). + expect(CName.CLOSE). + end(); + + var driver = executor.createDriver(); + driver.getTitle().then(function() { + driver.getCurrentUrl(). + then(function() { + return driver.switchTo().window('foo'); + }). + catch(function() {}); + driver.close(); + }); + + return waitForIdle(); + }); + }); + + describe('returningAPromise', function() { + it('fromACallback', function() { + let executor = new FakeExecutor(). + expect(CName.GET_TITLE). + expect(CName.GET_CURRENT_URL). + andReturnSuccess('http://www.google.com'). + end(); + + var driver = executor.createDriver(); + driver.getTitle(). + then(function() { + return driver.getCurrentUrl(); + }). + then(function(value) { + assert.equal('http://www.google.com', value); + }); + return waitForIdle(); + }); + + it('fromAnErrbackSuppressesTheError', function() { + var count = 0; + let executor = new FakeExecutor(). + expect(CName.SWITCH_TO_WINDOW, { + 'name': 'foo', + 'handle': 'foo' + }). + andReturnError(new StubError()). + expect(CName.GET_CURRENT_URL). + andReturnSuccess('http://www.google.com'). + end(); + + var driver = executor.createDriver(); + driver.switchTo().window('foo'). + catch(function(e) { + assertIsStubError(e); + count += 1; + return driver.getCurrentUrl(); + }). + then(function(url) { + count += 1; + assert.equal('http://www.google.com', url); + }); + return waitForIdle().then(function() { + assert.equal(2, count); + }); + }); + }); + + describe('customFunctions', function() { + it('returnsANonPromiseValue', function() { + var driver = new FakeExecutor().createDriver(); + return driver.call(() => 'abc123').then(function(value) { + assert.equal('abc123', value); + }); + }); + + it('executionOrderWithCustomFunctions', function() { + var msg = []; + let executor = new FakeExecutor(). + expect(CName.GET_TITLE).andReturnSuccess('cheese '). + expect(CName.GET_CURRENT_URL).andReturnSuccess('tasty'). + end(); + + var driver = executor.createDriver(); + + var pushMsg = msg.push.bind(msg); + driver.getTitle().then(pushMsg); + driver.call(() => 'is ').then(pushMsg); + driver.getCurrentUrl().then(pushMsg); + driver.call(() => '!').then(pushMsg); + + return waitForIdle().then(function() { + assert.equal('cheese is tasty!', msg.join('')); + }); + }); + + it('passingArgumentsToACustomFunction', function() { + var add = function(a, b) { + return a + b; + }; + var driver = new FakeExecutor().createDriver(); + return driver.call(add, null, 1, 2).then(function(value) { + assert.equal(3, value); + }); + }); + + it('passingPromisedArgumentsToACustomFunction', function() { + var promisedArg = promise.fulfilled(2); + var add = function(a, b) { + return a + b; + }; + var driver = new FakeExecutor().createDriver(); + return driver.call(add, null, 1, promisedArg).then(function(value) { + assert.equal(3, value); + }); + }); + + it('passingArgumentsAndScopeToACustomFunction', function() { + function Foo(name) { + this.name = name; + } + Foo.prototype.getName = function() { + return this.name; + }; + var foo = new Foo('foo'); + + var driver = new FakeExecutor().createDriver(); + return driver.call(foo.getName, foo).then(function(value) { + assert.equal('foo', value); + }); + }); + + it('customFunctionThrowsAnError', function() { + var driver = new FakeExecutor().createDriver(); + return driver.call(throwStubError).then(fail, assertIsStubError); + }); + + it('customFunctionSchedulesCommands', function() { + let executor = new FakeExecutor(). + expect(CName.GET_TITLE). + expect(CName.CLOSE). + expect(CName.QUIT). + end(); + + var driver = executor.createDriver(); + driver.call(function() { + driver.getTitle(); + driver.close(); + }); + driver.quit(); + return waitForIdle(); + }); + + it('returnsATaskResultAfterSchedulingAnother', function() { + let executor = new FakeExecutor(). + expect(CName.GET_TITLE). + andReturnSuccess('Google Search'). + expect(CName.CLOSE). + end(); + + var driver = executor.createDriver(); + return driver.call(function() { + var title = driver.getTitle(); + driver.close(); + return title; + }).then(function(title) { + assert.equal('Google Search', title); + }); + }); + + it('hasANestedCommandThatFails', function() { + let executor = new FakeExecutor(). + expect(CName.SWITCH_TO_WINDOW, { + 'name': 'foo', + 'handle': 'foo' + }). + andReturnError(new StubError()). + end(); + + var driver = executor.createDriver(); + return driver.call(function() { + return driver.switchTo().window('foo'); + }).then(fail, assertIsStubError); + }); + + it('doesNotCompleteUntilReturnedPromiseIsResolved', function() { + var order = []; + var driver = new FakeExecutor().createDriver(); + + var d = promise.defer(); + d.promise.then(function() { + order.push('b'); + }); + + driver.call(function() { + order.push('a'); + return d.promise; + }); + driver.call(function() { + order.push('c'); + }); + + // timeout to ensure the first function starts its execution before we + // trigger d's callbacks. + return new Promise(f => setTimeout(f, 0)).then(function() { + assert.deepEqual(['a'], order); + d.fulfill(); + return waitForIdle().then(function() { + assert.deepEqual(['a', 'b', 'c'], order); + }); + }); + }); + + it('returnsADeferredAction', function() { + let executor = new FakeExecutor(). + expect(CName.GET_TITLE).andReturnSuccess('Google'). + end(); + + var driver = executor.createDriver(); + driver.call(function() { + return driver.getTitle(); + }).then(function(title) { + assert.equal('Google', title); + }); + return waitForIdle(); + }); + }); + + describe('nestedCommands', function() { + it('commandExecutionOrder', function() { + var msg = []; + var driver = new FakeExecutor().createDriver(); + driver.call(msg.push, msg, 'a'); + driver.call(function() { + driver.call(msg.push, msg, 'c'); + driver.call(function() { + driver.call(msg.push, msg, 'e'); + driver.call(msg.push, msg, 'f'); + }); + driver.call(msg.push, msg, 'd'); + }); + driver.call(msg.push, msg, 'b'); + return waitForIdle().then(function() { + assert.equal('acefdb', msg.join('')); + }); + }); + + it('basicUsage', function() { + var msg = []; + var driver = new FakeExecutor().createDriver(); + var pushMsg = msg.push.bind(msg); + driver.call(() => 'cheese ').then(pushMsg); + driver.call(function() { + driver.call(() => 'is ').then(pushMsg); + driver.call(() => 'tasty').then(pushMsg); + }); + driver.call(() => '!').then(pushMsg); + return waitForIdle().then(function() { + assert.equal('cheese is tasty!', msg.join('')); + }); + }); + + it('canReturnValueFromNestedFunction', function() { + var driver = new FakeExecutor().createDriver(); + return driver.call(function() { + return driver.call(function() { + return driver.call(() => 'foobar'); + }); + }).then(function(value) { + assert.equal('foobar', value); + }); + }); + + it('normalCommandAfterNestedCommandThatReturnsAnAction', function() { + var msg = []; + let executor = new FakeExecutor(). + expect(CName.CLOSE). + end(); + var driver = executor.createDriver(); + driver.call(function() { + return driver.call(function() { + msg.push('a'); + return driver.call(() => 'foobar'); + }); + }); + driver.close().then(function() { + msg.push('b'); + }); + return waitForIdle().then(function() { + assert.equal('ab', msg.join('')); + }); + }); + + it('errorsBubbleUp_caught', function() { + var driver = new FakeExecutor().createDriver(); + var result = driver.call(function() { + return driver.call(function() { + return driver.call(throwStubError); + }); + }).then(fail, assertIsStubError); + return Promise.all([waitForIdle(), result]); + }); + + it('errorsBubbleUp_uncaught', function() { + var driver = new FakeExecutor().createDriver(); + driver.call(function() { + return driver.call(function() { + return driver.call(throwStubError); + }); + }); + return waitForAbort().then(assertIsStubError); + }); + + it('canScheduleCommands', function() { + let executor = new FakeExecutor(). + expect(CName.GET_TITLE). + expect(CName.CLOSE). + end(); + + var driver = executor.createDriver(); + driver.call(function() { + driver.call(function() { + driver.getTitle(); + }); + driver.close(); + }); + return waitForIdle(); + }); + }); + + describe('WebElementPromise', function() { + it('resolvesWhenUnderlyingElementDoes', function() { + var el = new WebElement({}, {'ELEMENT': 'foo'}); + return new WebElementPromise({}, promise.fulfilled(el)).then(function(e) { + assert.strictEqual(e, el); + }); + }); + + it('resolvesBeforeCallbacksOnWireValueTrigger', function() { + var el = new promise.Deferred(); + + var element = new WebElementPromise({}, el.promise); + var messages = []; + + element.then(function() { + messages.push('element resolved'); + }); + element.getId().then(function() { + messages.push('wire value resolved'); + }); + + assert.deepEqual([], messages); + el.fulfill(new WebElement({}, {'ELEMENT': 'foo'})); + return waitForIdle().then(function() { + assert.deepEqual([ + 'element resolved', + 'wire value resolved' + ], messages); + }); + }); + + it('isRejectedIfUnderlyingIdIsRejected', function() { + var element = new WebElementPromise({}, promise.rejected(new StubError)); + return element.then(fail, assertIsStubError); + }); + }); + + describe('executeScript', function() { + it('nullReturnValue', function() { + let executor = new FakeExecutor(). + expect(CName.EXECUTE_SCRIPT). + withParameters({ + 'script': 'return document.body;', + 'args': [] + }). + andReturnSuccess(null). + end(); + + var driver = executor.createDriver(); + return driver.executeScript('return document.body;') + .then((result) => assert.equal(null, result)); + }); + + it('primitiveReturnValue', function() { + let executor = new FakeExecutor(). + expect(CName.EXECUTE_SCRIPT). + withParameters({ + 'script': 'return document.body;', + 'args': [] + }). + andReturnSuccess(123). + end(); + + var driver = executor.createDriver(); + return driver.executeScript('return document.body;') + .then((result) => assert.equal(123, result)); + }); + + it('webElementReturnValue', function() { + var json = WebElement.buildId('foo'); + + let executor = new FakeExecutor(). + expect(CName.EXECUTE_SCRIPT). + withParameters({ + 'script': 'return document.body;', + 'args': [] + }). + andReturnSuccess(json). + end(); + + var driver = executor.createDriver(); + return driver.executeScript('return document.body;') + .then((element) => element.getId()) + .then((id) => assert.equal(id, 'foo')); + }); + + it('arrayReturnValue', function() { + var json = [WebElement.buildId('foo')]; + + let executor = new FakeExecutor(). + expect(CName.EXECUTE_SCRIPT). + withParameters({ + 'script': 'return document.body;', + 'args': [] + }). + andReturnSuccess(json). + end(); + + var driver = executor.createDriver(); + return driver.executeScript('return document.body;') + .then(function(array) { + assert.equal(1, array.length); + return array[0].getId(); + }) + .then((id) => assert.equal('foo', id)); + }); + + it('objectReturnValue', function() { + var json = {'foo': WebElement.buildId('foo')}; + + let executor = new FakeExecutor(). + expect(CName.EXECUTE_SCRIPT). + withParameters({ + 'script': 'return document.body;', + 'args': [] + }). + andReturnSuccess(json). + end(); + + var driver = executor.createDriver(); + var callback; + return driver.executeScript('return document.body;') + .then((obj) => obj['foo'].getId()) + .then((id) => assert.equal(id, 'foo')); + }); + + it('scriptAsFunction', function() { + let executor = new FakeExecutor(). + expect(CName.EXECUTE_SCRIPT). + withParameters({ + 'script': 'return (' + function() {} + + ').apply(null, arguments);', + 'args': [] + }). + andReturnSuccess(null). + end(); + + var driver = executor.createDriver(); + return driver.executeScript(function() {}); + }); + + it('simpleArgumentConversion', function() { + let executor = new FakeExecutor(). + expect(CName.EXECUTE_SCRIPT). + withParameters({ + 'script': 'return 1;', + 'args': ['abc', 123, true, [123, {'foo': 'bar'}]] + }). + andReturnSuccess(null). + end(); + + var driver = executor.createDriver(); + return driver.executeScript( + 'return 1;', 'abc', 123, true, [123, {'foo': 'bar'}]); + }); + + it('webElementArgumentConversion', function() { + var elementJson = WebElement.buildId('fefifofum'); + + let executor = new FakeExecutor(). + expect(CName.EXECUTE_SCRIPT). + withParameters({ + 'script': 'return 1;', + 'args': [elementJson] + }). + andReturnSuccess(null). + end(); + + var driver = executor.createDriver(); + return driver.executeScript('return 1;', + new WebElement(driver, 'fefifofum')); + }); + + it('webElementPromiseArgumentConversion', function() { + var elementJson = WebElement.buildId('bar'); + + let executor = new FakeExecutor(). + expect(CName.FIND_ELEMENT, + {'using': 'css selector', 'value': '*[id="foo"]'}). + andReturnSuccess(elementJson). + expect(CName.EXECUTE_SCRIPT). + withParameters({ + 'script': 'return 1;', + 'args': [elementJson] + }). + andReturnSuccess(null). + end(); + + var driver = executor.createDriver(); + var element = driver.findElement(By.id('foo')); + return driver.executeScript('return 1;', element); + }); + + it('argumentConversion', function() { + var elementJson = WebElement.buildId('fefifofum'); + + let executor = new FakeExecutor(). + expect(CName.EXECUTE_SCRIPT). + withParameters({ + 'script': 'return 1;', + 'args': ['abc', 123, true, elementJson, [123, {'foo': 'bar'}]] + }). + andReturnSuccess(null). + end(); + + var driver = executor.createDriver(); + var element = new WebElement(driver, 'fefifofum'); + return driver.executeScript('return 1;', + 'abc', 123, true, element, [123, {'foo': 'bar'}]); + }); + + it('scriptReturnsAnError', function() { + let executor = new FakeExecutor(). + expect(CName.EXECUTE_SCRIPT). + withParameters({ + 'script': 'throw Error(arguments[0]);', + 'args': ['bam'] + }). + andReturnError(new StubError). + end(); + var driver = executor.createDriver(); + return driver.executeScript('throw Error(arguments[0]);', 'bam'). + then(fail, assertIsStubError); + }); + + it('failsIfArgumentIsARejectedPromise', function() { + let executor = new FakeExecutor(); + + var arg = promise.rejected(new StubError); + arg.catch(function() {}); // Suppress default handler. + + var driver = executor.createDriver(); + return driver.executeScript(function() {}, arg). + then(fail, assertIsStubError); + }); + }); + + describe('executeAsyncScript', function() { + it('failsIfArgumentIsARejectedPromise', function() { + var arg = promise.rejected(new StubError); + arg.catch(function() {}); // Suppress default handler. + + var driver = new FakeExecutor().createDriver(); + return driver.executeAsyncScript(function() {}, arg). + then(fail, assertIsStubError); + }); + }); + + describe('findElement', function() { + it('elementNotFound', function() { + let executor = new FakeExecutor(). + expect(CName.FIND_ELEMENT, + {using: 'css selector', value: '*[id="foo"]'}). + andReturnError(new StubError). + end(); + + var driver = executor.createDriver(); + var element = driver.findElement(By.id('foo')); + element.click(); // This should never execute. + return waitForAbort().then(assertIsStubError); + }); + + it('elementNotFoundInACallback', function() { + let executor = new FakeExecutor(). + expect(CName.FIND_ELEMENT, + {using: 'css selector', value: '*[id="foo"]'}). + andReturnError(new StubError). + end(); + + var driver = executor.createDriver(); + promise.fulfilled().then(function() { + var element = driver.findElement(By.id('foo')); + return element.click(); // Should not execute. + }); + return waitForAbort().then(assertIsStubError); + }); + + it('elementFound', function() { + let executor = new FakeExecutor(). + expect(CName.FIND_ELEMENT, + {using: 'css selector', value: '*[id="foo"]'}). + andReturnSuccess(WebElement.buildId('bar')). + expect(CName.CLICK_ELEMENT, {'id': 'bar'}). + andReturnSuccess(). + end(); + + var driver = executor.createDriver(); + var element = driver.findElement(By.id('foo')); + element.click(); + return waitForIdle(); + }); + + it('canUseElementInCallback', function() { + let executor = new FakeExecutor(). + expect(CName.FIND_ELEMENT, + {using: 'css selector', value: '*[id="foo"]'}). + andReturnSuccess(WebElement.buildId('bar')). + expect(CName.CLICK_ELEMENT, {'id': 'bar'}). + andReturnSuccess(). + end(); + + var driver = executor.createDriver(); + driver.findElement(By.id('foo')).then(function(element) { + element.click(); + }); + return waitForIdle(); + }); + + it('byJs', function() { + let executor = new FakeExecutor(). + expect(CName.EXECUTE_SCRIPT, { + 'script': 'return document.body', + 'args': [] + }). + andReturnSuccess(WebElement.buildId('bar')). + expect(CName.CLICK_ELEMENT, {'id': 'bar'}). + end(); + + var driver = executor.createDriver(); + var element = driver.findElement(By.js('return document.body')); + element.click(); // just to make sure + return waitForIdle(); + }); + + it('byJs_returnsNonWebElementValue', function() { + let executor = new FakeExecutor(). + expect(CName.EXECUTE_SCRIPT, {'script': 'return 123', 'args': []}). + andReturnSuccess(123). + end(); + + var driver = executor.createDriver(); + var element = driver.findElement(By.js('return 123')); + element.click(); // Should not execute. + return waitForAbort().then(function(e) { + assertIsInstance(TypeError, e); + assert.equal('Custom locator did not return a WebElement', e.message); + }); + }); + + it('byJs_canPassArguments', function() { + var script = 'return document.getElementsByTagName(arguments[0]);'; + let executor = new FakeExecutor(). + expect(CName.EXECUTE_SCRIPT, { + 'script': script, + 'args': ['div'] + }). + andReturnSuccess({'ELEMENT':'one'}). + end(); + var driver = executor.createDriver(); + driver.findElement(By.js(script, 'div')); + return waitForIdle(); + }); + + it('customLocator', function() { + let executor = new FakeExecutor(). + expect(CName.FIND_ELEMENTS, {'using': 'css selector', 'value': 'a'}). + andReturnSuccess([ + WebElement.buildId('foo'), + WebElement.buildId('bar')]). + expect(CName.CLICK_ELEMENT, {'id': 'foo'}). + andReturnSuccess(). + end(); + + var driver = executor.createDriver(); + var element = driver.findElement(function(d) { + assert.equal(driver, d); + return d.findElements(By.tagName('a')); + }); + element.click(); + return waitForIdle(); + }); + + it('customLocatorThrowsIfresultIsNotAWebElement', function() { + var driver = new FakeExecutor().createDriver(); + driver.findElement(function() { + return 1; + }); + return waitForAbort().then(function(e) { + assertIsInstance(TypeError, e); + assert.equal('Custom locator did not return a WebElement', e.message); + }); + }); + }); + + describe('findElements', function() { + it('returnsMultipleElements', function() { + var ids = ['foo', 'bar', 'baz']; + let executor = new FakeExecutor(). + expect(CName.FIND_ELEMENTS, {'using':'css selector', 'value':'a'}). + andReturnSuccess(ids.map(WebElement.buildId)). + end(); + + var driver = executor.createDriver(); + return driver.findElements(By.tagName('a')) + .then(function(elements) { + return promise.all(elements.map(function(e) { + assert.ok(e instanceof WebElement); + return e.getId(); + })); + }) + .then((actual) => assert.deepEqual(ids, actual)); + }); + + it('byJs', function() { + var ids = ['foo', 'bar', 'baz']; + let executor = new FakeExecutor(). + expect(CName.EXECUTE_SCRIPT, { + 'script': 'return document.getElementsByTagName("div");', + 'args': [] + }). + andReturnSuccess(ids.map(WebElement.buildId)). + end(); + + var driver = executor.createDriver(); + + return driver. + findElements(By.js('return document.getElementsByTagName("div");')). + then(function(elements) { + return promise.all(elements.map(function(e) { + assert.ok(e instanceof WebElement); + return e.getId(); + })); + }). + then((actual) => assert.deepEqual(ids, actual)); + }); + + it('byJs_filtersOutNonWebElementResponses', function() { + var ids = ['foo', 'bar', 'baz']; + var json = [ + WebElement.buildId(ids[0]), + 123, + 'a', + false, + WebElement.buildId(ids[1]), + {'not a web element': 1}, + WebElement.buildId(ids[2]) + ]; + let executor = new FakeExecutor(). + expect(CName.EXECUTE_SCRIPT, { + 'script': 'return document.getElementsByTagName("div");', + 'args': [] + }). + andReturnSuccess(json). + end(); + + var driver = executor.createDriver(); + driver.findElements(By.js('return document.getElementsByTagName("div");')). + then(function(elements) { + return promise.all(elements.map(function(e) { + assert.ok(e instanceof WebElement); + return e.getId(); + })); + }). + then((actual) => assert.deepEqual(ids, actual)); + return waitForIdle(); + }); + + it('byJs_convertsSingleWebElementResponseToArray', function() { + let executor = new FakeExecutor(). + expect(CName.EXECUTE_SCRIPT, { + 'script': 'return document.getElementsByTagName("div");', + 'args': [] + }). + andReturnSuccess(WebElement.buildId('foo')). + end(); + + var driver = executor.createDriver(); + return driver. + findElements(By.js('return document.getElementsByTagName("div");')). + then(function(elements) { + return promise.all(elements.map(function(e) { + assert.ok(e instanceof WebElement); + return e.getId(); + })); + }). + then((actual) => assert.deepEqual(['foo'], actual)); + }); + + it('byJs_canPassScriptArguments', function() { + var script = 'return document.getElementsByTagName(arguments[0]);'; + let executor = new FakeExecutor(). + expect(CName.EXECUTE_SCRIPT, { + 'script': script, + 'args': ['div'] + }). + andReturnSuccess([ + WebElement.buildId('one'), + WebElement.buildId('two') + ]). + end(); + + var driver = executor.createDriver(); + return driver.findElements(By.js(script, 'div')) + then(function(elements) { + return promise.all(elements.map(function(e) { + assert.ok(e instanceof WebElement); + return e.getId(); + })); + }). + then((actual) => assert.deepEqual(['one', 'two'], actual)); + }); + }); + + describe('sendKeys', function() { + it('convertsVarArgsIntoStrings_simpleArgs', function() { + let executor = new FakeExecutor(). + expect(CName.SEND_KEYS_TO_ELEMENT, + {'id': 'one', 'value':'12abc3'.split('')}). + andReturnSuccess(). + end(); + + var driver = executor.createDriver(); + var element = new WebElement(driver, 'one'); + element.sendKeys(1, 2, 'abc', 3); + return waitForIdle(); + }); + + it('convertsVarArgsIntoStrings_promisedArgs', function() { + let executor = new FakeExecutor(). + expect(CName.FIND_ELEMENT, + {'using':'css selector', 'value':'*[id="foo"]'}). + andReturnSuccess(WebElement.buildId('one')). + expect(CName.SEND_KEYS_TO_ELEMENT, + {'id':'one', 'value':'abc123def'.split('')}). + andReturnSuccess(). + end(); + + var driver = executor.createDriver(); + var element = driver.findElement(By.id('foo')); + element.sendKeys( + promise.fulfilled('abc'), 123, + promise.fulfilled('def')); + return waitForIdle(); + }); + + it('sendKeysWithAFileDetector', function() { + let executor = new FakeExecutor(). + expect(CName.FIND_ELEMENT, + {'using':'css selector', 'value':'*[id="foo"]'}). + andReturnSuccess(WebElement.buildId('one')). + expect(CName.SEND_KEYS_TO_ELEMENT, + {'id': 'one', 'value':'modified/path'.split('')}). + andReturnSuccess(). + end(); + + let driver = executor.createDriver(); + let handleFile = function(d, path) { + assert.strictEqual(driver, d); + assert.equal(path, 'original/path'); + return promise.fulfilled('modified/path'); + }; + driver.setFileDetector({handleFile}); + + var element = driver.findElement(By.id('foo')); + element.sendKeys('original/', 'path'); + return waitForIdle(); + }); + }); + + describe("switchTo()", function() { + describe("window", function() { + it('should return a resolved promise when the window is found', function() { + let executor = new FakeExecutor(). + expect(CName.SWITCH_TO_WINDOW). + withParameters({ + 'name': 'foo', + 'handle': 'foo' + }). + andReturnSuccess(). + end(); + + executor.createDriver().switchTo().window('foo'); + return waitForIdle(); + }); + + it('should propogate exceptions', function() { + let e = new error.NoSuchWindowError('window not found'); + let executor = new FakeExecutor(). + expect(CName.SWITCH_TO_WINDOW). + withParameters({ + 'name': 'foo', + 'handle': 'foo' + }). + andReturnError(e). + end(); + + executor.createDriver().switchTo().window('foo'); + return waitForAbort().then(v => assert.strictEqual(v, e)); + }); + }); + }); + + describe('elementEquality', function() { + it('isReflexive', function() { + var a = new WebElement({}, 'foo'); + return WebElement.equals(a, a).then(assert.ok); + }); + + it('failsIfAnInputElementCouldNotBeFound', function() { + var id = promise.rejected(new StubError); + id.catch(function() {}); // Suppress default handler. + + var driver = new FakeExecutor().createDriver(); + var a = new WebElement(driver, 'foo'); + var b = new WebElementPromise(driver, id); + + return WebElement.equals(a, b).then(fail, assertIsStubError); + }); + }); + + describe('waiting', function() { + it('waitSucceeds', function() { + let executor = new FakeExecutor(). + expect(CName.FIND_ELEMENTS, + {using: 'css selector', value: '*[id="foo"]'}). + andReturnSuccess([]). + times(2). + expect(CName.FIND_ELEMENTS, + {using: 'css selector', value: '*[id="foo"]'}). + andReturnSuccess([WebElement.buildId('bar')]). + end(); + + var driver = executor.createDriver(); + driver.wait(function() { + return driver.findElements(By.id('foo')).then(els => els.length > 0); + }, 200); + return waitForIdle(); + }); + + it('waitTimesout_timeoutCaught', function() { + let executor = new FakeExecutor(). + expect(CName.FIND_ELEMENTS, + {using: 'css selector', value: '*[id="foo"]'}). + andReturnSuccess([]). + anyTimes(). + end(); + + var driver = executor.createDriver(); + return driver.wait(function() { + return driver.findElements(By.id('foo')).then(els => els.length > 0); + }, 25).then(fail, function(e) { + assert.equal('Wait timed out after ', + e.message.substring(0, 'Wait timed out after '.length)); + }); + }); + + it('waitTimesout_timeoutNotCaught', function() { + let executor = new FakeExecutor(). + expect(CName.FIND_ELEMENTS, + {using: 'css selector', value: '*[id="foo"]'}). + andReturnSuccess([]). + anyTimes(). + end(); + + var driver = executor.createDriver(); + driver.wait(function() { + return driver.findElements(By.id('foo')).then(els => els.length > 0); + }, 25); + return waitForAbort().then(function(e) { + assert.equal('Wait timed out after ', + e.message.substring(0, 'Wait timed out after '.length)); + }); + }); + }); + + describe('alert handling', function() { + it('alertResolvesWhenPromisedTextResolves', function() { + var deferredText = new promise.Deferred(); + + var alert = new AlertPromise({}, deferredText.promise); + assert.ok(alert.isPending()); + + deferredText.fulfill(new Alert({}, 'foo')); + return alert.getText().then(function(text) { + assert.equal('foo', text); + }); + }); + + it('cannotSwitchToAlertThatIsNotPresent', function() { + let e = new error.NoSuchAlertError; + let executor = new FakeExecutor() + .expect(CName.GET_ALERT_TEXT) + .andReturnError(e) + .end(); + + var driver = executor.createDriver(); + var alert = driver.switchTo().alert(); + alert.dismiss(); // Should never execute. + return waitForAbort().then(v => assert.strictEqual(v, e)); + }); + + it('alertsBelongToSameFlowAsParentDriver', function() { + let executor = new FakeExecutor() + .expect(CName.GET_ALERT_TEXT).andReturnSuccess('hello') + .end(); + + var driver = executor.createDriver(); + var otherFlow = new promise.ControlFlow(); + otherFlow.execute(function() { + driver.switchTo().alert().then(function() { + assert.strictEqual( + driver.controlFlow(), promise.controlFlow(), + 'Alert should belong to the same flow as its parent driver'); + }); + }); + + assert.notEqual(otherFlow, driver.controlFlow); + return Promise.all([ + waitForIdle(otherFlow), + waitForIdle(driver.controlFlow()) + ]); + }); + + it('commandsFailIfAlertNotPresent', function() { + let e = new error.NoSuchAlertError; + let executor = new FakeExecutor() + .expect(CName.GET_ALERT_TEXT) + .andReturnError(e) + .end(); + + var driver = executor.createDriver(); + var alert = driver.switchTo().alert(); + + var expectError = (v) => assert.strictEqual(v, e); + + return alert.getText() + .then(fail, expectedError) + .then(() => alert.accept()) + .then(fail, expectedError) + .then(() => alert.dismiss()) + .then(fail, expectError) + .then(() => alert.sendKeys('hi')) + .then(fail, expectError); + }); + }); + + it('testWebElementsBelongToSameFlowAsParentDriver', function() { + let executor = new FakeExecutor() + .expect(CName.FIND_ELEMENT, + {using: 'css selector', value: '*[id="foo"]'}) + .andReturnSuccess(WebElement.buildId('abc123')) + .end(); + + var driver = executor.createDriver(); + var otherFlow = new promise.ControlFlow(); + otherFlow.execute(function() { + driver.findElement({id: 'foo'}).then(function() { + assert.equal(driver.controlFlow(), promise.controlFlow()); + }); + }); + + assert.notEqual(otherFlow, driver.controlFlow); + return Promise.all([ + waitForIdle(otherFlow), + waitForIdle(driver.controlFlow()) + ]); + }); + + it('testFetchingLogs', function() { + let executor = new FakeExecutor(). + expect(CName.GET_LOG, {'type': 'browser'}). + andReturnSuccess([ + {'level': 'INFO', 'message': 'hello', 'timestamp': 1234}, + {'level': 'DEBUG', 'message': 'abc123', 'timestamp': 5678} + ]). + end(); + + var driver = executor.createDriver(); + return driver.manage().logs().get('browser').then(function(entries) { + assert.equal(2, entries.length); + + assert.ok(entries[0] instanceof logging.Entry); + assert.equal(logging.Level.INFO.value, entries[0].level.value); + assert.equal('hello', entries[0].message); + assert.equal(1234, entries[0].timestamp); + + assert.ok(entries[1] instanceof logging.Entry); + assert.equal(logging.Level.DEBUG.value, entries[1].level.value); + assert.equal('abc123', entries[1].message); + assert.equal(5678, entries[1].timestamp); + }); + }); + + it('testCommandsFailIfInitialSessionCreationFailed', function() { + var session = promise.rejected(new StubError); + + var driver = new FakeExecutor().createDriver(session); + var navigateResult = driver.get('some-url').then(fail, assertIsStubError); + var quitResult = driver.quit().then(fail, assertIsStubError); + + return waitForIdle().then(function() { + return promise.all(navigateResult, quitResult); + }); + }); + + it('testWebElementCommandsFailIfInitialDriverCreationFailed', function() { + var session = Promise.reject(new StubError); + var driver = new FakeExecutor().createDriver(session); + return driver.findElement(By.id('foo')).click(). + then(fail, assertIsStubError); + }); + + it('testWebElementCommansFailIfElementCouldNotBeFound', function() { + let e = new error.NoSuchElementError('Unable to find element'); + let executor = new FakeExecutor(). + expect(CName.FIND_ELEMENT, + {using: 'css selector', value: '*[id="foo"]'}). + andReturnError(e). + end(); + + var driver = executor.createDriver(); + return driver.findElement(By.id('foo')).click() + .then(fail, v => assert.strictEqual(v, e)); + }); + + it('testCannotFindChildElementsIfParentCouldNotBeFound', function() { + let e = new error.NoSuchElementError('Unable to find element'); + let executor = new FakeExecutor(). + expect(CName.FIND_ELEMENT, + {using: 'css selector', value: '*[id="foo"]'}). + andReturnError(e). + end(); + + var driver = executor.createDriver(); + return driver.findElement(By.id('foo')) + .findElement(By.id('bar')) + .findElement(By.id('baz')) + .then(fail, v => assert.strictEqual(v, e)); + }); + + describe('actions()', function() { + it('failsIfInitialDriverCreationFailed', function() { + let session = promise.rejected(new StubError); + session.catch(function() {}); + return new FakeExecutor(). + createDriver(session). + actions(). + mouseDown(). + mouseUp(). + perform(). + catch(assertIsStubError); + }); + + describe('mouseMove', function() { + it('noElement', function() { + let executor = new FakeExecutor() + .expect(CName.MOVE_TO, {'xoffset': 0, 'yoffset': 125}) + .andReturnSuccess() + .end(); + + return executor.createDriver(). + actions(). + mouseMove({x: 0, y: 125}). + perform(); + }); + + it('element', function() { + let executor = new FakeExecutor() + .expect(CName.FIND_ELEMENT, + {using: 'css selector', value: '*[id="foo"]'}) + .andReturnSuccess(WebElement.buildId('abc123')) + .expect(CName.MOVE_TO, + {'element': 'abc123', 'xoffset': 0, 'yoffset': 125}) + .andReturnSuccess() + .end(); + + var driver = executor.createDriver(); + var element = driver.findElement(By.id('foo')); + return driver.actions() + .mouseMove(element, {x: 0, y: 125}) + .perform(); + }); + }); + + it('supportsMouseDown', function() { + let executor = new FakeExecutor() + .expect(CName.MOUSE_DOWN, {'button': Button.LEFT}) + .andReturnSuccess() + .end(); + + return executor.createDriver(). + actions(). + mouseDown(). + perform(); + }); + + it('testActionSequence', function() { + let executor = new FakeExecutor() + .expect(CName.FIND_ELEMENT, + {using: 'css selector', value: '*[id="a"]'}) + .andReturnSuccess(WebElement.buildId('id1')) + .expect(CName.FIND_ELEMENT, + {using: 'css selector', value: '*[id="b"]'}) + .andReturnSuccess(WebElement.buildId('id2')) + .expect(CName.SEND_KEYS_TO_ACTIVE_ELEMENT, + {'value': [Key.SHIFT]}) + .andReturnSuccess() + .expect(CName.MOVE_TO, {'element': 'id1'}) + .andReturnSuccess() + .expect(CName.CLICK, {'button': Button.LEFT}) + .andReturnSuccess() + .expect(CName.MOVE_TO, {'element': 'id2'}) + .andReturnSuccess() + .expect(CName.CLICK, {'button': Button.LEFT}) + .andReturnSuccess() + .end(); + + var driver = executor.createDriver(); + var element1 = driver.findElement(By.id('a')); + var element2 = driver.findElement(By.id('b')); + + return driver.actions() + .keyDown(Key.SHIFT) + .click(element1) + .click(element2) + .perform(); + }); + }); + + describe('touchActions()', function() { + it('failsIfInitialDriverCreationFailed', function() { + let session = promise.rejected(new StubError); + session.catch(function() {}); + return new FakeExecutor(). + createDriver(session). + touchActions(). + scroll({x: 3, y: 4}). + perform(). + catch(assertIsStubError); + }); + + it('testTouchActionSequence', function() { + let executor = new FakeExecutor() + .expect(CName.TOUCH_DOWN, {x: 1, y: 2}).andReturnSuccess() + .expect(CName.TOUCH_MOVE, {x: 3, y: 4}).andReturnSuccess() + .expect(CName.TOUCH_UP, {x: 5, y: 6}).andReturnSuccess() + .end(); + + var driver = executor.createDriver(); + return driver.touchActions() + .tapAndHold({x: 1, y: 2}) + .move({x: 3, y: 4}) + .release({x: 5, y: 6}) + .perform(); + }); + }); + + describe('generator support', function() { + var driver; + + beforeEach(function() { + driver = new WebDriver( + new Session('test-session', {}), + new ExplodingExecutor()); + }); + + it('canUseGeneratorsWithWebDriverCall', function() { + return driver.call(function* () { + var x = yield promise.fulfilled(1); + var y = yield promise.fulfilled(2); + return x + y; + }).then(function(value) { + assert.deepEqual(3, value); + }); + }); + + it('canDefineScopeOnGeneratorCall', function() { + return driver.call(function* () { + var x = yield promise.fulfilled(1); + return this.name + x; + }, {name: 'Bob'}).then(function(value) { + assert.deepEqual('Bob1', value); + }); + }); + + it('canSpecifyArgsOnGeneratorCall', function() { + return driver.call(function* (a, b) { + var x = yield promise.fulfilled(1); + var y = yield promise.fulfilled(2); + return [x + y, a, b]; + }, null, 'abc', 123).then(function(value) { + assert.deepEqual([3, 'abc', 123], value); + }); + }); + + it('canUseGeneratorWithWebDriverWait', function() { + var values = []; + return driver.wait(function* () { + yield values.push(1); + values.push(yield promise.delayed(10).then(function() { + return 2; + })); + yield values.push(3); + return values.length === 6; + }, 250).then(function() { + assert.deepEqual([1, 2, 3, 1, 2, 3], values); + }); + }); + + /** + * @constructor + * @implements {CommandExecutor} + */ + function ExplodingExecutor() {} + + + /** @override */ + ExplodingExecutor.prototype.execute = function(command, cb) { + cb(Error('Unsupported operation')); + }; + }); + + describe('wire format', function() { + describe('can serialize', function() { + function runSerializeTest(input, want) { + let executor = new FakeExecutor(). + expect(CName.NEW_SESSION). + withParameters({'desiredCapabilities': want}). + andReturnSuccess({'browserName': 'firefox'}). + end(); + return WebDriver.createSession(executor, input) + .getSession(); + } + + it('function as a string', function() { + function foo() { return 'foo'; } + return runSerializeTest(foo, '' + foo); + }); + + it('object with toJSON()', function() { + return runSerializeTest( + new Date(605728511546), + '1989-03-12T17:55:11.546Z'); + }); + + it('Session', function() { + return runSerializeTest(new Session('foo', {}), 'foo'); + }); + + it('Capabilities', function() { + var prefs = new logging.Preferences(); + prefs.setLevel(logging.Type.BROWSER, logging.Level.DEBUG); + + var caps = Capabilities.chrome(); + caps.setLoggingPrefs(prefs); + + return runSerializeTest( + caps, + { + 'browserName': 'chrome', + 'loggingPrefs': {'browser': 'DEBUG'} + }); + }); + + it('WebElement', function() { + return runSerializeTest( + new WebElement({}, 'fefifofum'), + WebElement.buildId('fefifofum')); + }); + + it('WebElementPromise', function() { + return runSerializeTest( + new WebElementPromise( + {}, promise.fulfilled(new WebElement({}, 'fefifofum'))), + WebElement.buildId('fefifofum')); + }); + + describe('an array', function() { + it('with Serializable', function() { + return runSerializeTest([new Session('foo', {})], ['foo']); + }); + + it('with WebElement', function() { + return runSerializeTest( + [new WebElement({}, 'fefifofum')], + [WebElement.buildId('fefifofum')]); + }); + + it('with WebElementPromise', function() { + return runSerializeTest( + [new WebElementPromise( + {}, promise.fulfilled(new WebElement({}, 'fefifofum')))], + [WebElement.buildId('fefifofum')]); + }); + + it('complex array', function() { + var expected = [ + 'abc', 123, true, WebElement.buildId('fefifofum'), + [123, {'foo': 'bar'}] + ]; + + var element = new WebElement({}, 'fefifofum'); + var input = ['abc', 123, true, element, [123, {'foo': 'bar'}]]; + return runSerializeTest(input, expected); + }); + + it('nested promises', function() { + return runSerializeTest( + ['abc', Promise.resolve([123, Promise.resolve(true)])], + ['abc', [123, true]]); + }); + }); + + describe('an object', function() { + it('literal', function() { + var expected = {sessionId: 'foo'}; + return runSerializeTest({sessionId: 'foo'}, expected); + }); + + it('with sub-objects', function() { + var expected = {sessionId: {value: 'foo'}}; + return runSerializeTest( + {sessionId: {value: 'foo'}}, expected); + }); + + it('with values that have toJSON', function() { + return runSerializeTest( + {a: {b: new Date(605728511546)}}, + {a: {b: '1989-03-12T17:55:11.546Z'}}); + }); + + it('with a Session', function() { + return runSerializeTest( + {a: new Session('foo', {})}, + {a: 'foo'}); + }); + + it('nested', function() { + var elementJson = WebElement.buildId('fefifofum'); + var expected = { + 'script': 'return 1', + 'args': ['abc', 123, true, elementJson, [123, {'foo': 'bar'}]], + 'sessionId': 'foo' + }; + + var element = new WebElement({}, 'fefifofum'); + var parameters = { + 'script': 'return 1', + 'args':['abc', 123, true, element, [123, {'foo': 'bar'}]], + 'sessionId': new Session('foo', {}) + }; + + return runSerializeTest(parameters, expected); + }); + }); + }); + + describe('can deserialize', function() { + function runDeserializeTest(original, want) { + let executor = new FakeExecutor() + .expect(CName.GET_CURRENT_URL) + .andReturnSuccess(original) + .end(); + let driver = executor.createDriver(); + return driver.getCurrentUrl().then(function(got) { + assert.deepEqual(got, want); + }); + } + + it('primitives', function() { + return Promise.all([ + runDeserializeTest(1, 1), + runDeserializeTest('', ''), + runDeserializeTest(true, true), + runDeserializeTest(undefined, undefined), + runDeserializeTest(null, null) + ]); + }); + + it('simple object', function() { + return runDeserializeTest( + {sessionId: 'foo'}, + {sessionId: 'foo'}); + }); + + it('nested object', function() { + return runDeserializeTest( + {'foo': {'bar': 123}}, + {'foo': {'bar': 123}}); + }); + + it('array', function() { + return runDeserializeTest( + [{'foo': {'bar': 123}}], + [{'foo': {'bar': 123}}]); + }); + + it('passes through function properties', function() { + function bar() {} + return runDeserializeTest( + [{foo: {'bar': 123}, func: bar}], + [{foo: {'bar': 123}, func: bar}]); + }); + }); + }); +}); |