diff --git a/.gitignore b/.gitignore index 3351ce2..943eec3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *.log node_modules +built_spec diff --git a/.jshintignore b/.jshintignore new file mode 100644 index 0000000..31373a0 --- /dev/null +++ b/.jshintignore @@ -0,0 +1 @@ +./built_spec/* diff --git a/.npmignore b/.npmignore index 24efbee..23c47b1 100644 --- a/.npmignore +++ b/.npmignore @@ -1,4 +1,10 @@ node_modules/ spec/ -.travis.yml +built_spec/ +scripts/ +tsconfig.json +tslint.json +.gitignore +.jshintignore .npmignore +.travis.yml diff --git a/.travis.yml b/.travis.yml index 163b7f6..f7fe068 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,7 @@ +sudo: false language: node_js node_js: - - "0.10" + - "6" script: - npm test diff --git a/CHANGELOG.md b/CHANGELOG.md index a27c807..a4dad3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,42 +1,239 @@ -# 1.1.0 +# Changelog for jasminewd2 -## Dependency Updates -- ([daa67d6])(https://github.com/angular/jasminewd/commit/daa67d6eabdd9c70306748da8a0dc0a6f2edb90f)) chore(dependencies): update to selenium-webdriver 2.43.4 +# 2.1.0 + +## Features + +- ([41577a5](https://github.com/angular/jasminewd/commit/41577a5e10420d255fb2ec12aa0ea3a8e72f14ca)) + support native async functions (node 7.6+) (#87) -# 1.0.4 ## Bug Fixes -- ([a088e6f](https://github.com/angular/jasminewd/commit/a088e6f175ca817f59d5eea99549e45ab5861ce0)) fix(timeouts): should call special timeout handlers only for a jasmine timeout - Previously, it used to call the resets if anything matched 'timeout'. This was too - vague, since many error messages contain that string. +- ([0137d3f](https://github.com/angular/jasminewd/commit/0137d3f2ae96ef6d51d00055d64b5a8103ae83d0)) + minor fix to keep stack from original error (#86) - Closes #8 +- ([374f494](https://github.com/angular/jasminewd/commit/374f4946972673f86e06a011e20fc039bb73e234)) + Allow to specify a function as a custom matcher's message. (#29) -# 1.0.3 -## Bug Fixes -- ([00821b3](https://github.com/angular/jasminewd/commit/00821b3180a6674012fdccab106835f5ce94bb3f)) fix(timeout): better messaging if the control flow does not have a listed last task -# 1.0.2 -## Bug Fixes -- ([30b6811](https://github.com/angular/jasminewd/commit/30b68113759a7cb5c8dabc5b16ffcd89516882d8)) fix(timeout): output more information about the current task when a timeout occurs +# 2.0.0 + +(Skipping 1.x because `0.0.1` was originally accidently published as `1.0.0`.) + +## Breaking changes + +- ([fae803c](https://github.com/angular/protractor/commit/fae803cd294e5413523d37bdaa282a9f96cd65a1)) + pass webdriver instance into `init()` instead of using `require()` (#83) + + So where as before you would write: + + ```js + require('jasminewd').init(webdriver.promise.controlFlow()); + ``` + Now you will write: + + ```js + require('jasminewd').init(webdriver.promise.controlFlow(), webdriver); + ``` + + This removes the dependency on `selenium-webdriver` and protects jasminewd from having a + different webdriver instance than Protractor, which could be a huge problem if they had different + control flow settings. + + This is a breaking change because it changes the API for the `init` function. + + I also removed the dependency on jasmine, which didn't do anything anyway. Maybe it should have + been a peerDependency but those are deprecated. + + +## Features + +- ([171cbde](https://github.com/angular/protractor/commit/171cbde22f307bd3cc35c4c1785f171392dca8da)) + Added types (though you'll have to wait for `@types/jasminewd2` to use them) (#79) + + +- ([27b4850](https://github.com/angular/protractor/commit/27b485019589cd662ee69e7920893ffa50774b97)) + Support `SELENIUM_PROMISE_MANAGER=0` (#72) + + There are three major ways this was done in this change: + * In `callWhenIdle`, if `flow.isIdle` is not defined, we assume we are working with a + `SimpleScheduler` instance, and so the flow is effectively idle. + * In `initJasmineWd`, if `flow.reset` is not defined, we assume we are working with a + `SimpleScheduler` instance, and so don't bother resetting the flow. + * In `wrapInControlFlow`, we use `flow.promise` to create a new promise if possible. Since + `new webdriver.promise.Promise()` would have always made a `ManagedPromise`, but `flow.promise` + will do the right thing. + * In `wrapCompare`, we avoid the webdriver library entirely, and never instance any extra + promises. Using `webdriver.promise.when` and `webdriver.promise.all` could have been a problem + if our instance of `webdriver` had the control flow turned on, but another instance somewhere + did not (or even the same instance, but just at a different point in time). Instead we use the + new `maybePromise` tool, which is a mess but is also exactly what we want. + * In `specs/*`, we replace `webdriver.promise.fulfilled` with `webdriver.promise.when`. + * In `specs/*`, a new version of `adapterSpec.js` and `errorSpec.js` are created: + `asyncAwaitAdapterSpec.ts` and `asyncAwaitErrorSpec.ts`. + + I also also fixed a minor bug where we weren't correctly checking for promises inside an array of + expected results. Before we had: + + ```js + expected = Array.prototype.slice.call(arguments, 0); + + ... + + webdriver.promise.isPromise(expected); + ``` + + I thought about it for a little while, and there's no way that's correct. `expected` is an + `Array`, there's no way it has a `.then` function. + + Closes https://github.com/angular/jasminewd/issues/69 -# 1.0.1 ## Bug Fixes -- ([c507b37](https://github.com/angular/jasminewd/commit/c507b37dd04cf267a437a579fc3b14063abb2ef8)) - fix(index): stop infinite promise resolution -1.0.0 -===== +- ([369a249](https://github.com/angular/protractor/commit/369a2499189fbcdc541f354cfede49dba9335e6b)) + Don't rely on `webdriver.promise` functions (#82) + + While we support `SELENIUM_PROMISE_MANAGER=0` already, we rely on `SimpleScheduler` and some other + utility functions which will be going away after the control flow has been fully deprecated. This + commit allows jasminewd to work without those utility functions, and even allows people to pass + jasminewd their own custom scheduler implementation. + + This does not fix our tests, which will also break when those utility functions go away. See + https://github.com/angular/jasminewd/issues/81 + + Closes https://github.com/angular/jasminewd/issues/80 + +# 0.1.1 + +- ([cf1cd34](https://github.com/angular/jasminewd/commit/cf1cd34a4089b6492160349a10d717c7bcaa2c31)) + chore(isPromise): revert expose deferred object's promise (#78) + +# 0.1.0 + +Release for the selenium-webdriver 3.0.1 upgrade. + +# 0.1.0-beta.1 + +- ([5fe36a6](https://github.com/angular/jasminewd/commit/5fe36a60102b9033180d68b238ab233a25a52393)) + deps(selenium-webdriver): upgrade to 3.0.0 (#63) + + fix test "should wait till the expect to run the flow" + + - `isPending` exists but it is no longer part of `ManagedPromise` + - `isPending` also is no longer exported in `lib/promise.js` + - wrote an `isPending` similar to selenium-webdriver in common.js + require a minimum node version + + - selenium-webdriver 3.0.0 requires node >= 6.9.0 + - update travis test to use node 6 + +# 0.1.0-beta.0 + +This beta release is for the selenium-webdriver 3.0.0-beta-3 upgrade. + +## Dependencies + +- ([70c9f62](https://github.com/angular/jasminewd/commit/70c9f62af50018bea6ad326e12bacd9ca03e6ae5)) + upgrade(isPromise): expose the deferred object's promise (#58) + + - isPromise checks to see if the input parameter has a then method + - Deferred class has a promise property and no longer has a then method +- ([8870365](https://github.com/angular/jasminewd/commit/88703656b4f8a012a084ba184a4fe473f423a200)) + deps(selenium-webdriver): upgrade to 3.0.0-beta-3 (#57) + +# 0.0.10 + +- ([ff2e624](https://github.com/angular/jasminewd/commit/ff2e624159344cd83b04c6a6648334ba12e78ea6)) + fix(webdriver): Pass in the control flow. + + BREAKING CHANGE: The control flow now needs to be passed in when using jasminewd. This fixes + an issue where having multiple versions of selenium-webdriver in a package's dependency tree would + result in jasminewd and protractor using different control flows. You now have to initialize + jasminewd before you can use it, like so: `require('jasminewd2').init(webdriver.promise.controlFlow());` + + See https://github.com/angular/protractor/issues/3505 + +- ([db26b1a](https://github.com/angular/jasminewd/commit/db26b1a1e66477a6f526dac56ecaaa50d2cf4700)) + fix(stacktrace): do not crash if beforeEach block is rejected without any stated reason (#45) + +# 0.0.9 + +- ([790c81e](https://github.com/angular/protractor/commit/790c81eb0aba880fffbdcb4e834eb2161141620c)) + fix(expectations): allow custom matchers to return a promise when actual is not a promise + + See angular/protractor#2964 + + +# 0.0.8 + +- ([5abc745](https://github.com/angular/protractor/commit/5abc7457cd73a4a4ba70b3c9ceeadac6d42bbd76)) + chore(jasmine): update MatchFactory to allow message as function + +- ([750898c](https://github.com/angular/protractor/commit/750898c90a1cc1bef09384b60ef6e15adfe734f7)) + fix(expectation): expectations without promises no longer add to task queue + + Instead, expectations without promises in either expected or actual are unchanged from the + original Jasmine implementation. + + See https://github.com/angular/protractor/issues/2894 + +# 0.0.7 + +- ([55fd11e](https://github.com/angular/protractor/commit/55fd11e69c2f1ba8fba9a19a8acccbe933896084)) + fix(index): forward it's return value + +- ([f4c30a0](https://github.com/angular/protractor/commit/f4c30a0023c6ec33b15df762226c3fe8e741d26e)) + fix: allow empty it functions + +# 0.0.6 + +- ([4776c16](https://github.com/angular/jasminewd/commit/4776c16b9a9f3a9a3de8a8dddc0e051cb32331b4)) + chore(selenium-webdriver): update selenium webdriver to 2.47.0 + + Update selenium-webdriver to 2.47.0 from 2.45.1. This update introduces a convoluted situation + where some tests in Proractor's suite would hang - see + https://github.com/angular/protractor/issues/2245 + + This change includes a fix for those issues which removes the explicit + `flow.execute` wrapper around `expect` calls. This appears not to introduce any issues to existing + tests. + +# 0.0.5 + +- ([037c7de](https://github.com/angular/jasminewd/commit/037c7de7fea4de068734b6fa250d145800863633)) + chore(dependencies): update Jasmine to 2.3.1 + +# 0.0.4 + +- ([8f8b8b3](https://github.com/angular/jasminewd/commit/8f8b8b39e779559fd3b29b138d7577658b8a64b7)) + tests(context): test that the `this` variable points to the right thing + + Note: this means that using `this.addMatchers` no longer works inside before blocks or specs. It + should have been changed to `jamsine.addMatchers` since the upgrade to Jasmine 2. It was still + working by accident up until the previous commit. + +- ([c0f13d2](https://github.com/angular/jasminewd/commit/c0f13d254966c859db22d020a5390138dbf48e64)) + refactor(asyncTestFn): refactor async test wrapping to show more info + + Test wrapping for Jasmine 2 now more closely follows the test wrapping for Mocha at + https://github.com/SeleniumHQ/selenium/blob/master/javascript/node/selenium-webdriver/testing/index.js + + This also adds more information to the task names in the control flow, for easier debugging. -Support for Jasmine 1.3.1. Tested against minijasminenode @ 0.4.0. +# 0.0.3 -Features +- ([161e1fa](https://github.com/angular/jasminewd/commit/161e1fa48deaa5ea0f485027ea8ae41562864936)) + fix(errors): update webdriverjs, fix asynchronous error output - - Automatically makes tests asynchronously wait until the WebDriverJS control flow is empty. + Add some console logging, remove useless info about the last running task in the control flow, and + fix error where problems reported from done.fail were getting pushed into the following spec. - - If a `done` function is passed to the test, waits for both the control flow and until done is called. + Closes #18 - - Enhances `expect` so that it automatically unwraps promises before performing the assertion. +- ([fdb03a3](https://github.com/angular/jasminewd/commit/fdb03a388d4846952c09fb0ad75a37b46674c750)) + docs(readme): add note about jasmine 1 vs jasmine 2 +- ([acaec8b](https://github.com/angular/jasminewd/commit/acaec8bdd157e9933d608c66204a52335fb46ee4)) + feat(index): add jasmine2.0 support diff --git a/LICENSE b/LICENSE index 969c1d6..df2ef23 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2014 Google, Inc. +Copyright (c) 2014-2017 Google, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 9cd01dc..3be4e3e 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,12 @@ -jasminewd [![Build Status](https://travis-ci.org/angular/jasminewd.png?branch=master)](https://travis-ci.org/angular/jasminewd) +jasminewd2 [![Build Status](https://travis-ci.org/angular/jasminewd.svg?branch=jasminewd2)](https://travis-ci.org/angular/jasminewd) ========= Adapter for Jasmine-to-WebDriverJS. Used by [Protractor](http://www.github.com/angular/protractor). +**Important:** There are two active branches of jasminewd. + + - [jasminewd1](https://github.com/angular/jasminewd/tree/jasminewd1) is an adapter for Jasmine 1.3, and uses the package minijasminenode. It is published to npm as `jasminewd`. + - [jasminewd2](https://github.com/angular/jasminewd/tree/jasminewd2) is an adapter for Jasmine 2.x, and uses the package jasmine. It is published to npm as `jasminewd2`. Features -------- @@ -11,33 +15,40 @@ Features - If a `done` function is passed to the test, waits for both the control flow and until done is called. + - If a test returns a promise, waits for both the control flow and the promise to resolve. + - Enhances `expect` so that it automatically unwraps promises before performing the assertion. Installation ------------ ``` -npm install jasminewd +npm install jasminewd2 ``` Usage ----- -Assumes selenium-webdriver as a peer dependency. +In your setup: ```js -// In your setup. -var minijn = require('minijasminenode'); -require('jasminewd'); +var JasmineRunner = require('jasmine'); +var jrunner = new JasmineRunner(); +var webdriver = require('selenium-webdriver'); global.driver = new webdriver.Builder(). usingServer('http://localhost:4444/wd/hub'). withCapabilities({browserName: 'chrome'}). build(); -minijn.executeSpecs(/* ... */); +require('jasminewd2').init(driver.controlFlow(), webdriver); + +jrunner.projectBaseDir = ''; +jrunner.execute(['**/*_spec.js']); +``` -// In your tests +In your tests: +```js describe('tests with webdriver', function() { it('will wait until webdriver is done', function() { // This will be an asynchronous test. It will finish once webdriver has @@ -52,3 +63,32 @@ describe('tests with webdriver', function() { }); }) ``` + +TypeScript +---------- + +For the typings related to the changes in the global jasmine variables (e.g. +allowing `it()` blocks to return a promise), we publish the package +`@types/jasminewd2`. If you are writing tests using jasminewd (including +Protractor tests), be sure to include `@types/jasminewd2` in your +`devDependencies`, as these global type modifications are ***not*** bundled with +the `jasminewd2` npm module. + +jasminewd also exports one function directly: `init`. Unfortunately, we do not +publish typings for this function. If you call this function directly (e.g. you +are a Protractor dev), you should simply do: + +```ts +require('jasminewd2').init(controlFlow, webdriver); +``` + +`async` functions / `await` +--------------------------- + +`async` functions and the `await` keyword are likely coming in ES2017 (ES8), and +available via several compilers. At the moment, they often break the WebDriver +control flow. +([GitHub issue](https://github.com/SeleniumHQ/selenium/issues/3037)). You can +still use them, but if you do then you will have to use `await`/Promises for +almost all your synchronization. See `spec/asyncAwaitAdapterSpec.ts` and +`spec/asyncAwaitErrorSpec.ts` for examples. diff --git a/index.js b/index.js index b73969e..559d816 100644 --- a/index.js +++ b/index.js @@ -4,20 +4,9 @@ * https://code.google.com/p/selenium/source/browse/javascript/node/selenium-webdriver/testing/index.js */ -var webdriver = require('selenium-webdriver'); - -var flow = webdriver.promise.controlFlow(); - -/** - * Wraps a function so that all passed arguments are ignored. - * @param {!Function} fn The function to wrap. - * @return {!Function} The wrapped function. - */ -function seal(fn) { - return function() { - fn(); - }; -} +var WebElement; // Equal to webdriver.WebElement +var idleEventName = 'idle'; // Equal to webdriver.promise.ControlFlow.EventType.IDLE +var maybePromise = require('./maybePromise'); /** * Validates that the parameter is a function. @@ -26,7 +15,7 @@ function seal(fn) { * @return {Object} The original parameter. */ function validateFunction(functionToValidate) { - if (functionToValidate && Object.prototype.toString.call(functionToValidate) === '[object Function]') { + if (functionToValidate && typeof functionToValidate === 'function') { return functionToValidate; } else { throw Error(functionToValidate + ' is not a function'); @@ -62,68 +51,105 @@ function validateString(stringtoValidate) { } /** - * Wraps a function so it runs inside a webdriver.promise.ControlFlow and - * waits for the flow to complete before continuing. + * Calls a function once the scheduler is idle. If the scheduler does not support the idle API, + * calls the function immediately. See scheduler.md#idle-api for details. + * + * @param {Object} scheduler The scheduler to wait for. + * @param {!Function} fn The function to call. + */ +function callWhenIdle(scheduler, fn) { + if (!scheduler.once || !scheduler.isIdle || scheduler.isIdle()) { + fn(); + } else { + scheduler.once(idleEventName, function() { fn(); }); + } +} + + +/** + * Wraps a function so it runs inside a scheduler's `execute()` block. + * + * In the most common case, this means wrapping in a `webdriver.promise.ControlFlow` instance + * to wait for the control flow to complete one task before starting the next. See scheduler.md + * for details. + * + * @param {!Object} scheduler See scheduler.md for details. + * @param {!Function} newPromise Makes a new promise using whatever implementation the scheduler + * prefers. * @param {!Function} globalFn The function to wrap. + * @param {!string} fnName The name of the function being wrapped (e.g. `'it'`). * @return {!Function} The new function. */ -function wrapInControlFlow(globalFn, fnName) { +function wrapInScheduler(scheduler, newPromise, globalFn, fnName) { return function() { var driverError = new Error(); driverError.stack = driverError.stack.replace(/ +at.+jasminewd.+\n/, ''); - function asyncTestFn(fn, desc) { + function asyncTestFn(fn, description) { + description = description ? ('("' + description + '")') : ''; return function(done) { - var desc_ = 'Asynchronous test function: ' + fnName + '('; - if (desc) { - desc_ += '"' + desc + '"'; - } - desc_ += ')'; - - // deferred object for signaling completion of asychronous function within globalFn - var asyncFnDone = webdriver.promise.defer(); - - if (fn.length === 0) { - // function with globalFn not asychronous - asyncFnDone.fulfill(); - } else if (fn.length > 1) { - throw Error('Invalid # arguments (' + fn.length + ') within function "' + fnName +'"'); - } + var async = fn.length > 0; + var testFn = fn.bind(this); - var flowFinished = flow.execute(function() { - fn.call(jasmine.getEnv().currentSpec, function(userError) { - if (userError) { - asyncFnDone.reject(new Error(userError)); + scheduler.execute(function schedulerExecute() { + return newPromise(function(fulfill, reject) { + function wrappedReject(err) { + if(err instanceof Error) + reject(err); + else + reject(new Error(err)); + } + if (async) { + // If testFn is async (it expects a done callback), resolve the promise of this + // test whenever that callback says to. Any promises returned from testFn are + // ignored. + var proxyDone = fulfill; + proxyDone.fail = wrappedReject; + testFn(proxyDone); } else { - asyncFnDone.fulfill(); + // Without a callback, testFn can return a promise, or it will + // be assumed to have completed synchronously. + var ret = testFn(); + if (maybePromise.isPromise(ret)) { + ret.then(fulfill, wrappedReject); + } else { + fulfill(ret); + } } }); - }, desc_); - - webdriver.promise.all([asyncFnDone, flowFinished]).then(function() { - seal(done)(); - }, function(e) { - e.stack = e.stack + '==== async task ====\n' + driverError.stack; - done(e); - }); + }, 'Run ' + fnName + description + ' in control flow').then( + callWhenIdle.bind(null, scheduler, done), function(err) { + if (!err) { + err = new Error('Unknown Error'); + err.stack = ''; + } + err.stack = err.stack + '\nFrom asynchronous test: \n' + driverError.stack; + callWhenIdle(scheduler, done.fail.bind(done, err)); + } + ); }; } var description, func, timeout; switch (fnName) { case 'it': - case 'iit': + case 'fit': description = validateString(arguments[0]); + if (!arguments[1]) { + return globalFn(description); + } func = validateFunction(arguments[1]); if (!arguments[2]) { - globalFn(description, asyncTestFn(func)); + return globalFn(description, asyncTestFn(func, description)); } else { timeout = validateNumber(arguments[2]); - globalFn(description, asyncTestFn(func), timeout); + return globalFn(description, asyncTestFn(func, description), timeout); } break; case 'beforeEach': case 'afterEach': + case 'beforeAll': + case 'afterAll': func = validateFunction(arguments[0]); if (!arguments[1]) { globalFn(asyncTestFn(func)); @@ -138,118 +164,173 @@ function wrapInControlFlow(globalFn, fnName) { }; } -global.it = wrapInControlFlow(global.it, 'it'); -global.iit = wrapInControlFlow(global.iit, 'iit'); -global.beforeEach = wrapInControlFlow(global.beforeEach, 'beforeEach'); -global.afterEach = wrapInControlFlow(global.afterEach, 'afterEach'); - - /** - * Wrap a Jasmine matcher function so that it can take webdriverJS promises. - * @param {!Function} matcher The matcher function to wrap. - * @param {webdriver.promise.Promise} actualPromise The promise which will - * resolve to the actual value being tested. - * @param {boolean} not Whether this is being called with 'not' active. + * Initialize the JasmineWd adapter with a particlar scheduler, generally a webdriver control flow. + * + * @param {Object=} scheduler The scheduler to wrap tests in. See scheduler.md for details. + * Defaults to a mock scheduler that calls functions immediately. + * @param {Object=} webdriver The result of `require('selenium-webdriver')`. Passed in here rather + * than required by jasminewd directly so that jasminewd can't end up up with a different version + * of `selenium-webdriver` than your tests use. If not specified, jasminewd will still work, but + * it won't check for `WebElement` instances in expect() statements and could cause control flow + * problems if your tests are using an old version of `selenium-webdriver` (e.g. version 2.53.0). */ -function wrapMatcher(matcher, actualPromise, not) { - return function() { - var originalArgs = arguments; - var matchError = new Error("Failed expectation"); - matchError.stack = matchError.stack.replace(/ +at.+jasminewd.+\n/, ''); - actualPromise.then(function(actual) { - var expected = originalArgs[0]; +function initJasmineWd(scheduler, webdriver) { + if (jasmine.JasmineWdInitialized) { + throw Error('JasmineWd already initialized when init() was called'); + } + jasmine.JasmineWdInitialized = true; - var expectation = originalExpect(actual); - if (not) { - expectation = expectation.not; - } - var originalAddMatcherResult = expectation.spec.addMatcherResult; - var error = matchError; - expectation.spec.addMatcherResult = function(result) { - result.trace = error; - jasmine.Spec.prototype.addMatcherResult.call(this, result); - }; - if (webdriver.promise.isPromise(expected)) { - if (originalArgs.length > 1) { - throw error('Multi-argument matchers with promises are not ' + - 'supported.'); - } - expected.then(function(exp) { - expectation[matcher].apply(expectation, [exp]); - expectation.spec.addMatcherResult = originalAddMatcherResult; - }); - } else { - expectation.spec.addMatcherResult = function(result) { - result.trace = error; - originalAddMatcherResult.call(this, result); - }; - expectation[matcher].apply(expectation, originalArgs); - expectation.spec.addMatcherResult = originalAddMatcherResult; - } - }); - }; -} + // Pull information from webdriver instance + if (webdriver) { + WebElement = webdriver.WebElement || WebElement; + idleEventName = ( + webdriver.promise && + webdriver.promise.ControlFlow && + webdriver.promise.ControlFlow.EventType && + webdriver.promise.ControlFlow.EventType.IDLE + ) || idleEventname; + } -/** - * Return a chained set of matcher functions which will be evaluated - * after actualPromise is resolved. - * @param {webdriver.promise.Promise} actualPromise The promise which will - * resolve to the actual value being tested. - */ -function promiseMatchers(actualPromise) { - var promises = {not: {}}; - var env = jasmine.getEnv(); - var matchersClass = env.currentSpec.matchersClass || env.matchersClass; - - for (var matcher in matchersClass.prototype) { - promises[matcher] = wrapMatcher(matcher, actualPromise, false); - promises.not[matcher] = wrapMatcher(matcher, actualPromise, true); + // Default to mock scheduler + if (!scheduler) { + scheduler = { execute: function(fn) { + return Promise.resolve().then(fn); + } }; + } + + // Figure out how we're getting new promises + var newPromise; + if (typeof scheduler.promise == 'function') { + newPromise = scheduler.promise.bind(scheduler); + } else if (webdriver && webdriver.promise && webdriver.promise.ControlFlow && + (scheduler instanceof webdriver.promise.ControlFlow) && + (webdriver.promise.USE_PROMISE_MANAGER !== false)) { + newPromise = function(resolver) { + return new webdriver.promise.Promise(resolver, scheduler); + }; + } else { + newPromise = function(resolver) { + return new Promise(resolver); + }; } - return promises; + // Wrap functions + global.it = wrapInScheduler(scheduler, newPromise, global.it, 'it'); + global.fit = wrapInScheduler(scheduler, newPromise, global.fit, 'fit'); + global.beforeEach = wrapInScheduler(scheduler, newPromise, global.beforeEach, 'beforeEach'); + global.afterEach = wrapInScheduler(scheduler, newPromise, global.afterEach, 'afterEach'); + global.beforeAll = wrapInScheduler(scheduler, newPromise, global.beforeAll, 'beforeAll'); + global.afterAll = wrapInScheduler(scheduler, newPromise, global.afterAll, 'afterAll'); + + // Reset API + if (scheduler.reset) { + // On timeout, the flow should be reset. This will prevent webdriver tasks + // from overflowing into the next test and causing it to fail or timeout + // as well. This is done in the reporter instead of an afterEach block + // to ensure that it runs after any afterEach() blocks with webdriver tasks + // get to complete first. + jasmine.getEnv().addReporter(new OnTimeoutReporter(function() { + console.warn('A Jasmine spec timed out. Resetting the WebDriver Control Flow.'); + scheduler.reset(); + })); + } } var originalExpect = global.expect; - global.expect = function(actual) { - if (actual instanceof webdriver.WebElement) { - throw 'expect called with WebElement argument, expected a Promise. ' + - 'Did you mean to use .getText()?'; - } - if (webdriver.promise.isPromise(actual)) { - return promiseMatchers(actual); - } else { - return originalExpect(actual); + if (WebElement && (actual instanceof WebElement)) { + throw Error('expect called with WebElement argument, expected a Promise. ' + + 'Did you mean to use .getText()?'); } + return originalExpect(actual); }; -// Wrap internal Jasmine function to allow custom matchers -// to return promises that resolve to truthy or falsy values -var originalMatcherFn = jasmine.Matchers.matcherFn_; -jasmine.Matchers.matcherFn_ = function(matcherName, matcherFunction) { - var matcherFnThis = this; - var matcherFnArgs = jasmine.util.argsToArray(arguments); +/** + * Creates a matcher wrapper that resolves any promises given for actual and + * expected values, as well as the `pass` property of the result. + * + * Wrapped matchers will return either `undefined` or a promise which resolves + * when the matcher is complete, depending on if the matcher had to resolve any + * promises. + */ +jasmine.Expectation.prototype.wrapCompare = function(name, matcherFactory) { return function() { - var matcherThis = this; - var matcherArgs = jasmine.util.argsToArray(arguments); - var result = matcherFunction.apply(this, arguments); - - if (webdriver.promise.isPromise(result)) { - result.then(function(resolution) { - matcherFnArgs[1] = function() { - return resolution; - }; - originalMatcherFn.apply(matcherFnThis, matcherFnArgs). - apply(matcherThis, matcherArgs); + var expected = Array.prototype.slice.call(arguments, 0), + expectation = this, + matchError = new Error("Failed expectation"); + + matchError.stack = matchError.stack.replace(/ +at.+jasminewd.+\n/, ''); + + // Return either undefined or a promise of undefined + return maybePromise(expectation.actual, function(actual) { + return maybePromise.all(expected, function(expected) { + return compare(actual, expected); }); - } else { - originalMatcherFn.apply(matcherFnThis, matcherFnArgs). - apply(matcherThis, matcherArgs); + }); + + function compare(actual, expected) { + var args = expected.slice(0); + args.unshift(actual); + + var matcher = matcherFactory(expectation.util, expectation.customEqualityTesters); + var matcherCompare = matcher.compare; + + if (expectation.isNot) { + matcherCompare = matcher.negativeCompare || defaultNegativeCompare; + } + + var result = matcherCompare.apply(null, args); + + return maybePromise(result.pass, compareDone); + + // compareDone always returns undefined + function compareDone(pass) { + var message = ''; + + if (!pass) { + if (!result.message) { + args.unshift(expectation.isNot); + args.unshift(name); + message = expectation.util.buildFailureMessage.apply(null, args); + } else { + if (Object.prototype.toString.apply(result.message) === '[object Function]') { + message = result.message(expectation.isNot); + } else { + message = result.message; + } + } + } + + if (expected.length == 1) { + expected = expected[0]; + } + var res = { + matcherName: name, + passed: pass, + message: message, + actual: actual, + expected: expected, + error: matchError + }; + expectation.addExpectationResult(pass, res); + } + + function defaultNegativeCompare() { + var result = matcher.compare.apply(null, args); + result.pass = maybePromise(result.pass, function(pass) { + return !pass; + }); + return result; + } } }; }; +// Re-add core matchers so they are wrapped. +jasmine.Expectation.addCoreMatchers(jasmine.matchers); + /** * A Jasmine reporter which does nothing but execute the input function * on a timeout failure. @@ -258,42 +339,16 @@ var OnTimeoutReporter = function(fn) { this.callback = fn; }; -OnTimeoutReporter.prototype.reportRunnerStarting = function() {}; -OnTimeoutReporter.prototype.reportRunnerResults = function() {}; -OnTimeoutReporter.prototype.reportSuiteResults = function() {}; -OnTimeoutReporter.prototype.reportSpecStarting = function() {}; -OnTimeoutReporter.prototype.reportSpecResults = function(spec) { - if (!spec.results().passed()) { - var result = spec.results(); - var failureItem = null; - - var items_length = result.getItems().length; - for (var i = 0; i < items_length; i++) { - if (result.getItems()[i].passed_ === false) { - failureItem = result.getItems()[i]; - - var jasmineTimeoutRegexp = - /timed out after \d+ msec waiting for spec to complete/; - if (failureItem.toString().match(jasmineTimeoutRegexp)) { - this.callback(); - } +OnTimeoutReporter.prototype.specDone = function(result) { + if (result.status === 'failed') { + for (var i = 0; i < result.failedExpectations.length; i++) { + var failureMessage = result.failedExpectations[i].message; + + if (failureMessage.match(/Timeout/)) { + this.callback(); } } } }; -OnTimeoutReporter.prototype.log = function() {}; - -// On timeout, the flow should be reset. This will prevent webdriver tasks -// from overflowing into the next test and causing it to fail or timeout -// as well. This is done in the reporter instead of an afterEach block -// to ensure that it runs after any afterEach() blocks with webdriver tasks -// get to complete first. -jasmine.getEnv().addReporter(new OnTimeoutReporter(function() { - console.warn('A Jasmine spec timed out. Resetting the WebDriver Control Flow.'); - console.warn('The last active task was: '); - console.warn( - (flow.activeFrame_ && flow.activeFrame_.getPendingTask() ? - flow.activeFrame_.getPendingTask().toString() : - 'unknown')); - flow.reset(); -})); + +module.exports.init = initJasmineWd; diff --git a/maybePromise.js b/maybePromise.js new file mode 100644 index 0000000..2bac170 --- /dev/null +++ b/maybePromise.js @@ -0,0 +1,72 @@ +/** + * This file implements jasminewd's peculiar alternatives to Promise.resolve() + * and Promise.all(). Do not use the code from this file as polyfill for + * Promise.resolve() or Promise.all(). There are a number of reasons why this + * implementation will cause unexpected errors in most codebases. + * + * Called "maybePromise" because both the parameters and the return values may + * or may not be promises, and code execution may or may not be synchronous. + */ + + +/** + * Determines if a value is a promise. + * + * @param {*} val The value to check. + * @return {boolean} true if val is a promise, false otherwise. + */ +function isPromise(val) { + return val && (typeof val.then == 'function'); +} + + +/** + * Runs a callback synchronously against non-promise values and asynchronously + * against promises. Similar to ES6's `Promise.resolve` except that it is + * synchronous when possible and won't wrap the return value. + * + * This is not what you normally want. Normally you want the code to be + * consistently asynchronous, and you want the result wrapped into a promise. + * But because of webdriver's control flow, we're better off not introducing any + * extra layers of promises or asynchronous activity. + * + * @param {*} val The value to call the callback with. + * @param {!Function} callback The callback function + * @return {*} If val isn't a promise, the return value of the callback is + * directly returned. If val is a promise, a promise (generated by val.then) + * resolving to the callback's return value is returned. + */ +var maybePromise = module.exports = function maybePromise(val, callback) { + if (isPromise(val)) { + return val.then(callback); + } else { + return callback(val); + } +} + +maybePromise.isPromise = isPromise; + +/** + * Like maybePromise() but for an array of values. Analogous to `Promise.all`. + * + * @param {!Array<*>} vals An array of values to call the callback with + * @param {!Function} callback the callback function + * @return {*} If nothing in vals is a promise, the return value of the callback + * is directly returned. Otherwise, a promise (generated by the .then + * functions in vals) resolving to the callback's return value is returned. + */ +maybePromise.all = function all(vals, callback) { + var resolved = new Array(vals.length); + function resolveAt(i) { + if (i >= vals.length) { + return callback(resolved); + } else { + return maybePromise(vals[i], function(val) { + resolved[i] = val; + return resolveAt(i+1); + }); + } + } + return resolveAt(0); +} + diff --git a/package.json b/package.json index 1f0390d..c1dd667 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "jasminewd", - "description": "WebDriverJS adapter for Jasmine.", + "name": "jasminewd2", + "description": "WebDriverJS adapter for Jasmine2.", "homepage": "https://github.com/angular/jasminewd", "keywords": [ "test", @@ -12,9 +12,16 @@ ], "author": "Julie Ralph ", "devDependencies": { - "jshint": "2.5.0", - "minijasminenode": "1.1.1", - "selenium-webdriver": "2.43.4" + "@types/jasmine": "^2.5.40", + "@types/node": "^6.0.56", + "@types/selenium-webdriver": "^2.53.38", + "jasmine": "2.4.1", + "jshint": "^2.9.4", + "selenium-webdriver": "3.0.1", + "tslint": "^4.2.0", + "tslint-eslint-rules": "^3.2.3", + "typescript": "^2.0.10", + "vrsource-tslint-rules": "^4.0.0" }, "repository": { "type": "git", @@ -22,9 +29,16 @@ }, "main": "index.js", "scripts": { - "pretest": "node_modules/.bin/jshint index.js spec", - "test": "node node_modules/.bin/minijasminenode spec/adapterSpec.js" + "jshint": "jshint index.js spec", + "tslint": "tslint spec/*.ts", + "lint": "npm run jshint && npm run tslint", + "tsc": "tsc; cp spec/*.js built_spec", + "pretest": "npm run lint && npm run tsc", + "test": "scripts/test.sh" }, "license": "MIT", - "version": "1.1.0" + "engines": { + "node": ">= 6.9.x" + }, + "version": "2.1.0" } diff --git a/scheduler.md b/scheduler.md new file mode 100644 index 0000000..b30e2fc --- /dev/null +++ b/scheduler.md @@ -0,0 +1,68 @@ +# Schedulers + +Many of the core features of jasminewd are centered around automatically synchronizing your tests +with the WebDriver control flow. However, jasminewd can synchronize with any scheduler as long as +it implements the following interface: + +```ts +interface Scheduler { + execute(fn: () => Promise|T): Promise; +} +``` + +Where `execute` is the function used to put something on the scheduler. As long as your scheduler +implements this interface, you can pass it into `require('jasminewd2').init`. + +## Custom Promise Implementation + +Some schedulers need scheduled functions to use a specific implementation of the promise API. For +instance, WebDriver has its `ManagedPromise` implementation, which it needs in order to track +tasks across `then()` blocks. If your scheduler has its own promise implementation, you can +implement the following interface: + +```ts +interface SchedulerWithCustomPromises { + execute(fn: () => CustomPromise|T): CustomPromise; + promise(resolver: (resolve: (T) => void, reject: (any) => void) => void): CustomPromise; +} +``` + +If the `promise` function is specified, jasminewd will use that function to generate all of its +internal promises. If `scheduler.promise` is not specified, jasminewd will try to use WebDriver's +`ManagedPromise`. If `ManagedPromise` is not available (e.g. the control flow is disabled), +jasminewd will default to using native promises. + +### Idle API + +If your scheduler requires a custom promise implementation, it is highly recommended that you +implement the Idle API. This will help to mitigate issues with users who sometimes use other +promise implementations (see https://github.com/angular/jasminewd/issues/68#issuecomment-262317167). +To do this, implement the following interface: + +```ts +var EventEmitter = require('events'); + +interface SchedulerWithIdleAPI extends EventEmitter { + execute(fn: () => CustomPromise|T): CustomPromise; + promise(resolver: (resolve: (T) => void, reject: (any) => void) => void): CustomPromise; + isIdle(): boolean; +} +``` + +Your scheduler must emit `"idle"` when it becomes idle. + + +### Reset API + +If you want your scheduler to be reset whenever a spec times out, implement the following interface: + +```ts +interface SchedulerWithResetAPI { + execute(fn: () => CustomPromise|T): CustomPromise; + reset(): void; +} +``` + +jasminewd will automatically look for a `reset` function and call it when specs time out. This is +useful so that tasks from a timed out spec get cleared instead of continuing to tie up the scheduler +and potentially getting executed during future specs. diff --git a/scripts/test.sh b/scripts/test.sh new file mode 100755 index 0000000..bb80af6 --- /dev/null +++ b/scripts/test.sh @@ -0,0 +1,79 @@ +LIB_SPECS="spec/support/lib_specs.json" +PASSING_SPECS="spec/support/passing_specs.json" +FAILING_SPECS="spec/support/failing_specs.json" +NO_CF_PASSING_SPECS="spec/support/no_cf_passing_specs.json" +NO_CF_FAILING_SPECS="spec/support/no_cf_failing_specs.json" +CMD_BASE="node node_modules/.bin/jasmine JASMINE_CONFIG_PATH=" + +# Run unit tests + +echo "### running all unit tests" +CMD=$CMD_BASE$LIB_SPECS +echo "### $CMD" +$CMD +[ "$?" -eq 0 ] || exit 1 +echo + + +# Run all tests when the control flow is enabled + +export SELENIUM_PROMISE_MANAGER=1 + +echo "### running all passing specs" +CMD=$CMD_BASE$PASSING_SPECS +echo "### $CMD" +$CMD +[ "$?" -eq 0 ] || exit 1 +echo + +EXPECTED_RESULTS="38 specs, 34 failures" +echo "### running all failing specs (expecting $EXPECTED_RESULTS)" +CMD=$CMD_BASE$FAILING_SPECS +echo "### $CMD" +res=`$CMD 2>/dev/null` +results_line=`echo "$res" | tail -2 | head -1` +echo "result: $results_line" +[ "$results_line" = "$EXPECTED_RESULTS" ] || exit 1 + +# Run only the async/await tests when the control flow is disabled + +export SELENIUM_PROMISE_MANAGER=0 + +echo "### running async/await passing specs with control flow disabled" +CMD=$CMD_BASE$NO_CF_PASSING_SPECS +echo "### $CMD" +$CMD +[ "$?" -eq 0 ] || exit 1 +echo + +EXPECTED_RESULTS="19 specs, 17 failures" +echo "### running async/await failing specs (expecting $EXPECTED_RESULTS)" +CMD=$CMD_BASE$NO_CF_FAILING_SPECS +echo "### $CMD" +res=`$CMD 2>/dev/null` +results_line=`echo "$res" | tail -2 | head -1` +echo "result: $results_line" +[ "$results_line" = "$EXPECTED_RESULTS" ] || exit 1 + +# Run only the async/await tests with no scheduler + +export JASMINEWD_TESTS_NO_SCHEDULER=1 + +echo "### running async/await passing specs with no scheduler" +CMD=$CMD_BASE$NO_CF_PASSING_SPECS +echo "### $CMD" +$CMD +[ "$?" -eq 0 ] || exit 1 +echo + +EXPECTED_RESULTS="19 specs, 17 failures" +echo "### running async/await failing specs (expecting $EXPECTED_RESULTS)" +CMD=$CMD_BASE$NO_CF_FAILING_SPECS +echo "### $CMD" +res=`$CMD 2>/dev/null` +results_line=`echo "$res" | tail -2 | head -1` +echo "result: $results_line" +[ "$results_line" = "$EXPECTED_RESULTS" ] || exit 1 + + +echo "all pass" diff --git a/spec/@types_jasminewd2.d.ts b/spec/@types_jasminewd2.d.ts new file mode 100644 index 0000000..dc584b6 --- /dev/null +++ b/spec/@types_jasminewd2.d.ts @@ -0,0 +1,73 @@ +// This is jasminewd's internal version of @types/jasminewd2. If you need types +// for jasminewd2, please use @types/jasminewd2 instead + +// Type definitions for jasminewd2 +// Project https://github.com/angular/jasminewd +// Definitions by: Sammy Jelin + +declare function it(expectation: string, assertion?: () => Promise, timeout?: number): void; +declare function fit(expectation: string, assertion?: () => Promise, timeout?: number): void; +declare function xit(expectation: string, assertion?: () => Promise, timeout?: number): void; +declare function beforeEach(action: () => Promise, timeout?: number): void; +declare function afterEach(action: () => Promise, timeout?: number): void; +declare function beforeAll(action: () => Promise, timeout?: number): void; +declare function afterAll(action: () => Promise, timeout?: number): void; + +declare namespace jasmine { + // The global `Promise` type is too strict and kinda wrong + interface Promise { + then(onFulfill?: (value: T) => U | Promise, onReject?: (error: any) => U | Promise): Promise; + } + + interface Matchers { + toBe(expected: any, expectationFailOutput?: any): Promise; + toEqual(expected: any, expectationFailOutput?: any): Promise; + toMatch(expected: string | RegExp | Promise, expectationFailOutput?: any): Promise; + toBeDefined(expectationFailOutput?: any): Promise; + toBeUndefined(expectationFailOutput?: any): Promise; + toBeNull(expectationFailOutput?: any): Promise; + toBeNaN(): Promise; + toBeTruthy(expectationFailOutput?: any): Promise; + toBeFalsy(expectationFailOutput?: any): Promise; + toHaveBeenCalled(): Promise; + toHaveBeenCalledWith(...params: any[]): Promise; + toHaveBeenCalledTimes(expected: number | Promise): Promise; + toContain(expected: any, expectationFailOutput?: any): Promise; + toBeLessThan(expected: number | Promise, expectationFailOutput?: any): Promise; + toBeLessThanOrEqual(expected: number | Promise, expectationFailOutput?: any): Promise; + toBeGreaterThan(expected: number | Promise, expectationFailOutput?: any): Promise; + toBeGreaterThanOrEqual(expected: number | Promise, expectationFailOutput?: any): Promise; + toBeCloseTo(expected: number | Promise, precision?: any, expectationFailOutput?: any): Promise; + toThrow(expected?: any): Promise; + toThrowError(message?: string | RegExp | Promise): Promise; + toThrowError(expected?: new (...args: any[]) => Error | Promise Error>, message?: string | RegExp | Promise): Promise; + } + + function addMatchers(matchers: AsyncCustomMatcherFactories): void; + + interface Env { + addMatchers(matchers: AsyncCustomMatcherFactories): void; + } + + interface Spec { + addMatchers(matchers: AsyncCustomMatcherFactories): void; + } + + interface AsyncCustomMatcherFactories { + [index: string]: AsyncCustomMatcherFactory; + } + + interface AsyncCustomMatcherFactory { + (util: MatchersUtil, customEqualityTesters: Array): AsyncCustomMatcher; + } + + interface AsyncCustomMatcher { + compare(actual: T, expected: T): AsyncCustomMatcherResult; + compare(actual: any, expected: any): AsyncCustomMatcherResult; + } + + interface AsyncCustomMatcherResult { + pass: boolean | Promise; + message?: string; + } +} diff --git a/spec/adapterSpec.js b/spec/adapterSpec.js index e6fa185..401a667 100644 --- a/spec/adapterSpec.js +++ b/spec/adapterSpec.js @@ -1,5 +1,5 @@ -require('../index.js'); var webdriver = require('selenium-webdriver'); +var common = require('./common.js'); /** * Tests for the WebDriverJS Jasmine-Node Adapter. These tests use @@ -7,100 +7,60 @@ var webdriver = require('selenium-webdriver'); * webdriver. */ -var getFakeDriver = function() { - var flow = webdriver.promise.controlFlow(); - return { - controlFlow: function() { - return flow; - }, - sleep: function(ms) { - return flow.timeout(ms); - }, - setUp: function() { - return flow.execute(function() { - return webdriver.promise.fulfilled('setup done'); - }); - }, - getValueA: function() { - return flow.execute(function() { - return webdriver.promise.delayed(500).then(function() { - return webdriver.promise.fulfilled('a'); - }); - }); - }, - getOtherValueA: function() { - return flow.execute(function() { - return webdriver.promise.fulfilled('a'); - }); - }, - getValueB: function() { - return flow.execute(function() { - return webdriver.promise.fulfilled('b'); - }); - }, - getBigNumber: function() { - return flow.execute(function() { - return webdriver.promise.fulfilled(1111); - }); - }, - getDecimalNumber: function() { - return flow.execute(function() { - return webdriver.promise.fulfilled(3.14159); - }); - }, - getDisplayedElement: function() { - return flow.execute(function() { - return webdriver.promise.fulfilled({ - isDisplayed: function() { - return webdriver.promise.fulfilled(true); - } - }); - }); - }, - getHiddenElement: function() { - return flow.execute(function() { - return webdriver.promise.fulfilled({ - isDisplayed: function() { - return webdriver.promise.fulfilled(false); - } - }); - }); - } - }; -}; - -var fakeDriver = getFakeDriver(); +var fakeDriver = common.getFakeDriver(); describe('webdriverJS Jasmine adapter plain', function() { it('should pass normal synchronous tests', function() { expect(true).toBe(true); }); + + it('should allow an empty it block and mark as pending'); + + xit('should allow a spec marked as pending with xit', function() { + expect(true).toBe(false); + }); }); +describe('context', function() { + beforeEach(function() { + this.foo = 0; + }); + + it('can use the `this` to share state', function() { + expect(this.foo).toEqual(0); + this.bar = 'test pollution?'; + }); + + it('prevents test pollution by having an empty `this` created for the next spec', function() { + expect(this.foo).toEqual(0); + expect(this.bar).toBe(undefined); + }); +}); describe('webdriverJS Jasmine adapter', function() { // Shorten this and you should see tests timing out. - jasmine.getEnv().defaultTimeoutInterval = 2000; + jasmine.DEFAULT_TIMEOUT_INTERVAL = 2000; + var beforeEachMsg; beforeEach(function() { - // 'this' should work properly to add matchers. - this.addMatchers({ - toBeLotsMoreThan: function(expected) { - return this.actual > expected + 100; - }, - // Example custom matcher returning a promise that resolves to true/false. - toBeDisplayed: function() { - return this.actual.isDisplayed(); - } - }); + jasmine.addMatchers(common.getMatchers()); }); beforeEach(function() { fakeDriver.setUp().then(function(value) { - console.log('This should print before each test: ' + value); + beforeEachMsg = value; }); }); + afterEach(function() { + beforeEachMsg = ''; + }); + + it('should only allow initializing once', function() { + expect(require('../index.js').init).toThrow( + Error('JasmineWd already initialized when init() was called')); + }); + it('should pass normal synchronous tests', function() { expect(true).toEqual(true); }); @@ -110,11 +70,15 @@ describe('webdriverJS Jasmine adapter', function() { expect(fakeDriver.getValueB()).toEqual('b'); }); + it('beforeEach should wait for control flow', function() { + expect(beforeEachMsg).toEqual('setup done'); + }); + it('should wait till the expect to run the flow', function() { var promiseA = fakeDriver.getValueA(); - expect(promiseA.isPending()).toBe(true); + expect(common.isPending(promiseA)).toBe(true); expect(promiseA).toEqual('a'); - expect(promiseA.isPending()).toBe(true); + expect(common.isPending(promiseA)).toBe(true); }); it('should compare a promise to a promise', function() { @@ -136,6 +100,8 @@ describe('webdriverJS Jasmine adapter', function() { it('should allow the use of custom matchers', function() { expect(500).toBeLotsMoreThan(3); expect(fakeDriver.getBigNumber()).toBeLotsMoreThan(33); + expect(fakeDriver.getBigNumber()).toBeLotsMoreThan(fakeDriver.getSmallNumber()); + expect(fakeDriver.getSmallNumber()).not.toBeLotsMoreThan(fakeDriver.getBigNumber()); }); it('should allow custom matchers to return a promise', function() { @@ -153,6 +119,34 @@ describe('webdriverJS Jasmine adapter', function() { expect(fakeDriver.getDecimalNumber()).toBeCloseTo(3.14); }); + it('should allow iterating through arrays', function() { + // This is a convoluted test which shows a real issue which + // cropped up in version changes to the selenium-webdriver module. + // See https://github.com/angular/protractor/pull/2263 + var checkTexts = function(webElems) { + var texts = webElems.then(function(arr) { + var results = arr.map(function(webElem) { + return webElem.getText(); + }); + return webdriver.promise.all(results); + }); + + expect(texts).not.toContain('e'); + + return true; + }; + + fakeDriver.getValueList().then(function(list) { + var result = list.map(function(webElem) { + var webElemsPromise = webdriver.promise.when(webElem).then(function(webElem) { + return [webElem]; + }); + return webdriver.promise.fullyResolved(checkTexts(webElemsPromise)); + }); + return webdriver.promise.all(result); + }); + }); + describe('not', function() { it('should still pass normal synchronous tests', function() { expect(4).not.toEqual(5); @@ -165,6 +159,11 @@ describe('webdriverJS Jasmine adapter', function() { it('should compare a promise to a promise', function() { expect(fakeDriver.getValueA()).not.toEqual(fakeDriver.getValueB()); }); + + it('should allow custom matchers to return a promise when actual is not a promise', function() { + expect(fakeDriver.displayedElement).toBeDisplayed(); + expect(fakeDriver.hiddenElement).not.toBeDisplayed(); + }); }); it('should throw an error with a WebElement actual value', function() { @@ -172,25 +171,10 @@ describe('webdriverJS Jasmine adapter', function() { expect(function() { expect(webElement).toEqual(4); - }).toThrow('expect called with WebElement argument, expected a Promise. ' + - 'Did you mean to use .getText()?'); + }).toThrow(Error('expect called with WebElement argument, expected a Promise. ' + + 'Did you mean to use .getText()?')); }); - // Uncomment to see timeout failures. - - // it('should timeout after 200ms', function() { - // expect(fakeDriver.getValueA()).toEqual('a'); - // }, 300); - - // it('should timeout after 300ms', function() { - // fakeDriver.sleep(9999); - // expect(fakeDriver.getValueB()).toEqual('b'); - // }, 300); - - // it('should pass errors from done callback', function(done) { - // done('an error'); - // }); - it('should pass after the timed out tests', function() { expect(fakeDriver.getValueA()).toEqual('a'); }); @@ -217,4 +201,72 @@ describe('webdriverJS Jasmine adapter', function() { }, 500); }); }); + + describe('beforeAll and afterAll', function() { + var asyncValue, setupMsg; + + beforeAll(function(done) { + setTimeout(function() { + asyncValue = 5; + done(); + }, 500); + }); + + beforeAll(function() { + fakeDriver.setUp().then(function(msg) { + setupMsg = msg; + }); + }); + + afterAll(function() { + setupMsg = ''; + }); + + it('should have set asyncValue', function() { + expect(asyncValue).toEqual(5); + }); + + it('should wait for control flow', function() { + expect(setupMsg).toEqual('setup done'); + }); + }); + + describe('it return value', function() { + var spec1 = it('test1'); + var spec2 = it('test2', function() {}); + var spec3 = it('test3', function() {}, 1); + + it('should return the spec', function() { + expect(spec1.description).toBe('test1'); + expect(spec2.description).toBe('test2'); + expect(spec3.description).toBe('test3'); + }); + }); + + describe('native promises', function() { + it('should have done argument override return returned promise', function(done) { + var ret = new Promise(function() {}); + done(); + return ret; + }); + + var currentTest = null; + + it('should wait for webdriver events sent from native promise', function() { + currentTest = 'A'; + return new Promise(function(resolve) { + setTimeout(function() { + fakeDriver.sleep(100).then(function() { + expect(currentTest).toBe('A'); + }); + resolve(); + }, 100); + }); + }); + + it('should not start a test before another finishes', function(done) { + currentTest = 'B'; + setTimeout(done, 200); + }); + }); }); diff --git a/spec/asyncAwaitAdapterSpec.ts b/spec/asyncAwaitAdapterSpec.ts new file mode 100644 index 0000000..54f177f --- /dev/null +++ b/spec/asyncAwaitAdapterSpec.ts @@ -0,0 +1,285 @@ +import {promise as wdpromise, WebElement} from 'selenium-webdriver'; +import {getFakeDriver, getMatchers} from './common.js'; + +/** + * This file is very similar to adapterSpec.ts, but we use async/await instead + * of the WebDriver Control Flow for synchronization. These tests are desgined + * to work regardless of if the WebDriver Control Flow is disabled. + */ + +const fakeDriver = getFakeDriver(); + +/* jshint esversion: 6 */ +describe('webdriverJS Jasmine adapter plain', function() { + it('should pass normal synchronous tests', function() { + expect(true).toBe(true); + }); + + it('should allow an empty it block and mark as pending'); + + xit('should allow a spec marked as pending with xit', function() { + expect(true).toBe(false); + }); +}); + +describe('context', function() { + beforeEach(function() { + this.foo = 0; + }); + + it('can use the `this` to share state', function() { + expect(this.foo).toEqual(0); + this.bar = 'test pollution?'; + }); + + it('prevents test pollution by having an empty `this` created for the next spec', function() { + expect(this.foo).toEqual(0); + expect(this.bar).toBe(undefined); + }); +}); + +describe('webdriverJS Jasmine adapter', function() { + // Shorten this and you should see tests timing out. + jasmine.DEFAULT_TIMEOUT_INTERVAL = 2000; + let beforeEachMsg: string; + + beforeEach(function() { + jasmine.addMatchers(getMatchers()); + }); + + beforeEach(async function() { + await fakeDriver.setUp().then(function(value) { + beforeEachMsg = value; + }); + }); + + afterEach(function() { + beforeEachMsg = ''; + }); + + it('should only allow initializing once', function() { + expect(require('../index.js').init).toThrow( + Error('JasmineWd already initialized when init() was called')); + }); + + it('should pass normal synchronous tests', function() { + expect(true).toEqual(true); + }); + + it('should compare a promise to a primitive', async function() { + // You need `await` before `expect` if the expect needs to unwrap promises + await expect(fakeDriver.getValueA()).toEqual('a'); + await expect(fakeDriver.getValueB()).toEqual('b'); + }); + + it('beforeEach should wait for control flow', async function() { + // But you can also just add `await` wherever you like + await expect(beforeEachMsg).toEqual('setup done'); + }); + + it('should wait till the expect to run the flow', async function() { + const promiseA = fakeDriver.getValueA(); + // isPending() is only defined for WebDriver's ManagedPromise + if (!promiseA.isPending) { + promiseA.isPending = () => { return true; }; + } + + await expect(promiseA.isPending()).toBe(true); + const expectation = expect(promiseA).toEqual('a'); + await expect(promiseA.isPending()).toBe(true); + + // We still need to wait for the expectation to finish, since the control + // flow might be disabled + await expectation; + }); + + it('should compare a promise to a promise', async function() { + await expect(fakeDriver.getValueA()).toEqual(fakeDriver.getOtherValueA()); + }); + + it('should still allow use of the underlying promise', async function() { + const promiseA = fakeDriver.getValueA(); + await promiseA.then(function(value) { + expect(value).toEqual('a'); + }); + }); + + it('should allow scheduling of tasks', async function() { + await fakeDriver.sleep(300); + await expect(fakeDriver.getValueB()).toEqual('b'); + }); + + it('should allow the use of custom matchers', async function() { + await expect(500).toBeLotsMoreThan(3); + await expect(fakeDriver.getBigNumber()).toBeLotsMoreThan(33); + await expect(fakeDriver.getBigNumber()).toBeLotsMoreThan(fakeDriver.getSmallNumber()); + await expect(fakeDriver.getSmallNumber()).not.toBeLotsMoreThan(fakeDriver.getBigNumber()); + }); + + it('should allow custom matchers to return a promise', async function() { + await expect(fakeDriver.getDisplayedElement()).toBeDisplayed(); + await expect(fakeDriver.getHiddenElement()).not.toBeDisplayed(); + }); + + it('should pass multiple arguments to matcher', async function() { + // Passing specific precision + await expect(fakeDriver.getDecimalNumber()).toBeCloseTo(3.1, 1); + await expect(fakeDriver.getDecimalNumber()).not.toBeCloseTo(3.1, 2); + + // Using default precision (2) + await expect(fakeDriver.getDecimalNumber()).not.toBeCloseTo(3.1); + await expect(fakeDriver.getDecimalNumber()).toBeCloseTo(3.14); + }); + + it('should allow iterating through arrays', async function() { + // This is a convoluted test which shows a real issue which + // cropped up in version changes to the selenium-webdriver module. + // See https://github.com/angular/protractor/pull/2263 + const checkTexts = async function(webElems: wdpromise.Promise wdpromise.Promise}>>) { + const texts = webElems.then(function(arr) { + const results = arr.map(function(webElem) { + return webElem.getText(); + }); + return wdpromise.all(results); + }); + + await expect(texts).not.toContain('e'); + + return true; + }; + + await fakeDriver.getValueList().then(function(list) { + const result = list.map(function(webElem) { + const webElemsPromise = wdpromise.when(webElem).then(function(webElem) { + return [webElem]; + }); + return wdpromise.fullyResolved(checkTexts(webElemsPromise)); + }); + return wdpromise.all(result); + }); + }); + + describe('not', async function() { + it('should still pass normal synchronous tests', async function() { + expect(4).not.toEqual(5); + }); + + it('should compare a promise to a primitive', async function() { + await expect(fakeDriver.getValueA()).not.toEqual('b'); + }); + + it('should compare a promise to a promise', async function() { + await expect(fakeDriver.getValueA()).not.toEqual(fakeDriver.getValueB()); + }); + + it('should allow custom matchers to return a promise when actual is not a promise', async function() { + await expect(fakeDriver.displayedElement).toBeDisplayed(); + await expect(fakeDriver.hiddenElement).not.toBeDisplayed(); + }); + }); + + it('should throw an error with a WebElement actual value', function() { + const webElement = new WebElement(fakeDriver as any, 'idstring'); + + expect(function() { + expect(webElement).toEqual(4); + }).toThrow(Error('expect called with WebElement argument, expected a Promise. ' + + 'Did you mean to use .getText()?')); + }); + + it('should pass after the timed out tests', async function() { + await expect(fakeDriver.getValueA()).toEqual('a'); + }); + + describe('should work for both synchronous and asynchronous tests', function() { + let x: number; + + beforeEach(function() { + x = 0; + }); + + afterEach(function() { + expect(x).toBe(1); + }); + + it('should execute a synchronous test', function() { + x = 1; + }); + + it('should execute an asynchronous test', function(done) { + setTimeout(function(){ + x = 1; + done(); + }, 500); + }); + }); + + describe('beforeAll and afterAll', function() { + let asyncValue: number; + let setupMsg: string; + + beforeAll(function(done) { + setTimeout(function() { + asyncValue = 5; + done(); + }, 500); + }); + + beforeAll(async function() { + await fakeDriver.setUp().then(function(msg) { + setupMsg = msg; + }); + }); + + afterAll(function() { + setupMsg = ''; + }); + + it('should have set asyncValue', function() { + expect(asyncValue).toEqual(5); + }); + + it('should wait for control flow', function() { + expect(setupMsg).toEqual('setup done'); + }); + }); + + describe('it return value', function() { + const spec1 = it('test1') as any; + const spec2 = it('test2', function() {}) as any; + const spec3 = it('test3', function() {}, 1) as any; + + it('should return the spec', function() { + expect(spec1.description).toBe('test1'); + expect(spec2.description).toBe('test2'); + expect(spec3.description).toBe('test3'); + }); + }); + + describe('native promises', function() { + it('should have done argument override returned promise', async function(done) { + const ret = new Promise(function() {}); + done(); + await ret; + }); + + let currentTest: string = null; + + it('should wait for webdriver events sent from native promise', function() { + currentTest = 'A'; + return new Promise(function(resolve) { + setTimeout(async function() { + await fakeDriver.sleep(100).then(function() { + expect(currentTest).toBe('A'); + }); + resolve(); + }, 100); + }); + }); + + it('should not start a test before another finishes', function(done) { + currentTest = 'B'; + setTimeout(done, 200); + }); + }); +}); diff --git a/spec/asyncAwaitErrorSpec.ts b/spec/asyncAwaitErrorSpec.ts new file mode 100644 index 0000000..37adebc --- /dev/null +++ b/spec/asyncAwaitErrorSpec.ts @@ -0,0 +1,142 @@ +import {getFakeDriver, getMatchers} from './common.js'; + +/** + * This file is very similar to errorSpec.ts, but we use async/await instead of + * the WebDriver Control Flow for synchronization. These tests are desgined to + * work regardless of if the WebDriver Control Flow is disabled. + */ + +const fakeDriver = getFakeDriver(); + +/* jshint esversion: 6 */ +describe('Timeout cases', function() { + it('should timeout after 200ms', async function(done) { + // The returned promise is ignored and jasminewd will wait for the `done` + // callback to be called + await expect(fakeDriver.getValueA()).toEqual('a'); + }, 200); + + it('should timeout after 300ms', async function() { + await fakeDriver.sleep(9999); + await expect(fakeDriver.getValueB()).toEqual('b'); + }, 300); + + it('should pass after the timed out tests', function() { + expect(true).toEqual(true); + }); +}); + +describe('things that should fail', function() { + beforeEach(function() { + jasmine.addMatchers(getMatchers()); + }); + + it('should pass errors from done callback', function(done) { + done.fail('an error from done.fail'); + }); + + it('should error asynchronously in promise callbacks', async function() { + await fakeDriver.sleep(50).then(function() { + expect(true).toEqual(false); + }); + }); + + it('should error asynchronously within done callback', function(done) { + setTimeout(async function() { + await expect(false).toEqual(true); + done(); + }, 200); + }); + + it('should fail normal synchronous tests', function() { + expect(true).toBe(false); + }); + + it('should fail when an error is thrown', function() { + throw new Error('I am an intentional error'); + }); + + it('should compare a promise to a primitive', async function() { + await expect(fakeDriver.getValueA()).toEqual('d'); + await expect(fakeDriver.getValueB()).toEqual('e'); + }); + + it('should wait till the expect to run the flow', async function() { + const promiseA = fakeDriver.getValueA(); + // isPending() is only defined for WebDriver's ManagedPromise + if (!promiseA.isPending) { + promiseA.isPending = () => { return true; }; + } + + await expect(promiseA.isPending()).toBe(true); + const expectation = expect(promiseA).toEqual('a'); + await expect(promiseA.isPending()).toBe(false); + + // We still need to wait for the expectation to finish, since the control + // flow might be disabled + await expectation; + }); + + it('should compare a promise to a promise', async function() { + await expect(fakeDriver.getValueA()).toEqual(fakeDriver.getValueB()); + }); + + it('should still allow use of the underlying promise', async function() { + const promiseA = fakeDriver.getValueA(); + await promiseA.then(function(value) { + expect(value).toEqual('b'); + }); + }); + + it('should allow scheduling of tasks', async function() { + await fakeDriver.sleep(300); + await expect(fakeDriver.getValueB()).toEqual('c'); + }); + + it('should allow the use of custom matchers', async function() { + await expect(1000).toBeLotsMoreThan(999); + await expect(fakeDriver.getBigNumber()).toBeLotsMoreThan(1110); + await expect(fakeDriver.getBigNumber()).not.toBeLotsMoreThan(fakeDriver.getSmallNumber()); + await expect(fakeDriver.getSmallNumber()).toBeLotsMoreThan(fakeDriver.getBigNumber()); + }); + + it('should allow custom matchers to return a promise', async function() { + await expect(fakeDriver.getDisplayedElement()).not.toBeDisplayed(); + await expect(fakeDriver.getHiddenElement()).toBeDisplayed(); + }); + + it('should pass multiple arguments to matcher', async function() { + // Passing specific precision + await expect(fakeDriver.getDecimalNumber()).toBeCloseTo(3.5, 1); + + // Using default precision (2) + await expect(fakeDriver.getDecimalNumber()).toBeCloseTo(3.1); + await expect(fakeDriver.getDecimalNumber()).not.toBeCloseTo(3.14); + }); + + describe('native promises', function() { + it('should time out if done argument is never called, even if promise is returned', + async function(done) { + await new Promise(function() {}); + } + ); + + let testADone = false; + + it('should handle rejection from native promise', function() { + return new Promise(async function(resolve, reject) { + setTimeout(async function() { + await fakeDriver.sleep(100).then(function() { + testADone = true; + }); + reject('Rejected promise'); + }, 100); + }); + }); + + it('should not start a test before another finishes', function(done) { + expect(testADone).toBe(true); // this test actually passes + setTimeout(done, 200); + }); + }); +}); diff --git a/spec/common.ts b/spec/common.ts new file mode 100644 index 0000000..b99f142 --- /dev/null +++ b/spec/common.ts @@ -0,0 +1,141 @@ +import {promise as wdpromise, WebElement} from 'selenium-webdriver'; + +const flow = wdpromise.controlFlow(); +require('../index.js').init(process.env['JASMINEWD_TESTS_NO_SCHEDULER'] ? null : flow, + require('selenium-webdriver')); + +export function getFakeDriver() { + return { + controlFlow: function() { + return flow; + }, + sleep: function(ms: number) { + return flow.timeout(ms); + }, + setUp: function() { + return flow.execute(function() { + return wdpromise.when('setup done'); + }, 'setUp'); + }, + getValueA: function() { + return flow.execute(function() { + return wdpromise.delayed(500).then(function() { + return wdpromise.when('a'); + }); + }, 'getValueA'); + }, + getOtherValueA: function() { + return flow.execute(function() { + return wdpromise.when('a'); + }, 'getOtherValueA'); + }, + getValueB: function() { + return flow.execute(function() { + return wdpromise.when('b'); + }, 'getValueB'); + }, + getBigNumber: function(): wdpromise.Promise { + return flow.execute(function() { + return wdpromise.when(1111); + }, 'getBigNumber'); + }, + getSmallNumber: function(): wdpromise.Promise { + return flow.execute(function() { + return wdpromise.when(11); + }, 'getSmallNumber'); + }, + getDecimalNumber: function(): wdpromise.Promise { + return flow.execute(function() { + return wdpromise.when(3.14159); + }, 'getDecimalNumber'); + }, + getDisplayedElement: function() { + return flow.execute(function() { + return wdpromise.when({ + isDisplayed: function() { + return wdpromise.when(true); + } + }); + }, 'getDisplayedElement'); + }, + getHiddenElement: function() { + return flow.execute(function() { + return wdpromise.when({ + isDisplayed: function() { + return wdpromise.when(false); + } + }); + }, 'getHiddenElement'); + }, + getValueList: function(): wdpromise.Promise wdpromise.Promise}>> { + return flow.execute(function() { + return wdpromise.when([{ + getText: function() { + return flow.execute(function() { return wdpromise.when('a');}); + } + }, { + getText: function() { + return flow.execute(function() { return wdpromise.when('b');}); + } + }, { + getText: function() { + return flow.execute(function() { return wdpromise.when('c');}); + } + }, { + getText: function() { + return flow.execute(function() { return wdpromise.when('d');}); + } + }]); + }, 'getValueList'); + }, + displayedElement: { + isDisplayed: function() { + return wdpromise.when(true); + } + }, + hiddenElement: { + isDisplayed: function() { + return wdpromise.when(false); + } + } + }; +}; + +export function getMatchers() { + return { + toBeLotsMoreThan: function() { + return { + compare: function(actual: number, expected: number) { + return { + pass: actual > expected + 100 + }; + } + }; + }, + // Example custom matcher returning a promise that resolves to true/false. + toBeDisplayed: function() { + return { + compare: function(actual: WebElement, expected: void) { + return { + pass: actual.isDisplayed() + }; + } + }; + } + }; +}; + +// declare custom matcher types +declare global { + namespace jasmine { + interface Matchers { + toBeLotsMoreThan(expected: number | Promise): Promise; + toBeDisplayed(): Promise; + } + } +} + + +export function isPending(managedPromise: wdpromise.Promise) { + return (managedPromise as any).state_ === 'pending'; +}; diff --git a/spec/errorSpec.js b/spec/errorSpec.js new file mode 100644 index 0000000..bdee761 --- /dev/null +++ b/spec/errorSpec.js @@ -0,0 +1,131 @@ +var webdriver = require('selenium-webdriver'); +var common = require('./common.js'); + +/** + * Error tests for the WebDriverJS Jasmine-Node Adapter. These tests use + * WebDriverJS's control flow and promises without setting up the whole + * webdriver. + */ + +var fakeDriver = common.getFakeDriver(); + +describe('Timeout cases', function() { + it('should timeout after 200ms', function(done) { + expect(fakeDriver.getValueA()).toEqual('a'); + }, 200); + + it('should timeout after 300ms', function() { + fakeDriver.sleep(9999); + expect(fakeDriver.getValueB()).toEqual('b'); + }, 300); + + it('should pass after the timed out tests', function() { + expect(true).toEqual(true); + }); +}); + +describe('things that should fail', function() { + beforeEach(function() { + jasmine.addMatchers(common.getMatchers()); + }); + + it('should pass errors from done callback', function(done) { + done.fail('an error from done.fail'); + }); + + it('should error asynchronously in promise callbacks', function() { + fakeDriver.sleep(50).then(function() { + expect(true).toEqual(false); + }); + }); + + it('should error asynchronously within done callback', function(done) { + setTimeout(function() { + expect(false).toEqual(true); + done(); + }, 200); + }); + + it('should fail normal synchronous tests', function() { + expect(true).toBe(false); + }); + + it('should fail when an error is thrown', function() { + throw new Error('I am an intentional error'); + }); + + it('should compare a promise to a primitive', function() { + expect(fakeDriver.getValueA()).toEqual('d'); + expect(fakeDriver.getValueB()).toEqual('e'); + }); + + it('should wait till the expect to run the flow', function() { + var promiseA = fakeDriver.getValueA(); + expect(promiseA.isPending()).toBe(true); + expect(promiseA).toEqual('a'); + expect(promiseA.isPending()).toBe(false); + }); + + it('should compare a promise to a promise', function() { + expect(fakeDriver.getValueA()).toEqual(fakeDriver.getValueB()); + }); + + it('should still allow use of the underlying promise', function() { + var promiseA = fakeDriver.getValueA(); + promiseA.then(function(value) { + expect(value).toEqual('b'); + }); + }); + + it('should allow scheduling of tasks', function() { + fakeDriver.sleep(300); + expect(fakeDriver.getValueB()).toEqual('c'); + }); + + it('should allow the use of custom matchers', function() { + expect(1000).toBeLotsMoreThan(999); + expect(fakeDriver.getBigNumber()).toBeLotsMoreThan(1110); + expect(fakeDriver.getBigNumber()).not.toBeLotsMoreThan(fakeDriver.getSmallNumber()); + expect(fakeDriver.getSmallNumber()).toBeLotsMoreThan(fakeDriver.getBigNumber()); + }); + + it('should allow custom matchers to return a promise', function() { + expect(fakeDriver.getDisplayedElement()).not.toBeDisplayed(); + expect(fakeDriver.getHiddenElement()).toBeDisplayed(); + }); + + it('should pass multiple arguments to matcher', function() { + // Passing specific precision + expect(fakeDriver.getDecimalNumber()).toBeCloseTo(3.5, 1); + + // Using default precision (2) + expect(fakeDriver.getDecimalNumber()).toBeCloseTo(3.1); + expect(fakeDriver.getDecimalNumber()).not.toBeCloseTo(3.14); + }); + + describe('native promises', function() { + it('should time out if done argument is never called, even if promise is returned', + function(done) { + return new Promise(function() {}); + } + ); + + var testADone = false; + + it('should handle rejection from native promise', function() { + return new Promise(function(resolve, reject) { + setTimeout(function() { + fakeDriver.sleep(100).then(function() { + testADone = true; + }); + reject('Rejected promise'); + }, 100); + }); + }); + + it('should not start a test before another finishes', function(done) { + expect(testADone).toBe(true); // this test actually passes + setTimeout(done, 200); + }); + }); +}); diff --git a/spec/maybePromiseSpec.js b/spec/maybePromiseSpec.js new file mode 100644 index 0000000..1503530 --- /dev/null +++ b/spec/maybePromiseSpec.js @@ -0,0 +1,166 @@ +var maybePromise = require('../maybePromise.js'); +var webdriver = require('selenium-webdriver'); + +describe('maybePromise', function() { + // Helper values + var num = 588.79; // From Math.random() + var str = 'qqqpqc0'; // From math.random().toString(36); + var obj = { num: num, str: str, obj: obj, then: true }; + function idFun(x) { return x; } + function promiseMe(x) { + var promise = { then: function(callback) { return callback(x); } }; + spyOn(promise, 'then').and.callThrough(); + return promise; + } + + it('should be able to tell promises from non-promises', function() { + expect(maybePromise.isPromise(num)).toBe(false); + expect(maybePromise.isPromise(str)).toBe(false); + expect(maybePromise.isPromise(obj)).toBe(false); + expect(maybePromise.isPromise(idFun)).toBe(false); + expect(maybePromise.isPromise(promiseMe(num))).toBe(true); + expect(maybePromise.isPromise(promiseMe(str))).toBe(true); + expect(maybePromise.isPromise(promiseMe(obj))).toBe(true); + expect(maybePromise.isPromise(promiseMe(idFun))).toBe(true); + }); + + describe('singletons', function() { + it('should be able to use non-promises', function(done) { + maybePromise(num, function(n) { + expect(n).toBe(num); + done(); + }); + }); + + it('should not wrap non-promise values', function() { + expect(maybePromise(num, idFun)).toBe(num); + expect(maybePromise(str, idFun)).toBe(str); + expect(maybePromise(obj, idFun)).toBe(obj); + }); + + it('should be able to use promises', function(done) { + maybePromise(promiseMe(str), function(s) { + expect(s).toBe(str); + done(); + }); + }); + + it('should use a promise\'s own then() function without any wrapping', function() { + var promise = promiseMe(num); + var callback = jasmine.createSpy('callback', idFun).and.callThrough(); + expect(maybePromise(promise, callback)).toBe(num); + expect(promise.then).toHaveBeenCalled(); + expect(callback).toHaveBeenCalled(); + + promise = promiseMe(str); + callback.calls.reset(); + expect(maybePromise(promise, callback)).toBe(str); + expect(promise.then).toHaveBeenCalled(); + expect(callback).toHaveBeenCalled(); + + promise = promiseMe(obj); + callback.calls.reset(); + expect(maybePromise(promise, callback)).toBe(obj); + expect(promise.then).toHaveBeenCalled(); + expect(callback).toHaveBeenCalled(); + }); + + it('should work with a real promise implementation', function(done) { + var promise = webdriver.promise.when(str); + maybePromise(promise, function(s) { + expect(s).toBe(str); + return webdriver.promise.when(num); + }).then(function(n) { + expect(n).toEqual(num); + done(); + }); + }); + + it('should fail in an expected way with poorly implemented promises', function() { + var badPromise = promiseMe(obj); + badPromise.then.and.stub(); + badPromise.then.and.returnValue(str); + var callback = jasmine.createSpy('callback'); + var ret = maybePromise(badPromise, callback); + expect(badPromise.then).toHaveBeenCalled(); + expect(callback).not.toHaveBeenCalled(); + expect(ret).toBe(str); + }); + }); + + describe('.all', function() { + it('should work with an empty array, without wrapping', function() { + var callback = jasmine.createSpy('callback', idFun).and.callThrough(); + expect(maybePromise.all([], callback)).toEqual([]); + expect(callback).toHaveBeenCalled(); + }); + + it('should work with an array of non-promises', function(done) { + var arr = [num, str, obj]; + maybePromise.all(arr, function(a) { + expect(a).toEqual(arr); + done(); + }); + }); + + it('should not wrap non-promise values', function() { + var arr = [num, str, obj]; + expect(maybePromise.all(arr, idFun)).toEqual(arr); + }); + + it('should work with array of promises', function(done) { + var arr = [num, str, obj]; + maybePromise.all(arr.map(promiseMe), function(a) { + expect(a).toEqual(arr); + done(); + }); + }); + + it('should use promise\'s own then() function without any wrapping', function() { + var arr = [num, str, obj]; + var promiseArr = arr.map(promiseMe); + var callback = jasmine.createSpy('callback', idFun).and.callThrough(); + expect(maybePromise.all(promiseArr, callback)).toEqual(arr); + expect(callback.calls.count()).toBe(1); + for (var i = 0; i < promiseArr.length; i++) { + expect(promiseArr[i].then.calls.count()).toBe(1); + } + }); + + it('should work with a real promise implementation', function(done) { + var arr = [str, obj]; + maybePromise.all(arr.map(webdriver.promise.when), function(a) { + expect(a).toEqual(arr); + return webdriver.promise.when(num); + }).then(function(n) { + expect(n).toEqual(num); + done(); + }); + }); + + it('should work with a mix of promises and non-promises', function(done) { + var arr = [num, promiseMe(str), webdriver.promise.when(obj), + webdriver.promise.when(str), webdriver.promise.when(num), + str, promiseMe(num), obj, promiseMe(obj)]; // Random order + maybePromise.all(arr, function(resolved) { + maybePromise(webdriver.promise.all(arr), function(wdResolved) { + expect(resolved).toEqual(wdResolved); + done(); + }); + }); + }); + + it('should fail in an expected way with poorly implemented promises', function() { + var arr = [num, promiseMe(str), str, promiseMe(num), obj, promiseMe(obj)]; // Random order + var badPromise = promiseMe(obj); + badPromise.then.and.stub(); + badPromise.then.and.returnValue(str); + arr.push(badPromise); + var callback = jasmine.createSpy('callback'); + var ret = maybePromise.all(arr, callback); + expect(badPromise.then).toHaveBeenCalled(); + expect(callback).not.toHaveBeenCalled(); + expect(ret).toBe(str); + }); + }); +}); diff --git a/spec/support/failing_specs.json b/spec/support/failing_specs.json new file mode 100644 index 0000000..03954c5 --- /dev/null +++ b/spec/support/failing_specs.json @@ -0,0 +1,6 @@ +{ + "spec_dir": "built_spec", + "spec_files": [ + "*rrorSpec.js" + ] +} diff --git a/spec/support/lib_specs.json b/spec/support/lib_specs.json new file mode 100644 index 0000000..91e9cf4 --- /dev/null +++ b/spec/support/lib_specs.json @@ -0,0 +1,6 @@ +{ + "spec_dir": "built_spec", + "spec_files": [ + "maybePromiseSpec.js" + ] +} diff --git a/spec/support/no_cf_failing_specs.json b/spec/support/no_cf_failing_specs.json new file mode 100644 index 0000000..a3ef0b4 --- /dev/null +++ b/spec/support/no_cf_failing_specs.json @@ -0,0 +1,6 @@ +{ + "spec_dir": "built_spec", + "spec_files": [ + "asyncAwaitErrorSpec.js" + ] +} diff --git a/spec/support/no_cf_passing_specs.json b/spec/support/no_cf_passing_specs.json new file mode 100644 index 0000000..ec49d46 --- /dev/null +++ b/spec/support/no_cf_passing_specs.json @@ -0,0 +1,6 @@ +{ + "spec_dir": "built_spec", + "spec_files": [ + "asyncAwaitAdapterSpec.js" + ] +} diff --git a/spec/support/passing_specs.json b/spec/support/passing_specs.json new file mode 100644 index 0000000..3c91a6e --- /dev/null +++ b/spec/support/passing_specs.json @@ -0,0 +1,6 @@ +{ + "spec_dir": "built_spec", + "spec_files": [ + "*dapterSpec.js" + ] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..ec87d58 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2015", + "module": "commonjs", + "moduleResolution": "node", + "sourceMap": false, + "declaration": false, + "removeComments": false, + "noImplicitAny": true, + "outDir": "built_spec" + }, + "include": [ + "spec/*.ts" + ] +} diff --git a/tslint.json b/tslint.json new file mode 100644 index 0000000..5935ad9 --- /dev/null +++ b/tslint.json @@ -0,0 +1,15 @@ +{ + "rulesDirectory": [ + "node_modules/vrsource-tslint-rules/rules", + "node_modules/tslint-eslint-rules/dist/rules" + ], + "rules": { + "no-duplicate-imports": true, + "no-duplicate-variable": true, + "no-jasmine-focus": true, + "no-var-keyword": true, + "semicolon": [true], + "variable-name": [true, "ban-keywords"], + "no-inner-declarations": [true, "function"] + } +}