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'); await this.addStyleTag({ content: ` *, *::after, *::before { transition-delay: 0s !important; transition-duration: 0s !important; animation-delay: -0.0001s !important; animation-duration: 0s !important; animation-play-state: paused !important; caret-color: transparent !important; }` }); }, 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 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.waitForFunction(state => { let $state = angular.element(document.body).injector().get('$state'); return !$state.transition && $state.is(state); }, {}, state); await this.waitForFunction(() => { return angular.element(() => { return true; }); }); await this.waitForSpinnerLoad(); }, waitForTransition: async function() { await this.waitForFunction(() => { const $state = angular.element(document.body).injector().get('$state'); return !$state.transition; }); await this.waitForSpinnerLoad(); }, accessToSection: async function(state, name = 'Others') { 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 li[name="${name}"]`; await this.evaluate(selector => { document.querySelector(selector).scrollIntoViewIfNeeded(); }, selector); await this.waitToClick(selector); await this.waitForSelector('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="launch"]'); await this.accessToSection(state); }, forceReloadSection: async function(sectionRoute) { await this.waitToClick('vn-icon[icon="launch"]'); await this.waitToClick('button[response="accept"]'); await this.waitForSelector('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.waitForSelector('.vn-descriptor'); }, getProperty: async function(selector, property) { return 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 handle.jsonValue(); }, waitPropertyLength: async function(selector, property, minLength) { await this.waitForFunction((selector, property, minLength) => { const element = document.querySelector(selector); const isValidElement = element && element[property] != null && element[property] !== ''; return isValidElement && element[property].length >= minLength; }, {}, selector, property, minLength); return this.getProperty(selector, property); }, expectPropertyValue: async function(selector, property, value) { let builtSelector = selector; if (property != 'innerText') builtSelector = await this.selectorFormater(selector); try { return 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 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 this.click(selector); }, writeOnEditableTD: async function(selector, text) { let builtSelector = await this.selectorFormater(selector); await this.waitToClick(selector); await this.waitForSelector(builtSelector, {visible: true}); await this.type(builtSelector, text); await this.keyboard.press('Enter'); }, focusElement: async function(selector) { await this.waitForSelector(selector); return this.evaluate(selector => { let element = document.querySelector(selector); element.focus(); }, selector); }, isVisible: async function(selector) { await this.waitForSelector(selector); return this.evaluate(checkVisibility, selector); }, waitImgLoad: async function(selector) { await this.waitForSelector(selector); return this.waitForFunction(selector => { const imageReady = document.querySelector(selector).complete; return imageReady; }, {}, selector); }, countElement: async function(selector) { return this.evaluate(selector => { return document.querySelectorAll(selector).length; }, selector); }, waitForNumberOfElements: async function(selector, count) { try { await this.waitForFunction((selector, count) => { return document.querySelectorAll(selector).length == count; }, {}, selector, count); } catch (error) { const amount = await this.countElement(selector); throw new Error(`actual amount of elements was: ${amount} instead of ${count}, ${error}`); } }, waitForClassNotPresent: async function(selector, className) { await this.waitForSelector(selector); return this.waitForFunction((selector, className) => { if (!document.querySelector(selector).classList.contains(className)) return true; }, {}, selector, className); }, waitForClassPresent: async function(selector, className) { await this.waitForSelector(selector); return this.waitForFunction((elementSelector, targetClass) => { if (document.querySelector(elementSelector).classList.contains(targetClass)) return true; }, {}, selector, className); }, waitForTextInElement: async function(selector, text) { await this.waitForFunction((selector, text) => { if (document.querySelector(selector)) { const innerText = document.querySelector(selector).innerText.toLowerCase(); const expectedText = text.toLowerCase(); if (innerText.includes(expectedText)) return innerText; } }, {}, selector, text); }, waitForTextInField: async function(selector, text) { const builtSelector = await this.selectorFormater(selector); const expectedValue = text.toLowerCase(); try { await this.waitForFunction((selector, text) => { const element = document.querySelector(selector); if (element) { const value = element.value.toLowerCase(); if (value.includes(text)) return true; } }, {}, builtSelector, expectedValue); } catch (error) { throw new Error(`${text} wasn't the value of ${builtSelector}, ${error}`); } }, selectorFormater: function(selector) { if (selector.includes('vn-textarea')) return `${selector} textarea`; if (selector.includes('vn-input-file')) return `${selector} section`; return `${selector} input`; }, waitForInnerText: async function(selector) { await this.waitForSelector(selector, {}); await this.waitForFunction(selector => { const innerText = document.querySelector(selector).innerText; return innerText != null && innerText != ''; }, {}, selector); return this.evaluate(selector => { return document.querySelector(selector).innerText; }, selector); }, waitForEmptyInnerText: async function(selector) { return this.waitFunction(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.waitForTimeout(300); await this.evaluate(() => { let hideButton = document .querySelector('vn-snackbar .shape.shown button'); if (hideButton) return hideButton.click(); }); await this.waitForSelector('vn-snackbar .shape.shown', {hidden: true}); }, waitForSnackbar: async function() { const selector = 'vn-snackbar .shape.shown'; await this.waitForSelector(selector); const 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); message.isSuccess = message.type == 'success'; await this.hideSnackbar(); return message; }, pickDate: async function(selector, date) { date = date || Date.vnNew(); const timeZoneOffset = date.getTimezoneOffset() * 60000; const localDate = (new Date(date.getTime() - timeZoneOffset)) .toISOString().substr(0, 10); await this.waitForSelector(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.waitForSelector(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); }, 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.waitForSelector('.vn-drop-down', {hidden: true}); }, checkboxState: async function(selector) { await this.waitForSelector(selector); const value = await this.getInputValue(selector); switch (value) { case null: return 'intermediate'; case true: return 'checked'; default: return 'unchecked'; } }, isDisabled: async function(selector) { await this.waitForSelector(selector); return this.evaluate(selector => { let element = document.querySelector(selector); return element.$ctrl.disabled; }, selector); }, waitForStylePresent: async function(selector, property, value) { return this.waitForFunction((selector, property, value) => { const element = document.querySelector(selector); return element.style[property] == value; }, {}, selector, property, value); }, waitForSpinnerLoad: async function() { await this.waitForSelector('vn-topbar vn-spinner', {hidden: true}); }, waitForWatcherData: async function(selector) { await this.waitForSelector(selector); await this.waitForFunction(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.waitForSelector('.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(); }, async getInputValue(selector) { return this.evaluate(selector => { const input = document.querySelector(selector); return input.$ctrl.field; }, selector); }, async getValue(selector) { return await this.waitToGetProperty(selector, 'value'); }, async innerText(selector) { const element = await this.$(selector); const handle = await element.getProperty('innerText'); return handle.jsonValue(); }, async setInput(selector, value) { const input = await this.$(selector); const tagName = (await input.evaluate(e => e.tagName)).toLowerCase(); switch (tagName) { case 'vn-textfield': case 'vn-account-number': case 'vn-datalist': case 'vn-input-number': await this.clearInput(selector); if (value) await this.write(selector, value.toString()); break; case 'vn-autocomplete': case 'vn-worker-autocomplete': if (value) await this.autocompleteSearch(selector, value.toString()); else await this.clearInput(selector); break; case 'vn-date-picker': if (value) await this.pickDate(selector, value); else await this.clearInput(selector); break; case 'vn-input-time': if (value) await this.pickTime(selector, value); else await this.clearInput(selector); break; case 'vn-check': for (let i = 0; i < 3; i++) { if (await this.getInput(selector) == value) break; await this.click(selector); } break; } }, async getInput(selector) { const input = await this.$(selector); const tagName = (await input.evaluate(e => e.tagName)).toLowerCase(); let el; let value; switch (tagName) { case 'vn-textfield': case 'vn-account-number': case 'vn-autocomplete': case 'vn-worker-autocomplete': case 'vn-input-time': case 'vn-datalist': el = await input.$('input'); value = await el.getProperty('value'); return value.jsonValue(); case 'vn-check': case 'vn-input-number': return await this.getInputValue(selector); case 'vn-textarea': el = await input.$('textarea'); value = await el.getProperty('value'); return value.jsonValue(); case 'vn-date-picker': el = await input.$('input'); value = await el.getProperty('value'); if (value) { const date = new Date(await value.jsonValue()); date.setUTCHours(0, 0, 0, 0); return date; } else return null; default: value = await this.innerText(selector); return value.jsonValue(); } }, async clearInput(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); }, async fetchForm(selector, inputNames) { const values = {}; for (const inputName of inputNames) values[inputName] = await this.getInput(`${selector} [vn-name="${inputName}"]`); return values; }, async fillForm(selector, values) { for (const inputName in values) await this.setInput(`${selector} [vn-name="${inputName}"]`, values[inputName]); }, async sendForm(selector, values) { if (values) await this.fillForm(selector, values); await this.click(`${selector} button[type=submit]`); return await this.waitForSnackbar(); } }; 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(', '); const myErr = new Error(`${err.message}\n at Page.${name}(${stringArgs})`); myErr.stack = err.stack; throw myErr; } }; } page.wait = page.waitFor; return page; } export default actions;