function checkVisibility(selector) { let selectorMatches = document.querySelectorAll(selector); let element = selectorMatches[0]; if (selectorMatches.length > 1) throw new Error(`Multiple matches of ${selector} found`); let isVisible = false; if (element) { let eventHandler = event => { event.preventDefault(); isVisible = true; }; element.addEventListener('mouseover', eventHandler); let rect = element.getBoundingClientRect(); let x = rect.left + rect.width / 2; let y = rect.top + rect.height / 2; let elementInCenter = document.elementFromPoint(x, y); let elementInTopLeft = document.elementFromPoint(rect.left, rect.top); let elementInBottomRight = document.elementFromPoint(rect.right, rect.bottom); let e = new MouseEvent('mouseover', { view: window, bubbles: true, cancelable: true, }); if (elementInCenter) elementInCenter.dispatchEvent(e); if (elementInTopLeft) elementInTopLeft.dispatchEvent(e); if (elementInBottomRight) elementInBottomRight.dispatchEvent(e); element.removeEventListener('mouseover', eventHandler); } return isVisible; } let actions = { clickIfExists: async function(selector) { let exists; try { exists = await this.waitForSelector(selector, {timeout: 500}); } catch (error) { exists = false; } if (exists) await this.waitToClick(selector); return exists; }, expectURL: async function(expectedHash) { try { await this.waitForFunction(expectedHash => { return document.location.hash.includes(expectedHash); }, {}, expectedHash); } catch (error) { throw new Error(`Failed to reach URL containing: ${expectedHash}`); } await this.waitForSpinnerLoad(); return true; }, doLogin: async function(userName, password = 'nightmare') { let state = await this.getState(); if (state != 'login') { try { await this.gotoState('login'); } catch (err) { let dialog = await this.evaluate( () => document.querySelector('button[response="accept"]')); if (dialog) await this.waitToClick('button[response="accept"]'); else throw err; } } await this.waitForState('login'); await this.waitForSelector(`vn-login vn-textfield[ng-model="$ctrl.user"]`, {visible: true}); await this.clearInput(`vn-login vn-textfield[ng-model="$ctrl.user"]`); await this.write(`vn-login vn-textfield[ng-model="$ctrl.user"]`, userName); await this.clearInput(`vn-login vn-textfield[ng-model="$ctrl.password"]`); await this.write(`vn-login vn-textfield[ng-model="$ctrl.password"]`, password); await this.waitToClick('vn-login button[type=submit]'); }, login: async function(userName) { await this.doLogin(userName); await this.waitForState('home'); }, selectModule: async function(moduleName) { let state = `${moduleName}.index`; await this.waitToClick(`vn-home a[ui-sref="${state}"]`); await this.waitForState(state); }, loginAndModule: async function(userName, moduleName) { await this.login(userName); await this.selectModule(moduleName); }, getState: async function() { return await this.evaluate(() => { let $state = angular.element(document.body).injector().get('$state'); return $state.current.name; }); }, gotoState: async function(state, params) { await this.evaluate((state, params) => { let $state = angular.element(document.body).injector().get('$state'); return $state.go(state, params); }, state, params); await this.waitForSpinnerLoad(); }, waitForState: async function(state) { await this.waitFor(state => { let $state = angular.element(document.body).injector().get('$state'); return !$state.transition && $state.is(state); }, {}, state); await this.waitForSpinnerLoad(); }, waitForTransition: async function() { await this.waitFor(() => { const $state = angular.element(document.body).injector().get('$state'); return !$state.transition; }); await this.waitForSpinnerLoad(); }, accessToSection: async function(state) { await this.waitForSelector('vn-left-menu'); let nested = await this.evaluate(state => { return document.querySelector(`vn-left-menu li li > a[ui-sref="${state}"]`) != null; }, state); if (nested) { let selector = 'vn-left-menu vn-item-section > vn-icon[icon=keyboard_arrow_down]'; await this.evaluate(selector => { document.querySelector(selector).scrollIntoViewIfNeeded(); }, selector); await this.waitToClick(selector); await this.wait('vn-left-menu .expanded'); } await this.evaluate(state => { let navButton = document.querySelector(`vn-left-menu li > a[ui-sref="${state}"]`); navButton.scrollIntoViewIfNeeded(); return navButton.click(); }, state); await this.waitForState(state); }, reloadSection: async function(state) { await this.click('vn-icon[icon="desktop_windows"]'); await this.accessToSection(state); }, forceReloadSection: async function(sectionRoute) { await this.waitToClick('vn-icon[icon="desktop_windows"]'); await this.waitToClick('button[response="accept"]'); await this.wait('vn-card.summary'); await this.waitToClick(`vn-left-menu li > a[ui-sref="${sectionRoute}"]`); }, doSearch: async function(searchValue) { await this.clearInput('vn-searchbar'); if (searchValue) await this.write('vn-searchbar', searchValue); await this.waitToClick('vn-searchbar vn-icon[icon="search"]'); await this.waitForTransition(); }, accessToSearchResult: async function(searchValue) { await this.doSearch(searchValue); await this.waitFor('.vn-descriptor'); }, getProperty: async function(selector, property) { return await this.evaluate((selector, property) => { return document.querySelector(selector)[property].replace(/\s+/g, ' ').trim(); }, selector, property); }, getClassName: async function(selector) { const element = await this.$(selector); const handle = await element.getProperty('className'); return await handle.jsonValue(); }, waitPropertyLength: async function(selector, property, minLength) { await this.waitForFunction((selector, property, minLength) => { const element = document.querySelector(selector); return element && element[property] != null && element[property] !== '' && element[property].length >= minLength; }, {}, selector, property, minLength); return await this.getProperty(selector, property); }, expectPropertyValue: async function(selector, property, value) { let builtSelector = selector; if (property != 'innerText') builtSelector = await this.selectorFormater(selector); try { return await this.waitForFunction((selector, property, value) => { const element = document.querySelector(selector); return element[property] == value; }, {}, builtSelector, property, value); } catch (error) { throw new Error(`${value} wasn't the value of ${builtSelector}, ${error}`); } }, waitToGetProperty: async function(selector, property) { let builtSelector = selector; if (selector.includes('vn-input-file') || property != 'innerText') builtSelector = await this.selectorFormater(selector); try { await this.waitForFunction((selector, property) => { const element = document.querySelector(selector); return element && element[property] != null && element[property] !== ''; }, {}, builtSelector, property); return await this.getProperty(builtSelector, property); } catch (error) { throw new Error(`couldn't get property: ${property} of ${builtSelector}, ${error}`); } }, write: async function(selector, text) { let builtSelector = await this.selectorFormater(selector); await this.waitForSelector(selector, {}); await this.type(builtSelector, text); await this.waitForTextInField(selector, text); }, overwrite: async function(selector, text) { await this.clearInput(selector); await this.write(selector, text); }, waitToClick: async function(selector) { await this.waitForSelector(selector); await this.waitForFunction(checkVisibility, {}, selector); return await this.click(selector); }, writeOnEditableTD: async function(selector, text) { let builtSelector = await this.selectorFormater(selector); await this.waitToClick(selector); await this.type(builtSelector, text); await this.keyboard.press('Enter'); }, focusElement: async function(selector) { await this.wait(selector); return await this.evaluate(selector => { let element = document.querySelector(selector); element.focus(); }, selector); }, isVisible: async function(selector) { await this.waitForSelector(selector); return await this.evaluate(checkVisibility, selector); }, waitImgLoad: async function(selector) { await this.wait(selector); return await this.wait(selector => { const imageReady = document.querySelector(selector).complete; return imageReady; }, {}, selector); }, countElement: async function(selector) { return await this.evaluate(selector => { return document.querySelectorAll(selector).length; }, selector); }, waitForNumberOfElements: async function(selector, count) { return await this.waitForFunction((selector, count) => { return document.querySelectorAll(selector).length == count; }, {}, selector, count); }, waitForClassNotPresent: async function(selector, className) { await this.wait(selector); return await this.wait((selector, className) => { if (!document.querySelector(selector).classList.contains(className)) return true; }, {}, selector, className); }, waitForClassPresent: async function(selector, className) { await this.wait(selector); return await this.wait((elementSelector, targetClass) => { if (document.querySelector(elementSelector).classList.contains(targetClass)) return true; }, {}, selector, className); }, waitForTextInElement: async function(selector, text) { const expectedText = text.toLowerCase(); return new Promise((resolve, reject) => { let attempts = 0; const interval = setInterval(async() => { const currentText = await this.evaluate(selector => { return document.querySelector(selector).innerText.toLowerCase(); }, selector); if (currentText === expectedText || attempts === 40) { clearInterval(interval); resolve(currentText); } attempts += 1; }, 100); }).then(result => { return expect(result).toContain(expectedText); }); }, selectorFormater: function(selector) { if (selector.includes('vn-textarea')) return `${selector} textarea`; if (selector.includes('vn-input-file')) return `${selector} section`; return `${selector} input`; }, waitForTextInField: async function(selector, text) { let builtSelector = await this.selectorFormater(selector); await this.waitForSelector(builtSelector); return await this.waitForFunction((selector, text) => { return document.querySelector(selector).value.toLowerCase().includes(text.toLowerCase()); }, {}, builtSelector, text); }, waitForInnerText: async function(selector) { await this.waitForSelector(selector, {}); await this.waitForFunction(selector => { const innerText = document.querySelector(selector).innerText; return innerText != null && innerText != ''; }, {}, selector); return await this.evaluate(selector => { return document.querySelector(selector).innerText; }, selector); }, waitForEmptyInnerText: async function(selector) { return await this.wait(selector => { return document.querySelector(selector).innerText == ''; }, selector); }, hideSnackbar: async function() { // Holds up for the snackbar to be visible for a small period of time. if (process.env.E2E_DEBUG) await this.waitFor(300); await this.evaluate(() => { let hideButton = document .querySelector('vn-snackbar .shape.shown button'); if (hideButton) return hideButton.click(); }); await this.waitFor('vn-snackbar .shape.shown', {hidden: true}); }, waitForSnackbar: async function() { const selector = 'vn-snackbar .shape.shown'; await this.waitForSelector(selector); let message = await this.evaluate(selector => { const shape = document.querySelector(selector); const message = { text: shape.querySelector('.text').innerText }; const types = ['error', 'success']; for (let type of types) { if (shape.classList.contains(type)) { message.type = type; break; } } return message; }, selector); await this.hideSnackbar(); return message; }, pickDate: async function(selector, date) { date = date || new Date(); const timeZoneOffset = date.getTimezoneOffset() * 60000; const localDate = (new Date(date.getTime() - timeZoneOffset)) .toISOString().substr(0, 10); await this.wait(selector); await this.evaluate((selector, localDate) => { let input = document.querySelector(selector).$ctrl.input; input.value = localDate; input.dispatchEvent(new Event('change')); }, selector, localDate); }, pickTime: async function(selector, time) { await this.wait(selector); await this.evaluate((selector, time) => { let input = document.querySelector(selector).$ctrl.input; input.value = time; input.dispatchEvent(new Event('change')); }, selector, time); }, clearTextarea: async function(selector) { await this.waitForSelector(selector, {visible: true}); await this.evaluate(inputSelector => { return document.querySelector(`${inputSelector} textarea`).value = ''; }, selector); }, clearInput: async function(selector) { await this.waitForSelector(selector); let field = await this.evaluate(selector => { return document.querySelector(`${selector} input`).closest('.vn-field').$ctrl.field; }, selector); if ((field != null && field != '') || field == '0') { let coords = await this.evaluate(selector => { let rect = document.querySelector(selector).getBoundingClientRect(); return {x: rect.x + (rect.width / 2), y: rect.y + (rect.height / 2), width: rect.width}; }, selector); await this.mouse.move(coords.x, coords.y); await this.waitForSelector(`${selector} [icon="clear"]`, {visible: true}); await this.waitToClick(`${selector} [icon="clear"]`); } await this.evaluate(selector => { return document.querySelector(`${selector} input`).closest('.vn-field').$ctrl.field == ''; }, selector); }, autocompleteSearch: async function(selector, searchValue) { let builtSelector = await this.selectorFormater(selector); await this.waitToClick(selector); await this.write('.vn-drop-down.shown vn-textfield', searchValue); try { await this.waitForFunction((selector, searchValue) => { let element = document .querySelector(`${selector} vn-drop-down`).$ctrl.content .querySelector('li.active'); if (element) return element.innerText.toLowerCase().includes(searchValue.toLowerCase()); }, {}, selector, searchValue); } catch (error) { let inputValue = await this.evaluate(() => { return document.querySelector('.vn-drop-down.shown vn-textfield input').value; }); throw new Error(`${builtSelector} value is ${inputValue}! ${error}`); } await this.keyboard.press('Enter'); await this.waitForFunction((selector, searchValue) => { return document.querySelector(selector).value.toLowerCase() .includes(searchValue.toLowerCase()); }, {}, builtSelector, searchValue); await this.waitFor('.vn-drop-down', {hidden: true}); }, checkboxState: async function(selector) { await this.wait(selector); return await this.evaluate(selector => { let checkbox = document.querySelector(selector); switch (checkbox.$ctrl.field) { case null: return 'intermediate'; case true: return 'checked'; default: return 'unchecked'; } }, selector); }, isDisabled: async function(selector) { await this.waitForSelector(selector); return await this.evaluate(selector => { let element = document.querySelector(selector); return element.$ctrl.disabled; }, selector); }, waitForStylePresent: async function(selector, property, value) { return await this.wait((selector, property, value) => { const element = document.querySelector(selector); return element.style[property] == value; }, {}, selector, property, value); }, waitForSpinnerLoad: async function() { await this.waitFor('vn-topbar vn-spinner', {hidden: true}); }, waitForWatcherData: async function(selector) { await this.wait(selector); await this.wait(selector => { let watcher = document.querySelector(selector); let orgData = watcher.$ctrl.orgData; return !angular.equals({}, orgData) && orgData != null; }, {}, selector); await this.waitForSpinnerLoad(); }, waitForMutation: async function(selector, type) { try { await this.evaluate((selector, type) => { return new Promise(resolve => { const config = {attributes: true, childList: true, subtree: true}; const target = document.querySelector(selector); const onEnd = function(mutationsList, observer) { resolve(); observer.disconnect(); }; const observer = new MutationObserver(onEnd); observer.expectedType = type; observer.observe(target, config); }); }, selector, type); } catch (error) { throw new Error(`failed to wait for mutation type: ${type}`); } }, waitForTransitionEnd: async function(selector) { await this.evaluate(selector => { return new Promise(resolve => { const transition = document.querySelector(selector); const onEnd = function() { transition.removeEventListener('transitionend', onEnd); resolve(); }; transition.addEventListener('transitionend', onEnd); }); }, selector); }, closePopup: async function() { await Promise.all([ this.keyboard.press('Escape'), this.waitFor('.vn-popup', {hidden: true}), ]); }, respondToDialog: async function(response) { await this.waitForSelector('.vn-dialog.shown'); const firstCount = await this.evaluate(text => { const dialogs = document.querySelectorAll('.vn-dialog'); const dialogOnTop = dialogs[dialogs.length - 1]; const button = dialogOnTop.querySelector(`div.buttons [response="${text}"]`); button.click(); return dialogs.length; }, response); await this.waitForFunction(firstCount => { const dialogs = document.querySelectorAll('.vn-dialog'); return dialogs.length < firstCount; }, {}, firstCount); }, waitForContentLoaded: async function() { await this.waitForSpinnerLoad(); } }; export function extendPage(page) { for (let name in actions) { page[name] = async(...args) => { try { return await actions[name].apply(page, args); } catch (err) { let stringArgs = args .map(i => typeof i == 'function' ? 'Function' : i) .join(', '); throw new Error(`.${name}(${stringArgs}): ${err.message}`); } }; } page.wait = page.waitFor; return page; } export default actions;