UNPKG

cold-brew

Version:

A testing module for TDD with apps that use the RTCPeerConnection API

375 lines (319 loc) 12.2 kB
/* eslint no-use-before-define: 0 */ /* eslint no-param-reassign: 0 */ /* eslint no-undef: 0 */ /* eslint no-unused-vars: 0 */ /* eslint prefer-arrow-callback: 0 */ /* eslint func-names: 0 */ /* eslint no-shadow: 0 */ 'use strict'; const selenium = require('selenium-webdriver'); const { By } = selenium; /** * createClient - Creates and returns a webdriver object with the coldBrew * methods attached, allowing it to listen for RTCPeerConnection events * that occur in the browser. * * @return webdriver A selenium webdriver object with the coldBrew * methods attached */ function createClient() { let client; // const client = new selenium.Builder() // .usingServer() // .withCapabilities({ // browserName: 'chrome', // chromeOptions: { // args: [ // '--use-fake-ui-for-media-stream', // ], // }, // }) // .build(); if (process.env.SAUCE_USERNAME !== undefined) { client = new selenium.Builder() .usingServer('http://' + process.env.SAUCE_USERNAME + ':' + process.env.SAUCE_ACCESS_KEY + '@ondemand.saucelabs.com:80/wd/hub') .withCapabilities({ 'tunnel-identifier': process.env.TRAVIS_JOB_NUMBER, build: process.env.TRAVIS_BUILD_NUMBER, username: process.env.SAUCE_USERNAME, accessKey: process.env.SAUCE_ACCESS_KEY, browserName: 'chrome', chromeOptions: { args: [ '--use-fake-ui-for-media-stream', ], }, }).build(); } else { client = new selenium.Builder() .usingServer() .withCapabilities({ browserName: 'chrome', chromeOptions: { args: [ '--use-fake-ui-for-media-stream', ], }, }) .build(); } return addColdBrewMethods(client); } /** * addColdBrewMethods - Attaches the coldBrew methods to a webdriver instance, * which will allow it to listen for RTCPeerCOnnection events to occur in the * browser. * * @param {type} client description * @return {type} description */ function addColdBrewMethods(client) { /** * untilRTCEvents - allows the webdriver to wait until certain * RTCPeerCOnnection events to occur before proceeding * * @param ...Strings events A list of the names of the events that * the client should wait for before proceeding * * @return function A function that returns a promise to execute a * script in the browser that checks whether or not the provided RTC events * exist in the window.RTCEvents object. This function will be executed * repeatedly if it is used within a webdriver.wait() call, * e.g. client.wait(client.untilRTCEvents('signalingstatechange', 'iceconnectionstatechange')) */ client.untilRTCEvents = function (events, options = {}) { const { inOrder, label } = options; if (inOrder && typeof inOrder !== 'boolean') { throw new TypeError( `Invalid option passed into untilRTCEvents: inOrder: ${inOrder}` ); } if (label && typeof label !== 'string') { throw new TypeError( `Invalid option passed into untilRTCEvents: label: ${label}` ); } // Needs to return a plain function instead of a promise so that // it will be executed repeatedly within the client.wait() function // // The plain function should return true if the events we are waiting // for exist on the window object, false otherwise. return function () { return client.executeScript(function (events, options) { const { inOrder, label } = options; // Check to make sure coldBrewData has been initialized if (!(window.coldBrewData && window.coldBrewData.RTCEvents)) { return false; } const loggedEvents = label ? window.coldBrewData.peerConnections[label] : window.coldBrewData.RTCEvents; loggedEventNames = loggedEvents.map(event => event.type); // Handle the case where the user doesn't care if the events // happened in a certain order if (!inOrder) { return events.every(eventName => loggedEventNames.includes(eventName)); } else { return sameElementsInSameOrder(events, loggedEventNames); } function sameElementsInSameOrder(arr1, arr2) { let remainingArr2 = arr2; return arr1.reduce((truth, element) => { if (!truth) return false; const index = remainingArr2.indexOf(element); if (index === -1) { return false; } remainingArr2 = remainingArr2.slice(index); return true; }, true); } }, events, options); }; }; /** * waitUntilRTCEvents - convenience function to simplify the usage * of client.untilRTCEvents * * @param events description * @return {type} description */ client.waitUntilRTCEvents = function (events, options, timeout) { return client.wait(client.untilRTCEvents(events, options), timeout); }; client.untilSendSignaling = function (events, options = {}) { // Needs to return a plain function instead of a promise so that // it will be executed repeatedly within the client.wait() function // // The plain function should return true if the events we are waiting // for exist on the window object, false otherwise. const { inOrder } = options; if (inOrder && typeof inOrder !== 'boolean') { throw new TypeError( `Invalid option passed into untilSendSignaling: inOrder: ${inOrder}` ); } return function () { return client.executeScript(function (events, inOrder) { // Check to make sure coldBrewData has been initialized if (!(window.coldBrewData && window.coldBrewData.socketEvents)) { return false; } if (!inOrder) { const outgoingSocketEvents = window.coldBrewData.socketEvents.outgoing .map(event => event.type); return events.every(eventName => outgoingSocketEvents.includes(eventName)); } const socketEvents = window.coldBrewData.socketEvents.outgoing .map(event => event.type); return sameElementsInSameOrder(events, socketEvents); function sameElementsInSameOrder(arr1, arr2) { let remainingArr2 = arr2; return arr1.reduce((truth, element) => { if (!truth) return false; const index = remainingArr2.indexOf(element); if (index === -1) { return false; } remainingArr2 = remainingArr2.slice(index); return true; }, true); } }, events, inOrder); }; }; client.waitUntilSendSignaling = function (events, options, timeout) { return client.wait(client.untilSendSignaling(events, options), timeout); }; client.untilRecieveSignaling = function (events, options = {}) { const { inOrder } = options; if (inOrder && typeof inOrder !== 'boolean') { throw new TypeError( `Invalid option passed into untilRecieveSignaling: inOrder: ${inOrder}` ); } return function () { return client.executeScript(function (events, inOrder) { if (!(window.coldBrewData && window.coldBrewData.socketEvents)) { return false; } if (!inOrder) { const incomingSocketEvents = window.coldBrewData.socketEvents.incoming .map(event => event.type); return events.every(eventName => incomingSocketEvents.includes(eventName)); } const socketEvents = window.coldBrewData.socketEvents.incoming .map(event => event.type); return sameElementsInSameOrder(events, socketEvents); function sameElementsInSameOrder(arr1, arr2) { let remainingArr2 = arr2; return arr1.reduce((truth, element) => { if (!truth) return false; const index = remainingArr2.indexOf(element); if (index === -1) { return false; } remainingArr2 = remainingArr2.slice(index); return true; }, true); } }, events, inOrder); }; }; client.waitUntilReceiveSignaling = function (events, options, timeout) { return client.wait(client.untilRecieveSignaling(events, options), timeout); }; client.untilDataChannelEvents = function (events, options = {}) { const { inOrder } = options; if (inOrder && typeof inOrder !== 'boolean') { throw new TypeError( `Invalid option passed into untilDataChannelEvents: inOrder: ${inOrder}` ); } return function () { return client.executeScript(function (events, inOrder) { if (!(window.coldBrewData && window.coldBrewData.RTCDataChannelEvents)) { return false; } if (!inOrder) { const RTCDataChannelEvents = window.coldBrewData.RTCDataChannelEvents.map(event => event.type); return events.every(eventName => RTCDataChannelEvents.includes(eventName)); } }, events, inOrder); } } client.waitUntilDataChannelEvents = function (events, options, timeout) { return client.wait(client.untilDataChannelEvents(events, options), timeout); }; /** * findElementByAttributes - allows the webdriver to locate elements * by a css selector and then filter those elements by other attributes * that they possess. * * @param String selector A css selector for the element to find * * @param Object attributes An object whose keys are DOM node attribute * names and values are the corresponding values of the desired element * * @return Promise A promise that resolves with the first element * that is located by the given css selector AND possesses the given * attributes */ client.findElementByAttributes = function (selector, attributes) { return client.findElement(By.js(function (selector, attributes) { let elements = Array.from(document.querySelectorAll(selector)); Object.keys(attributes).forEach(attribute => { elements = elements.filter(element => element[attribute] === attributes[attribute]); }); return elements[0]; }, selector, attributes || {})); }; /** * client - Schedules several navigation tasks at once * * Convenience method to allow the webdriver to more easily * navigate throughout the website * * @param Array navigationEvents An array of subarrays, each subarray * representing one navigation task. The navigation tasks are of the form * [action, selector, atributes], e.g. * ['click', 'button.login', {innerText: 'Login'}] * * The allowable actions are 'click', and 'sendKeys'. If 'sendKeys' is given, * a fourth element should be included in the array indicating the keys to * be sent, e.g. * ['sendKeys', 'input', {placeholder: 'username'}, 'dking'] * * @return undefined */ client.do = function (navigationEvents) { const validInput = navigationEvents.every(event => validNavigation(event)); if (!validInput) throw new TypeError('Navigation events'); const navigationPromises = navigationEvents.reduce((arr, event) => { const action = event[0]; const selector = event[1]; const attributes = event[2]; const keys = event[3]; // May be undefined if no 3rd item was given const navigationPromise = client.findElementByAttributes(selector, attributes) .then((element) => element[action](keys)) .catch((err) => { throw new TypeError( `No element found with selector ${selector} and attributes ${JSON.stringify(attributes)}` ); }); arr.push(navigationPromise); return arr; }, []); return selenium.promise.all(navigationPromises); }; function validNavigation(navigationEvent) { return Array.isArray(navigationEvent) && ['click', 'sendKeys'].includes(navigationEvent[0]); } return client; } class ColdBrewError extends Error { } module.exports = { createClient, addColdBrewMethods };