UNPKG

selenium-webdriver

Version:

The official WebDriver JavaScript bindings from the Selenium project

267 lines (241 loc) 8.51 kB
// 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 Defines a handful of utility functions to simplify working * with promises. */ 'use strict' const { isObject, isPromise } = require('./util') /** * Creates a promise that will be resolved at a set time in the future. * @param {number} ms The amount of time, in milliseconds, to wait before * resolving the promise. * @return {!Promise<void>} The promise. */ function delayed(ms) { return new Promise((resolve) => setTimeout(resolve, ms)) } /** * Wraps a function that expects a node-style callback as its final * argument. This callback expects two arguments: an error value (which will be * null if the call succeeded), and the success value as the second argument. * The callback will the resolve or reject the returned promise, based on its * arguments. * @param {!Function} fn The function to wrap. * @param {...?} args The arguments to apply to the function, excluding the * final callback. * @return {!Thenable} A promise that will be resolved with the * result of the provided function's callback. */ function checkedNodeCall(fn, ...args) { return new Promise(function (fulfill, reject) { try { fn(...args, function (error, value) { error ? reject(error) : fulfill(value) }) } catch (ex) { reject(ex) } }) } /** * Registers a listener to invoke when a promise is resolved, regardless * of whether the promise's value was successfully computed. This function * is synonymous with the {@code finally} clause in a synchronous API: * * // Synchronous API: * try { * doSynchronousWork(); * } finally { * cleanUp(); * } * * // Asynchronous promise API: * doAsynchronousWork().finally(cleanUp); * * __Note:__ similar to the {@code finally} clause, if the registered * callback returns a rejected promise or throws an error, it will silently * replace the rejection error (if any) from this promise: * * try { * throw Error('one'); * } finally { * throw Error('two'); // Hides Error: one * } * * let p = Promise.reject(Error('one')); * promise.finally(p, function() { * throw Error('two'); // Hides Error: one * }); * * @param {!IThenable<?>} promise The promise to add the listener to. * @param {function(): (R|IThenable<R>)} callback The function to call when * the promise is resolved. * @return {!Promise<R>} A promise that will be resolved with the callback * result. * @template R */ async function thenFinally(promise, callback) { try { await Promise.resolve(promise) return callback() } catch (e) { await callback() throw e } } /** * Calls a function for each element in an array and inserts the result into a * new array, which is used as the fulfillment value of the promise returned * by this function. * * If the return value of the mapping function is a promise, this function * will wait for it to be fulfilled before inserting it into the new array. * * If the mapping function throws or returns a rejected promise, the * promise returned by this function will be rejected with the same reason. * Only the first failure will be reported; all subsequent errors will be * silently ignored. * * @param {!(Array<TYPE>|IThenable<!Array<TYPE>>)} array The array to iterate * over, or a promise that will resolve to said array. * @param {function(this: SELF, TYPE, number, !Array<TYPE>): ?} fn The * function to call for each element in the array. This function should * expect three arguments (the element, the index, and the array itself. * @param {SELF=} self The object to be used as the value of 'this' within `fn`. * @template TYPE, SELF */ async function map(array, fn, self = undefined) { const v = await Promise.resolve(array) if (!Array.isArray(v)) { throw TypeError('not an array') } const arr = /** @type {!Array} */ (v) const values = [] for (const [index, item] of arr.entries()) { values.push(await Promise.resolve(fn.call(self, item, index, arr))) } return values } /** * Calls a function for each element in an array, and if the function returns * true adds the element to a new array. * * If the return value of the filter function is a promise, this function * will wait for it to be fulfilled before determining whether to insert the * element into the new array. * * If the filter function throws or returns a rejected promise, the promise * returned by this function will be rejected with the same reason. Only the * first failure will be reported; all subsequent errors will be silently * ignored. * * @param {!(Array<TYPE>|IThenable<!Array<TYPE>>)} array The array to iterate * over, or a promise that will resolve to said array. * @param {function(this: SELF, TYPE, number, !Array<TYPE>): ( * boolean|IThenable<boolean>)} fn The function * to call for each element in the array. * @param {SELF=} self The object to be used as the value of 'this' within `fn`. * @template TYPE, SELF */ async function filter(array, fn, self = undefined) { const v = await Promise.resolve(array) if (!Array.isArray(v)) { throw TypeError('not an array') } const arr = /** @type {!Array} */ (v) const values = [] for (const [index, item] of arr.entries()) { const isConditionTrue = await Promise.resolve(fn.call(self, item, index, arr)) if (isConditionTrue) { values.push(item) } } return values } /** * Returns a promise that will be resolved with the input value in a * fully-resolved state. If the value is an array, each element will be fully * resolved. Likewise, if the value is an object, all keys will be fully * resolved. In both cases, all nested arrays and objects will also be * fully resolved. All fields are resolved in place; the returned promise will * resolve on {@code value} and not a copy. * * Warning: This function makes no checks against objects that contain * cyclical references: * * var value = {}; * value['self'] = value; * promise.fullyResolved(value); // Stack overflow. * * @param {*} value The value to fully resolve. * @return {!Thenable} A promise for a fully resolved version * of the input value. */ async function fullyResolved(value) { value = await Promise.resolve(value) if (Array.isArray(value)) { return fullyResolveKeys(/** @type {!Array} */ (value)) } if (isObject(value)) { return fullyResolveKeys(/** @type {!Object} */ (value)) } if (typeof value === 'function') { return fullyResolveKeys(/** @type {!Object} */ (value)) } return value } /** * @param {!(Array|Object)} obj the object to resolve. * @return {!Thenable} A promise that will be resolved with the * input object once all of its values have been fully resolved. */ async function fullyResolveKeys(obj) { const isArray = Array.isArray(obj) const numKeys = isArray ? obj.length : Object.keys(obj).length if (!numKeys) { return obj } async function forEachProperty(obj, fn) { for (let key in obj) { await fn(obj[key], key) } } async function forEachElement(arr, fn) { for (let i = 0; i < arr.length; i++) { await fn(arr[i], i) } } const forEachKey = isArray ? forEachElement : forEachProperty await forEachKey(obj, async function (partialValue, key) { if (!Array.isArray(partialValue) && (!partialValue || typeof partialValue !== 'object')) { return } obj[key] = await fullyResolved(partialValue) }) return obj } // PUBLIC API module.exports = { checkedNodeCall, delayed, filter, finally: thenFinally, fullyResolved, isPromise, map, }