diff --git a/e2e/chromeOpts.js b/e2e/chromeOpts.js index f5447a0912e3ef936136ed0cbc91b17ba00b3f2e..7e4076611c1484a12ce26fd9b26f5e2246d1920d 100644 --- a/e2e/chromeOpts.js +++ b/e2e/chromeOpts.js @@ -3,7 +3,7 @@ const { width, height } = require('./opts') module.exports = [ ...(process.env.DISABLE_CHROME_HEADLESS ? [] : ['--headless']), '--no-sandbox', - '--disable-gpu', + ...(process.env.ENABLE_GPU ? []: ['--disable-gpu']), '--disable-setuid-sandbox', "--disable-extensions", `--window-size=${width},${height}`, diff --git a/e2e/src/advanced/browsingForDatasets.prod.e2e-spec.js b/e2e/src/advanced/browsingForDatasets.prod.e2e-spec.js index 87099a58e463dedc82b7c57680badd71f30013c5..beb15ab520ef7820ada5ecfc0e676db7c36babad 100644 --- a/e2e/src/advanced/browsingForDatasets.prod.e2e-spec.js +++ b/e2e/src/advanced/browsingForDatasets.prod.e2e-spec.js @@ -1,7 +1,6 @@ const { AtlasPage } = require('../util') -const { ARIA_LABELS } = require('../../../common/constants') +const { CONST } = require('../../../common/constants') const { retry } = require('../../../common/util') -const { TOGGLE_EXPLORE_PANEL, MODALITY_FILTER, DOWNLOAD_PREVIEW, DOWNLOAD_PREVIEW_CSV } = ARIA_LABELS const atlasName = `Multilevel Human Atlas` @@ -110,7 +109,7 @@ describe('> dataset browser', () => { await iavPage.selectSearchRegionAutocompleteWithText() await retry(async () => { await iavPage.dismissModal() - await iavPage._setRegionalFeaturesExpanded(true) + await iavPage.toggleExpansionPanelState(`${CONST.REGIONAL_FEATURES}`) }, { timeout: 2000, retries: 10 @@ -126,153 +125,3 @@ describe('> dataset browser', () => { }) } }) - -const template = 'ICBM 2009c Nonlinear Asymmetric' -const area = 'Area hOc1 (V1, 17, CalcS)' - -const receptorName = `Density measurements of different receptors for Area hOc1 (V1, 17, CalcS) [human, v1.0]` - -// describe('> receptor dataset previews', () => { -// let iavPage -// beforeEach(async () => { -// iavPage = new AtlasPage() -// await iavPage.init() -// await iavPage.goto() -// await iavPage.selectTitleCard(template) -// await iavPage.wait(500) -// await iavPage.waitUntilAllChunksLoaded() - -// await iavPage.searchRegionWithText(area) -// await iavPage.wait(2000) -// await iavPage.selectSearchRegionAutocompleteWithText() -// await iavPage.dismissModal() -// await iavPage.searchRegionWithText('') - -// const datasets = await iavPage.getVisibleDatasets() -// const receptorIndex = datasets.indexOf(receptorName) - -// await iavPage.clickNthDataset(receptorIndex) -// await iavPage.wait(500) -// await iavPage.click(`[aria-label="${ARIA_LABELS.SHOW_DATASET_PREVIEW}"]`) -// await iavPage.waitFor(true, true) -// }) - -// describe('> can display graph', () => { - -// it('> can display radar graph', async () => { -// const files = await iavPage.getBottomSheetList() -// const fingerprintIndex = files.findIndex(file => /fingerprint/i.test(file)) -// await iavPage.clickNthItemFromBottomSheetList(fingerprintIndex) -// await iavPage.waitFor(true, true) -// const modalHasCanvas = await iavPage.modalHasChild('canvas') -// expect(modalHasCanvas).toEqual(true) - -// await iavPage.wait(500) - -// const modalHasDownloadBtn = await iavPage.modalHasChild(`[aria-label="${DOWNLOAD_PREVIEW}"]`) -// const modalHasDownloadCSVBtn = await iavPage.modalHasChild(`[aria-label="${DOWNLOAD_PREVIEW_CSV}"]`) - -// expect(modalHasDownloadBtn).toEqual(true) -// expect(modalHasDownloadCSVBtn).toEqual(true) -// }) - -// it('> can display profile', async () => { - -// const files = await iavPage.getBottomSheetList() -// const profileIndex = files.findIndex(file => /profile/i.test(file)) -// await iavPage.clickNthItemFromBottomSheetList(profileIndex) -// await iavPage.waitFor(true, true) -// const modalHasCanvas = await iavPage.modalHasChild('canvas') -// expect(modalHasCanvas).toEqual(true) - -// await iavPage.wait(500) - -// const modalHasDownloadBtn = await iavPage.modalHasChild(`[aria-label="${DOWNLOAD_PREVIEW}"]`) -// const modalHasDownloadCSVBtn = await iavPage.modalHasChild(`[aria-label="${DOWNLOAD_PREVIEW_CSV}"]`) - -// expect(modalHasDownloadBtn).toEqual(true) -// expect(modalHasDownloadCSVBtn).toEqual(true) -// }) -// }) -// it('> can display image', async () => { -// const files = await iavPage.getBottomSheetList() -// const imageIndex = files.findIndex(file => /image\//i.test(file)) -// await iavPage.clickNthItemFromBottomSheetList(imageIndex) -// await iavPage.wait(500) -// const modalHasImage = await iavPage.modalHasChild('div[data-img-src]') -// expect(modalHasImage).toEqual(true) - -// await iavPage.wait(500) - -// const modalHasDownloadBtn = await iavPage.modalHasChild(`[aria-label="${DOWNLOAD_PREVIEW}"]`) -// const modalHasDownloadCSVBtn = await iavPage.modalHasChild(`[aria-label="${DOWNLOAD_PREVIEW_CSV}"]`) - -// expect(modalHasDownloadBtn).toEqual(true) -// expect(modalHasDownloadCSVBtn).toEqual(false) -// }) -// }) - -// describe('> modality picker', () => { -// let iavPage -// beforeAll(async () => { -// iavPage = new AtlasPage() -// await iavPage.init() -// await iavPage.goto() -// }) -// it('> sorted alphabetically', async () => { -// await iavPage.selectTitleCard(templates[1]) -// await iavPage.wait(500) -// await iavPage.waitUntilAllChunksLoaded() -// await iavPage.click(`[aria-label="${TOGGLE_EXPLORE_PANEL}"]`) -// await iavPage.wait(500) -// await iavPage.clearAlerts() -// await iavPage.click(`[aria-label="${MODALITY_FILTER}"]`) -// await iavPage.wait(500) -// const modalities = await iavPage.getModalities() -// for (let i = 1; i < modalities.length; i ++) { -// expect( -// modalities[i].charCodeAt(0) -// ).toBeGreaterThanOrEqual( -// modalities[i - 1].charCodeAt(0) -// ) -// } -// }) -// }) - - -// describe('> pmap dataset preview', () => { -// let iavPage - -// beforeAll(async () => { -// // loads pmap and centers on hot spot -// const url = `/?templateSelected=MNI+152+ICBM+2009c+Nonlinear+Asymmetric&parcellationSelected=JuBrain+Cytoarchitectonic+Atlas&cNavigation=0.0.0.-W000..2_ZG29.-ASCS.2-8jM2._aAY3..BSR0..dABI~.525x0~.7iMV..1EPC&niftiLayers=https%3A%2F%2Fneuroglancer.humanbrainproject.eu%2Fprecomputed%2FJuBrain%2F17%2Ficbm152casym%2Fpmaps%2FVisual_hOc1_l_N10_nlin2MNI152ASYM2009C_2.4_publicP_d3045ee3c0c4de9820eb1516d2cc72bb.nii.gz&previewingDatasetFiles=%5B%7B"datasetId"%3A"minds%2Fcore%2Fdataset%2Fv1.0.0%2F5c669b77-c981-424a-858d-fe9f527dbc07"%2C"filename"%3A"Area+hOc1+%28V1%2C+17%2C+CalcS%29+%5Bv2.4%2C+ICBM+2009c+Asymmetric%2C+left+hemisphere%5D"%7D%5D` -// iavPage = new AtlasPage() -// await iavPage.init() -// await iavPage.goto(url) -// await iavPage.waitUntilAllChunksLoaded() -// }) - -// it('> can display pmap', async () => { -// const { red, green, blue } = await iavPage.getRgbAt({position: [200, 597]}) -// expect(red).toBeGreaterThan(green) -// expect(red).toBeGreaterThan(blue) -// }) - -// it('> on update of layer control, pmap retains', async () => { -// // by default, additional layer control is collapsed -// // await iavPage.toggleLayerControl() // deprecated -// await iavPage.wait(500) -// await iavPage.toggleNthLayerControl(0) -// await iavPage.wait(5500) - -// // interact with control -// await iavPage.click(`[aria-label="Remove background"]`) -// await iavPage.wait(500) - -// // color map should be unchanged -// const { red, green, blue } = await iavPage.getRgbAt({position: [200, 597]}) -// expect(red).toBeGreaterThan(green) -// expect(red).toBeGreaterThan(blue) - -// }) -// }) diff --git a/e2e/src/advanced/nonAtlasImages.prod.e2e-spec.js b/e2e/src/advanced/nonAtlasImages.prod.e2e-spec.js index d9e46c2364b528cbe9c5d44000861c63a766bab2..3277e69e5a4328233addc63260b9fd06e876eb5b 100644 --- a/e2e/src/advanced/nonAtlasImages.prod.e2e-spec.js +++ b/e2e/src/advanced/nonAtlasImages.prod.e2e-spec.js @@ -1,5 +1,6 @@ const { AtlasPage } = require('../util') const { URLSearchParams } = require('url') +const { ARIA_LABELS } = require('../../../common/constants') describe('> non-atlas images', () => { let iavPage @@ -196,9 +197,15 @@ describe('> non-atlas images', () => { await iavPage.goto(`/?${searchParam.toString()}`) await iavPage.wait(2000) - const additionalLayerControlIsShown = await iavPage.additionalLayerControlIsVisible() - - expect(additionalLayerControlIsShown).toEqual(false) + try { + const additionalLayerControlIsShown = await iavPage.isVisible(`[aria-label="${ARIA_LABELS.ADDITIONAL_VOLUME_CONTROL}"]`) + expect(additionalLayerControlIsShown).toEqual(false) + } catch (e) { + /** + * error when css querying additional volume control + * expected behaviour, when it is not visible + */ + } }) @@ -219,7 +226,7 @@ describe('> non-atlas images', () => { await iavPage.goto(`/?${searchParam.toString()}`, { forceTimeout: 20000 }) await iavPage.wait(2000) - const additionalLayerCtrlIsExpanded2 = await iavPage.additionalLayerControlIsExpanded() + const additionalLayerCtrlIsExpanded2 = await iavPage.isVisible(`[aria-label="${ARIA_LABELS.ADDITIONAL_VOLUME_CONTROL}"]`) expect(additionalLayerCtrlIsExpanded2).toEqual(true) }) diff --git a/e2e/src/navigating/originDataset.prod.e2e-spec.js b/e2e/src/navigating/originDataset.prod.e2e-spec.js index 0dbd2282f0031a91db584f90a8085e2953b09719..ff4aea1de27ca8338f393a8c9e0f1e560272d2ed 100644 --- a/e2e/src/navigating/originDataset.prod.e2e-spec.js +++ b/e2e/src/navigating/originDataset.prod.e2e-spec.js @@ -67,7 +67,7 @@ describe('origin dataset pmap', () => { await iavPage.wait(5000) await iavPage.waitForAsync() - const additionalLayerControlIsShown = await iavPage.additionalLayerControlIsVisible() + const additionalLayerControlIsShown = await iavPage.isVisible(`[aria-label="${ARIA_LABELS.ADDITIONAL_VOLUME_CONTROL}"]`) expect(additionalLayerControlIsShown).toEqual(false) const checked = await iavPage.switchIsChecked(cssSelector) diff --git a/e2e/src/util.js b/e2e/src/util.js index 26d9e739bcb6e256ee0b97b30e2d6dd6230fa349..2b8ada185465a382874c25a8009da43c710fea8f 100644 --- a/e2e/src/util.js +++ b/e2e/src/util.js @@ -1,1130 +1,9 @@ -const chromeOpts = require('../chromeOpts') -// const pptr = require('puppeteer') -const ATLAS_URL = (process.env.ATLAS_URL || 'http://localhost:3000').replace(/\/$/, '') -const USE_SELENIUM = !!process.env.SELENIUM_ADDRESS -if (ATLAS_URL.length === 0) throw new Error(`ATLAS_URL must either be left unset or defined.`) -if (ATLAS_URL[ATLAS_URL.length - 1] === '/') throw new Error(`ATLAS_URL should not trail with a slash: ${ATLAS_URL}`) -const { By, Key, until } = require('selenium-webdriver') -const CITRUS_LIGHT_URL = `https://unpkg.com/citruslight@0.1.0/citruslight.js` -const { polyFillClick } = require('./material-util') -const { ARIA_LABELS, CONST } = require('../../common/constants') -const { retry } = require('../../common/util') +const { + BasePage, + AtlasPage, + LayoutPage, +} = require('../util/helper') -function getActualUrl(url) { - return /^http\:\/\//.test(url) ? url : `${ATLAS_URL}/${url.replace(/^\//, '')}` -} - -function _getTextFromWebElement(webElement) { - return webElement.getText() -} - -async function _getIndexFromArrayOfWebElements(search, webElements) { - const texts = await Promise.all( - webElements.map(_getTextFromWebElement) - ) - return texts.findIndex(text => text.indexOf(search) >= 0) -} - -const verifyPosition = position => { - - if (!position) throw new Error(`cursorGoto: position must be defined!`) - const x = Array.isArray(position) ? position[0] : position.x - const y = Array.isArray(position) ? position[1] : position.y - if (!x) throw new Error(`cursorGoto: position.x or position[0] must be defined`) - if (!y) throw new Error(`cursorGoto: position.y or position[1] must be defined`) - - return { - x, - y - } -} - -class WdBase{ - constructor() { - browser.waitForAngularEnabled(false) - } - get _browser(){ - return browser - } - get _driver(){ - return this._browser.driver - } - - // without image header - // output as b64 png - async takeScreenshot(cssSelector){ - - if(cssSelector) { - await this._browser.executeAsyncScript(async () => { - const cb = arguments[arguments.length - 1] - const moduleUrl = arguments[0] - const cssSelector = arguments[1] - - const el = document.querySelector(cssSelector) - if (!el) throw new Error(`css selector not fetching anything`) - import(moduleUrl) - .then(async m => { - m.citruslight(el) - cb() - }) - }, CITRUS_LIGHT_URL, cssSelector) - } - - await this.wait(1000) - const result = await this._browser.takeScreenshot() - - if (cssSelector) { - await this._browser.executeAsyncScript(async () => { - const cb = arguments[arguments.length - 1] - const moduleUrl = arguments[0] - const cssSelector = arguments[1] - - const el = document.querySelector(cssSelector) - if (!el) throw new Error(`css selector not fetching anything`) - import(moduleUrl) - .then(async m => { - m.clearAll() - cb() - }) - }, CITRUS_LIGHT_URL, cssSelector) - } - - await this.wait(1000) - return result - } - - async getRgbAt({ position } = {}, cssSelector = null){ - if (!position) throw new Error(`position is required for getRgbAt`) - const { x, y } = verifyPosition(position) - const screenshotData = await this.takeScreenshot(cssSelector) - const [ red, green, blue ] = await this._driver.executeAsyncScript(() => { - - const dataUri = arguments[0] - const pos = arguments[1] - const dim = arguments[2] - const cb = arguments[arguments.length - 1] - - const img = new Image() - img.onload = () => { - const canvas = document.createElement('canvas') - canvas.width = dim[0] - canvas.height = dim[1] - - const ctx = canvas.getContext('2d') - ctx.drawImage(img, 0, 0) - const imgData = ctx.getImageData(0, 0, dim[0], dim[1]) - - const idx = (dim[0] * pos[1] + pos[0]) * 4 - const red = imgData.data[idx] - const green = imgData.data[idx + 1] - const blue = imgData.data[idx + 2] - cb([red, green, blue]) - } - img.src = dataUri - - }, `data:image/png;base64,${screenshotData}`, [x, y], [800, 796]) - - return { red, green, blue } - } - - async switchIsChecked(cssSelector){ - if (!cssSelector) throw new Error(`switchChecked method requies css selector`) - const checked = await this._browser - .findElement( By.css(cssSelector) ) - .getAttribute('aria-checked') - return checked === 'true' - } - - async click(cssSelector){ - return await polyFillClick.bind(this)(cssSelector) - } - - async getText(cssSelector){ - if (!cssSelector) throw new Error(`getText needs to define css selector`) - const el = await this._browser.findElement( By.css(cssSelector) ) - - const text = await el.getText() - return text - } - - async areVisible(cssSelector) { - - if (!cssSelector) throw new Error(`getText needs to define css selector`) - const els = await this._browser.findElements( By.css(cssSelector) ) - - const returnArr = [] - - for (const el of els) { - const isDisplayed = await el.isDisplayed() - if (isDisplayed) returnArr.push(true) - else returnArr.push(false) - } - - return returnArr - } - - async isVisible(cssSelector) { - - if (!cssSelector) throw new Error(`getText needs to define css selector`) - const el = await this._browser.findElement( By.css(cssSelector) ) - const isDisplayed = await el.isDisplayed() - - return isDisplayed - } - - async areVisible(cssSelector){ - if (!cssSelector) throw new Error(`getText needs to define css selector`) - const els = await this._browser.findElements( By.css( cssSelector ) ) - const returnArr = [] - - for (const el of els) { - returnArr.push(await el.isDisplayed()) - } - return returnArr - } - - async isAt(cssSelector){ - if (!cssSelector) throw new Error(`getText needs to define css selector`) - const { x, y, width, height } = await this._browser.findElement( By.css(cssSelector) ).getRect() - return { x, y, width, height } - } - - async areAt(cssSelector){ - - if (!cssSelector) throw new Error(`getText needs to define css selector`) - const els = await this._browser.findElements( By.css( cssSelector ) ) - const returnArr = [] - - for (const el of els) { - const { x, y, width, height } = await el.getRect() - returnArr.push({ x, y, width, height }) - } - return returnArr - } - - historyBack() { - return this._browser.navigate().back() - } - - historyForward() { - return this._browser.navigate().forward() - } - - async init() { - const wSizeArg = chromeOpts.find(arg => arg.indexOf('--window-size') >= 0) - const [ _, width, height ] = /\=([0-9]{1,})\,([0-9]{1,})$/.exec(wSizeArg) - - const newDim = await this._browser.executeScript(async () => { - return [ - window.outerWidth - window.innerWidth + (+ arguments[0]), - window.outerHeight - window.innerHeight + (+ arguments[1]), - ] - }, width, height) - - await this._browser.manage() - .window() - .setRect({ - width: newDim[0], - height: newDim[1] - }) - } - - async waitForAsync(){ - - const checkReady = async () => { - const els = await this._browser.findElements( - By.css('.spinnerAnimationCircle') - ) - const visibleEls = [] - for (const el of els) { - if (await el.isDisplayed()) { - visibleEls.push(el) - } - } - return !visibleEls.length - } - - do { - await this.wait(500) - } while ( - !(await retry(checkReady.bind(this), { timeout: 1000, retries: 10 })) - ) - } - - async cursorMoveTo({ position }) { - const { x, y } = verifyPosition(position) - return this._driver.actions() - .move() - .move({ - x, - y, - duration: 1000 - }) - .perform() - } - - async cursorMoveToElement(cssSelector) { - if (!cssSelector) throw new Error(`cursorMoveToElement needs to define css selector`) - const el = await this._browser.findElement( By.css(cssSelector) ) - await this._driver.actions() - .move() - .move({ - origin: el, - duration: 1000 - }) - .perform() - } - - async scrollElementBy(cssSelector, options) { - const { delta } = options - await this._browser.executeScript(() => { - const { delta } = arguments[1] - const el = document.querySelector(arguments[0]) - el.scrollBy(...delta) - }, cssSelector, { delta }) - } - - async getScrollStatus(cssSelector) { - const val = await this._browser.executeScript(() => { - const el = document.querySelector(arguments[0]) - return el.scrollTop - }, cssSelector) - return val - } - - async cursorMoveToAndClick({ position }) { - const { x, y } = verifyPosition(position) - return this._driver.actions() - .move() - .move({ - x, - y, - duration: 1000 - }) - .click() - .perform() - } - - async cursorMoveToAndDrag({ position, delta }) { - const { x, y } = verifyPosition(position) - const { x: deltaX, y: deltaY } = verifyPosition(delta) - return this._driver.actions() - .move() - .move({ - x, - y, - duration: 1000 - }) - .press() - .move({ - x: x + deltaX, - y: y + deltaY, - duration: 1000 - }) - .release() - .perform() - } - - async initHttpInterceptor(){ - await this._browser.executeScript(() => { - if (window.__isIntercepting__) return - window.__isIntercepting__ = true - const open = window.XMLHttpRequest.prototype.open - window.__interceptedXhr__ = [] - window.XMLHttpRequest.prototype.open = function () { - window.__interceptedXhr__.push({ - method: arguments[0], - url: arguments[1] - }) - return open.apply(this, arguments) - } - }) - } - - async isHttpIntercepting(){ - return await this._browser.executeScript(() => { - return window.__isIntercepting__ - }) - } - - async getInterceptedHttpCalls(){ - return await this._browser.executeScript(() => { - return window['__interceptedXhr__'] - }) - } - - // it seems if you set intercept http to be true, you might also want ot set do not automat to be true - async goto(url = '/', { interceptHttp, doNotAutomate, forceTimeout = 20 * 1000 } = {}){ - this.__trackingNavigationState__ = false - const actualUrl = getActualUrl(url) - if (interceptHttp) { - this._browser.get(actualUrl) - await this.initHttpInterceptor() - } else { - await this._browser.get(actualUrl) - } - - // if doNotAutomate is not set - // should wait for async operations to end - if (!doNotAutomate) { - await this.wait(200) - await this.dismissModal() - await this.wait(200) - - if (forceTimeout) { - await Promise.race([ - this.waitForAsync(), - this.wait(forceTimeout) - ]) - } else { - await this.waitForAsync() - } - } - } - - async wait(ms) { - if (!ms) throw new Error(`wait duration must be specified!`) - return new Promise(rs => { - setTimeout(rs, ms) - }) - } - - async waitForCss(cssSelector) { - if (!cssSelector) throw new Error(`css selector must be defined`) - await this._browser.wait( - until.elementLocated( By.css(cssSelector) ), - 1e3 * 60 * 10 - ) - } - - async waitFor(animation = false, async = false){ - if (animation) await this.wait(500) - if (async) await this.waitForAsync() - } - - async getSnackbarMessage(){ - const txt = await this._driver - .findElement( By.tagName('simple-snack-bar') ) - .findElement( By.tagName('span') ) - .getText() - return txt - } - - async clickSnackbarAction(){ - await this._driver - .findElement( By.tagName('simple-snack-bar') ) - .findElement( By.tagName('button') ) - .click() - } - - _getBottomSheet() { - return this._driver.findElement( By.tagName('mat-bottom-sheet-container') ) - } - - _getBottomSheetList(){ - return this._getBottomSheet().findElements( By.tagName('mat-list-item') ) - } - - async getBottomSheetList(){ - const listItems = await this._getBottomSheetList() - const output = [] - for (const item of listItems) { - output.push( - await _getTextFromWebElement(item) - ) - } - return output - } - - async clickNthItemFromBottomSheetList(index, cssSelector){ - - const list = await this._getBottomSheetList() - - if (!list[index]) throw new Error(`index out of bound: ${index} in list with size ${list.length}`) - - if (cssSelector) { - await list[index] - .findElement( By.css(cssSelector) ) - .click() - } else { - await list[index].click() - } - } - - async clearAlerts() { - await this._driver - .actions() - .sendKeys( - Key.ESCAPE, - Key.ESCAPE, - Key.ESCAPE, - Key.ESCAPE - ) - .perform() - - await this.wait(500) - } - - async execScript(fn, ...arg){ - const result = await this._driver.executeScript(fn) - return result - } -} - -class WdLayoutPage extends WdBase{ - constructor(){ - super() - WdLayoutPage.TagNames = { - ...(WdBase.TagNames || {} ) - } - } - - _getModal(){ - return this._browser.findElement( By.tagName('mat-dialog-container') ) - } - - async dismissModal() { - try { - const okBtn = await this._getModal() - .findElement( By.tagName('mat-dialog-actions') ) - .findElement( By.css('button[color="primary"]') ) - await okBtn.click() - } catch (e) { - - } - } - - _getModalBtns(){ - return this._getModal().findElements( By.tagName('button') ) - } - - async getModalText(){ - const el = await this._getModal() - const txt = await _getTextFromWebElement(el) - return txt - } - - async getModalActions(){ - const btns = await this._getModalBtns() - - const arr = [] - for (const btn of btns){ - arr.push(await _getTextFromWebElement(btn)) - } - return arr - } - - async modalHasChild(cssSelector){ - try { - const isDisplayed = await this._getModal() - .findElement( By.css( cssSelector ) ) - .isDisplayed() - return isDisplayed - } catch (e) { - return false - } - } - - // text can be instance of regex or string - async clickModalBtnByText(text){ - const btns = await this._getModalBtns() - const arr = await this.getModalActions() - if (typeof text === 'string') { - const idx = arr.indexOf(text) - if (idx < 0) throw new Error(`clickModalBtnByText: ${text} not found.`) - await btns[idx].click() - return - } - if (text instanceof RegExp) { - const idx = arr.findIndex(item => text.test(item)) - if (idx < 0) throw new Error(`clickModalBtnByText: regexp ${text.toString()} not found`) - await btns[idx].click() - return - } - - throw new Error(`clickModalBtnByText arg must be instance of string or regexp`) - } - - async _findTitleCard(title) { - const titleCards = await this._browser - .findElement( By.css('ui-splashscreen') ) - .findElements( By.css('mat-card') ) - const idx = await _getIndexFromArrayOfWebElements(title, titleCards) - if (idx >= 0) return titleCards[idx] - else throw new Error(`${title} does not fit any titleCards`) - } - - async selectTitleCard( title ) { - const titleCard = await this._findTitleCard(title) - await titleCard.click() - } - - async selectTitleTemplateParcellation(templateName, parcellationName){ - throw new Error(`selectTitleTemplateParcellation has been deprecated. use selectAtlasTemplateParcellation`) - } - - /** - * _setAtlasSelectorExpanded - * toggle/set the open state of the atlas-layer-selector element - * If the only argument (flag) is not provided, it will toggle the atlas-layer-selector - * - * Will throw if atlas-layer-selector is not in the DOM - * - * @param {boolean} flag - * - */ - async _setAtlasSelectorExpanded(flag) { - const atlasLayerSelectorEl = this._browser.findElement( - By.css('atlas-layer-selector') - ) - const openedFlag = (await atlasLayerSelectorEl.getAttribute('data-opened')) === 'true' - if (typeof flag === 'undefined' || flag !== openedFlag) { - await atlasLayerSelectorEl.findElement(By.css(`button[aria-label="${ARIA_LABELS.TOGGLE_ATLAS_LAYER_SELECTOR}"]`)).click() - } - } - - async changeTemplate(templateName){ - if (!templateName) throw new Error(`templateName needs to be provided`) - await this._setAtlasSelectorExpanded(true) - await this.wait(1000) - const allTiles = await this._browser - .findElement( By.css('atlas-layer-selector') ) - .findElements( By.css(`mat-grid-tile`) ) - - const idx = await _getIndexFromArrayOfWebElements(templateName, allTiles) - if (idx >= 0) await allTiles[idx].click() - else throw new Error(`#changeTemplate: templateName ${templateName} cannot be found.`) - } - - async changeParc(parcName) { - throw new Error(`changeParc NYI`) - } - - async selectAtlasTemplateParcellation(atlasName, templateName, parcellationName, parcVersion) { - if (!atlasName) throw new Error(`atlasName needs to be provided`) - try { - /** - * if at title screen - */ - await (await this._findTitleCard(atlasName)).click() - } catch (e) { - /** - * if not at title screen - * select from dropdown - */ - } - - if (templateName) { - await this.wait(1000) - await this.waitUntilAllChunksLoaded() - await this.changeTemplate(templateName) - } - - if (parcellationName) { - await this.wait(1000) - await this.waitUntilAllChunksLoaded() - await this.changeParc(parcellationName) - } - - await this._setAtlasSelectorExpanded(false) - } - - // SideNav - _getSideNavPrimary(){ - return this._browser.findElement( - By.css('mat-drawer[data-mat-drawer-primary-open]') - ) - } - - async _getSideNavPrimaryExpanded(){ - return (await this._getSideNavPrimary() - .getAttribute('data-mat-drawer-primary-open')) === 'true' - } - - _getSideNavSecondary(){ - return this._browser.findElement( - By.css('mat-drawer[data-mat-drawer-secondary-open]') - ) - } - - async _getSideNavSecondaryExpanded(){ - return (await this._getSideNavSecondary() - .getAttribute('data-mat-drawer-secondary-open')) === 'true' - } - - async _setSideNavPrimaryExpanded(flag) { - const matDrawerPrimaryEl = this._getSideNavPrimary() - const openedFlag = await this._getSideNavPrimaryExpanded() - if (typeof flag === 'undefined' || flag !== openedFlag) { - await this._browser.findElement(By.css(`button[aria-label="${ARIA_LABELS.TOGGLE_SIDE_PANEL}"]`)).click() - } - } - - _getSideNav() { - throw new Error(`side bar no longer exist`) - } - - async getSideNavTag(){ - return await this._browser - .findElement( By.css('[mat-drawer-trigger]') ) - .findElement( By.tagName('i') ) - } - - sideNavIsVisible(){ - return this._getSideNav().isDisplayed() - } - - // SideNavTag - _getSideNavTab(){ - return this._browser - .findElement( By.css('[mat-drawer-trigger]') ) - .findElement( By.tagName('i') ) - } - - sideNavTabIsVisible(){ - return this._getSideNavTab().isDisplayed() - } - - clickSideNavTab(){ - return this._getSideNavTab().click() - } - - // statusPanel - _getStatusPanel(){ - return this._browser.findElement( By.css('[mat-drawer-status-panel]') ) - } - - async statusPanelIsVisible() { - try { - return await this._getStatusPanel().isDisplayed() - } catch (e) { - return false - } - } - - clickStatusPanel() { - // Will throw if status panel is not visible - return this._getStatusPanel().click() - } - - // will throw if sidenav is not visible - async getTemplateInfo(){ - const ariaText = `Hover to find out more info on the selected template` - const infoBtn = await this._getSideNav() - .findElement( By.css(`[aria-label="${ariaText}"]`) ) - - await this._driver.actions() - .move() - .move({ - origin: infoBtn, - duration: 1000 - }) - .perform() - - await this.wait(500) - const text = await this._getSideNav() - .findElement( By.id('selected-template-detailed-info') ) - .getText() - - return text - } - - _getAdditionalLayerControl(){ - return this._browser.findElement( - By.css(`[aria-label="${ARIA_LABELS.ADDITIONAL_VOLUME_CONTROL}"]`) - ) - } - - async additionalLayerControlIsVisible(){ - try { - return await this._getAdditionalLayerControl().isDisplayed() - } catch (e) { - return false - } - } - - // will throw if additional layer control is not visible - additionalLayerControlIsExpanded() { - return this._getAdditionalLayerControl() - .findElement( - By.css('layer-browser') - ) - .isDisplayed() - } - - async toggleNthLayerControl(idx) { - const els = await this._getAdditionalLayerControl() - .findElements( By.css(`[aria-label="${ARIA_LABELS.TOGGLE_SHOW_LAYER_CONTROL}"]`)) - if (!els[idx]) throw new Error(`toggleNthLayerControl index out of bound: accessor ${idx} with length ${els.length}`) - await els[idx].click() - } - - // Signin banner - _getToolsIcon(){ - return this._driver - .findElement( By.css('[aria-label="Show tools and plugins"]') ) - } - - async showToolsMenu(){ - await this._getToolsIcon().click() - } - - _getToolsMenu(){ - return this._driver - .findElement( By.css('[aria-label="Tools and plugins menu"]') ) - } - - _getAllTools(){ - return this._getToolsMenu().findElements( By.css('[role="menuitem"]') ) - } - - async getVisibleTools(){ - // may throw if tools menu not visible - const menuItems = await this._getAllTools() - const returnArr = [] - for (const menuItem of menuItems){ - returnArr.push( - await _getTextFromWebElement(menuItem) - ) - } - return returnArr - } - - async clickOnNthTool(index, cssSelector){ - const menuItems = await this._getAllTools() - if (!menuItems[index]) throw new Error(`index out of bound: accessing index ${index} of length ${menuItems.length}`) - if (cssSelector) await menuItems[index].findElement( By.css(cssSelector) ).click() - else await menuItems[index].click() - } - - _getFavDatasetIcon(){ - return this._driver - .findElement( By.css('[aria-label="Show pinned datasets"]') ) - } - - async getNumberOfFavDataset(){ - const attr = await this._getFavDatasetIcon().getAttribute('pinned-datasets-length') - return Number(attr) - } - - async showPinnedDatasetPanel(){ - await this._getFavDatasetIcon().click() - await this.wait(500) - } - - _getPinnedDatasetPanel(){ - return this._driver - .findElement( - By.css('[aria-label="Pinned datasets panel"]') - ) - } - - async getPinnedDatasetsFromOpenedPanel(){ - const list = await this._getPinnedDatasetPanel() - .findElements( - By.tagName('mat-list-item') - ) - - const returnArr = [] - for (const el of list) { - const text = await _getTextFromWebElement(el) - returnArr.push(text) - } - return returnArr - } - - async unpinNthDatasetFromOpenedPanel(index){ - const list = await this._getPinnedDatasetPanel() - .findElements( - By.tagName('mat-list-item') - ) - - if (!list[index]) throw new Error(`index out of bound: ${index} in list with size ${list.length}`) - await list[index] - .findElement( By.css('[aria-label="Toggle pinning this dataset"]') ) - .click() - } - - _getWidgetPanel(title){ - return this._driver.findElement( By.css(`[aria-label="Widget for ${title}"]`) ) - } - - async widgetPanelIsDispalyed(title){ - try { - const isDisplayed = await this._getWidgetPanel(title).isDisplayed() - return isDisplayed - } catch (e) { - console.warn(`widgetPanelIsDisplayed error`, e) - return false - } - } - - async closeWidgetByname(title){ - await this._getWidgetPanel(title) - .findElement( By.css(`[aria-label="close"]`) ) - .click() - } -} - -class WdIavPage extends WdLayoutPage{ - constructor(){ - super() - } - - async clearAllSelectedRegions() { - const clearAllRegionBtn = await this._browser.findElement( - By.css(`[aria-label="${ARIA_LABELS.CLEAR_SELECTED_REGION}"]`) - ) - await clearAllRegionBtn.click() - await this.wait(500) - } - - async waitUntilAllChunksLoaded(){ - await this._browser.wait(async () => { - const els = await this._browser.findElements( - By.css('div.loadingIndicator') - ) - const els2 = await this._browser.findElements( - By.css('.spinnerAnimationCircle') - ) - return [...els, ...els2].length === 0 - }, 1e3 * 60 * 10) - } - - async getFloatingCtxInfoAsText(){ - const floatingContainer = await this._browser.findElement( - By.css('div[floatingMouseContextualContainerDirective]') - ) - - const text = await floatingContainer.getText() - return text - } - - async selectDropdownTemplate(title) { - const templateBtn = await this._getSideNav() - .findElement( By.tagName('viewer-state-controller') ) - .findElement( By.css('[aria-label="Select a new template"]') ) - await templateBtn.click() - - await this._browser.wait( - until.elementLocated( By.tagName('mat-option') ), - 1e3 * 60 * 10 - ) - - const options = await this._browser.findElements( - By.tagName('mat-option') - ) - const idx = await _getIndexFromArrayOfWebElements(title, options) - if (idx >= 0) { - retry(async () => { - await options[idx].click() - }, { timeout: 1000, retries: 3 }) - } - else throw new Error(`${title} is not found as one of the dropdown templates`) - } - - async _getSearchRegionInput(){ - await this._setSideNavPrimaryExpanded(true) - await this.wait(500) - const secondaryOpen = await this._getSideNavSecondaryExpanded() - if (secondaryOpen) { - return this._getSideNavSecondary().findElement( By.css(`[aria-label="${ARIA_LABELS.TEXT_INPUT_SEARCH_REGION}"]`) ) - } else { - return this._getSideNavPrimary().findElement( By.css(`[aria-label="${ARIA_LABELS.TEXT_INPUT_SEARCH_REGION}"]`) ) - } - } - - async searchRegionWithText(text=''){ - const searchRegionInput = await this._getSearchRegionInput() - await searchRegionInput - .sendKeys( - Key.chord(Key.CONTROL, 'a'), - text - ) - } - - async clearSearchRegionWithText() { - const searchRegionInput = await this._getSearchRegionInput() - await searchRegionInput - .sendKeys( - Key.chord(Key.CONTROL, 'a'), - Key.BACK_SPACE, - Key.ESCAPE - ) - } - - async _getAutcompleteOptions(){ - const input = await this._getSearchRegionInput() - const autocompleteId = await input.getAttribute('aria-owns') - const el = await this._browser.findElement( By.css( `[id=${autocompleteId}]` ) ) - return this._browser - .findElement( By.id( autocompleteId ) ) - .findElements( By.tagName('mat-option') ) - } - - async getSearchRegionInputAutoCompleteOptions(){ - const options = await this._getAutcompleteOptions() - return await Promise.all( - options.map(_getTextFromWebElement) - ) - } - - async selectSearchRegionAutocompleteWithText(text = ''){ - - const options = await this._getAutcompleteOptions() - - const idx = await _getIndexFromArrayOfWebElements(text, options) - if (idx >= 0) { - await options[idx].click() - } else { - throw new Error(`_getIndexFromArrayOfWebElements ${text} option not founds`) - } - } - - _getModalityListView(){ - return this._browser - .findElement( By.css('modality-picker') ) - .findElements( By.css('mat-checkbox') ) - } - - async getModalities(){ - const els = await this._getModalityListView() - const returnArr = [] - for (const el of els) { - returnArr.push( - await el.getText() - ) - } - return returnArr - } - - _getSingleDatasetListView(){ - return this._browser - .findElement( By.css('data-browser') ) - .findElements( By.css('single-dataset-list-view') ) - } - - _getRegionalFeatureEl(){ - return this._getSideNavSecondary().findElement( - By.css(`mat-expansion-panel[data-mat-expansion-title="${CONST.REGIONAL_FEATURES}"]`) - ) - } - - async _setRegionalFeaturesExpanded(flag){ - const regionFeatureExpEl = this._getRegionalFeatureEl() - const openedFlag = (await regionFeatureExpEl.getAttribute('data-opened')) === 'true' - if (typeof flag === 'undefined' || flag !== openedFlag) { - await regionFeatureExpEl.findElement(By.css(`mat-expansion-panel-header`)).click() - } - } - - async getVisibleDatasets() { - const singleDatasetListView = await this._getSingleDatasetListView() - - const returnArr = [] - for (const item of singleDatasetListView) { - returnArr.push( await item.getText() ) - } - return returnArr - } - - async clickNthDataset(index){ - if (!Number.isInteger(index)) throw new Error(`index needs to be an integer`) - const list = await this._getSingleDatasetListView() - await list[index].click() - } - - async togglePinNthDataset(index) { - if (!Number.isInteger(index)) throw new Error(`index needs to be an integer`) - const list = await this._getSingleDatasetListView() - if (!list[index]) throw new Error(`out of bound ${index} in list with length ${list.length}`) - await list[index] - .findElement( By.css('[aria-label="Toggle pinning this dataset"]') ) - .click() - } - - async viewerIsPopulated() { - try { - const ngContainer = await this._browser.findElement( - By.id('neuroglancer-container') - ) - if (! (await ngContainer.isDisplayed())) { - return false - } - const canvas = await ngContainer.findElement( - By.tagName('canvas') - ) - if (!(await canvas.isDisplayed())) { - return false - } - return true - } catch (e) { - return false - } - } - - async getNavigationState() { - if (!this.__trackingNavigationState__) { - await this._browser.executeScript(async () => { - window.__iavE2eNavigationState__ = {} - - const getPr = () => new Promise(rs => { - - window.__iavE2eNavigationStateSubptn__ = nehubaViewer.navigationState.all - .subscribe(({ orientation, perspectiveOrientation, perspectiveZoom, position, zoom }) => { - window.__iavE2eNavigationState__ = { - orientation: Array.from(orientation), - perspectiveOrientation: Array.from(perspectiveOrientation), - perspectiveZoom, - zoom, - position: Array.from(position) - } - rs() - }) - }) - - await getPr() - }) - - this.__trackingNavigationState__ = true - } - - const returnVal = await this._browser.executeScript(() => window.__iavE2eNavigationState__) - return returnVal - } - -} - -class PptrIAVPage{ - - constructor(){ - this._browser = null - this._page = null - } - - async init() { - this._browser = browser - - this._page = await this._browser.newPage() - await this._page.setViewport({ - width: 1600, - height: 900 - }) - } - - async goto(url = '/') { - const actualUrl = getActualUrl(url) - await this._page.goto(actualUrl, { waitUntil: 'networkidle2' }) - } - - async wait(ms) { - if (!ms) throw new Error(`wait duration must be specified!`) - await this._page.waitFor(ms) - } -} - -exports.waitMultiple = process.env.WAIT_ULTIPLE || 1 - -exports.AtlasPage = WdIavPage -exports.LayoutPage = WdLayoutPage +exports.AtlasPage = AtlasPage +exports.LayoutPage = LayoutPage diff --git a/e2e/util/helper.js b/e2e/util/helper.js new file mode 100644 index 0000000000000000000000000000000000000000..10a7176ad2c2428057a87561e0e1427f76e5ea84 --- /dev/null +++ b/e2e/util/helper.js @@ -0,0 +1,11 @@ +const { + WdIavPage, + WdLayoutPage, + WdBase, +} = require('./selenium/iav') + +module.exports = { + BasePage: WdBase, + LayoutPage: WdLayoutPage, + AtlasPage: WdIavPage +} diff --git a/e2e/util/selenium/base.js b/e2e/util/selenium/base.js new file mode 100644 index 0000000000000000000000000000000000000000..c7172156bb35fb1df676433916fc91c46f41bc60 --- /dev/null +++ b/e2e/util/selenium/base.js @@ -0,0 +1,408 @@ +const CITRUS_LIGHT_URL = `https://unpkg.com/citruslight@0.1.0/citruslight.js` +const { By, Key, until } = require('selenium-webdriver') +const { retry } = require('../../../common/util') +const chromeOpts = require('../../chromeOpts') +const ATLAS_URL = (process.env.ATLAS_URL || 'http://localhost:3000').replace(/\/$/, '') + +function getActualUrl(url) { + return /^http\:\/\//.test(url) ? url : `${ATLAS_URL}/${url.replace(/^\//, '')}` +} + +async function polyFillClick(cssSelector){ + if (!cssSelector) throw new Error(`click method needs to define a css selector`) + const webEl = this._browser.findElement( By.css(cssSelector) ) + try { + await webEl.click() + } catch (e) { + const id = await webEl.getAttribute('id') + const newId = id.replace(/-input$/, '') + await this._browser.findElement( + By.id(newId) + ).click() + } +} + +const verifyPosition = position => { + + if (!position) throw new Error(`cursorGoto: position must be defined!`) + const x = Array.isArray(position) ? position[0] : position.x + const y = Array.isArray(position) ? position[1] : position.y + if (!x) throw new Error(`cursorGoto: position.x or position[0] must be defined`) + if (!y) throw new Error(`cursorGoto: position.y or position[1] must be defined`) + + return { + x, + y + } +} + +class WdBase{ + constructor() { + browser.waitForAngularEnabled(false) + } + get _browser(){ + return browser + } + get _driver(){ + return this._browser.driver + } + + // without image header + // output as b64 png + async takeScreenshot(cssSelector){ + + if(cssSelector) { + await this._browser.executeAsyncScript(async () => { + const cb = arguments[arguments.length - 1] + const moduleUrl = arguments[0] + const cssSelector = arguments[1] + + const el = document.querySelector(cssSelector) + if (!el) throw new Error(`css selector not fetching anything`) + import(moduleUrl) + .then(async m => { + m.citruslight(el) + cb() + }) + }, CITRUS_LIGHT_URL, cssSelector) + } + + await this.wait(1000) + const result = await this._browser.takeScreenshot() + + if (cssSelector) { + await this._browser.executeAsyncScript(async () => { + const cb = arguments[arguments.length - 1] + const moduleUrl = arguments[0] + const cssSelector = arguments[1] + + const el = document.querySelector(cssSelector) + if (!el) throw new Error(`css selector not fetching anything`) + import(moduleUrl) + .then(async m => { + m.clearAll() + cb() + }) + }, CITRUS_LIGHT_URL, cssSelector) + } + + await this.wait(1000) + return result + } + + async getRgbAt({ position } = {}, cssSelector = null){ + if (!position) throw new Error(`position is required for getRgbAt`) + const { x, y } = verifyPosition(position) + const screenshotData = await this.takeScreenshot(cssSelector) + const [ red, green, blue ] = await this._driver.executeAsyncScript(() => { + + const dataUri = arguments[0] + const pos = arguments[1] + const dim = arguments[2] + const cb = arguments[arguments.length - 1] + + const img = new Image() + img.onload = () => { + const canvas = document.createElement('canvas') + canvas.width = dim[0] + canvas.height = dim[1] + + const ctx = canvas.getContext('2d') + ctx.drawImage(img, 0, 0) + const imgData = ctx.getImageData(0, 0, dim[0], dim[1]) + + const idx = (dim[0] * pos[1] + pos[0]) * 4 + const red = imgData.data[idx] + const green = imgData.data[idx + 1] + const blue = imgData.data[idx + 2] + cb([red, green, blue]) + } + img.src = dataUri + + }, `data:image/png;base64,${screenshotData}`, [x, y], [800, 796]) + + return { red, green, blue } + } + + async switchIsChecked(cssSelector){ + if (!cssSelector) throw new Error(`switchChecked method requies css selector`) + const checked = await this._browser + .findElement( By.css(cssSelector) ) + .getAttribute('aria-checked') + return checked === 'true' + } + + async click(cssSelector){ + return await polyFillClick.bind(this)(cssSelector) + } + + async getText(cssSelector){ + if (!cssSelector) throw new Error(`getText needs to define css selector`) + const el = await this._browser.findElement( By.css(cssSelector) ) + + const text = await el.getText() + return text + } + + async isVisible(cssSelector) { + + if (!cssSelector) throw new Error(`getText needs to define css selector`) + const el = await this._browser.findElement( By.css(cssSelector) ) + const isDisplayed = await el.isDisplayed() + + return isDisplayed + } + + async areVisible(cssSelector){ + if (!cssSelector) throw new Error(`getText needs to define css selector`) + const els = await this._browser.findElements( By.css( cssSelector ) ) + const returnArr = [] + + for (const el of els) { + returnArr.push(await el.isDisplayed()) + } + return returnArr + } + + async isAt(cssSelector){ + if (!cssSelector) throw new Error(`getText needs to define css selector`) + const { x, y, width, height } = await this._browser.findElement( By.css(cssSelector) ).getRect() + return { x, y, width, height } + } + + async areAt(cssSelector){ + + if (!cssSelector) throw new Error(`getText needs to define css selector`) + const els = await this._browser.findElements( By.css( cssSelector ) ) + const returnArr = [] + + for (const el of els) { + const { x, y, width, height } = await el.getRect() + returnArr.push({ x, y, width, height }) + } + return returnArr + } + + historyBack() { + return this._browser.navigate().back() + } + + historyForward() { + return this._browser.navigate().forward() + } + + async init() { + const wSizeArg = chromeOpts.find(arg => arg.indexOf('--window-size') >= 0) + const [ _, width, height ] = /\=([0-9]{1,})\,([0-9]{1,})$/.exec(wSizeArg) + + const newDim = await this._browser.executeScript(async () => { + return [ + window.outerWidth - window.innerWidth + (+ arguments[0]), + window.outerHeight - window.innerHeight + (+ arguments[1]), + ] + }, width, height) + + await this._browser.manage() + .window() + .setRect({ + width: newDim[0], + height: newDim[1] + }) + } + + async waitForAsync(){ + + const checkReady = async () => { + const els = await this._browser.findElements( + By.css('.spinnerAnimationCircle') + ) + const visibleEls = [] + for (const el of els) { + if (await el.isDisplayed()) { + visibleEls.push(el) + } + } + return !visibleEls.length + } + + do { + await this.wait(500) + } while ( + !(await retry(checkReady.bind(this), { timeout: 1000, retries: 10 })) + ) + } + + async cursorMoveTo({ position }) { + const { x, y } = verifyPosition(position) + return this._driver.actions() + .move() + .move({ + x, + y, + duration: 1000 + }) + .perform() + } + + async cursorMoveToElement(cssSelector) { + if (!cssSelector) throw new Error(`cursorMoveToElement needs to define css selector`) + const el = await this._browser.findElement( By.css(cssSelector) ) + await this._driver.actions() + .move() + .move({ + origin: el, + duration: 1000 + }) + .perform() + } + + async scrollElementBy(cssSelector, options) { + const { delta } = options + await this._browser.executeScript(() => { + const { delta } = arguments[1] + const el = document.querySelector(arguments[0]) + el.scrollBy(...delta) + }, cssSelector, { delta }) + } + + async getScrollStatus(cssSelector) { + const val = await this._browser.executeScript(() => { + const el = document.querySelector(arguments[0]) + return el.scrollTop + }, cssSelector) + return val + } + + async cursorMoveToAndClick({ position }) { + const { x, y } = verifyPosition(position) + return this._driver.actions() + .move() + .move({ + x, + y, + duration: 1000 + }) + .click() + .perform() + } + + async cursorMoveToAndDrag({ position, delta }) { + const { x, y } = verifyPosition(position) + const { x: deltaX, y: deltaY } = verifyPosition(delta) + return this._driver.actions() + .move() + .move({ + x, + y, + duration: 1000 + }) + .press() + .move({ + x: x + deltaX, + y: y + deltaY, + duration: 1000 + }) + .release() + .perform() + } + + async initHttpInterceptor(){ + await this._browser.executeScript(() => { + if (window.__isIntercepting__) return + window.__isIntercepting__ = true + const open = window.XMLHttpRequest.prototype.open + window.__interceptedXhr__ = [] + window.XMLHttpRequest.prototype.open = function () { + window.__interceptedXhr__.push({ + method: arguments[0], + url: arguments[1] + }) + return open.apply(this, arguments) + } + }) + } + + async isHttpIntercepting(){ + return await this._browser.executeScript(() => { + return window.__isIntercepting__ + }) + } + + async getInterceptedHttpCalls(){ + return await this._browser.executeScript(() => { + return window['__interceptedXhr__'] + }) + } + + // it seems if you set intercept http to be true, you might also want ot set do not automat to be true + async goto(url = '/', { interceptHttp, doNotAutomate, forceTimeout = 20 * 1000 } = {}){ + this.__trackingNavigationState__ = false + const actualUrl = getActualUrl(url) + if (interceptHttp) { + this._browser.get(actualUrl) + await this.initHttpInterceptor() + } else { + await this._browser.get(actualUrl) + } + + // if doNotAutomate is not set + // should wait for async operations to end + if (!doNotAutomate) { + await this.wait(200) + await this.dismissModal() + await this.wait(200) + + if (forceTimeout) { + await Promise.race([ + this.waitForAsync(), + this.wait(forceTimeout) + ]) + } else { + await this.waitForAsync() + } + } + } + + async wait(ms) { + if (!ms) throw new Error(`wait duration must be specified!`) + return new Promise(rs => { + setTimeout(rs, ms) + }) + } + + async waitForCss(cssSelector) { + if (!cssSelector) throw new Error(`css selector must be defined`) + await this._browser.wait( + until.elementLocated( By.css(cssSelector) ), + 1e3 * 60 * 10 + ) + } + + async waitFor(animation = false, async = false){ + if (animation) await this.wait(500) + if (async) await this.waitForAsync() + } + + async clearAlerts() { + await this._driver + .actions() + .sendKeys( + Key.ESCAPE, + Key.ESCAPE, + Key.ESCAPE, + Key.ESCAPE + ) + .perform() + + await this.wait(500) + } + + async execScript(fn, ...arg){ + const result = await this._driver.executeScript(fn) + return result + } +} + +module.exports = { + WdBase +} diff --git a/e2e/util/selenium/iav.js b/e2e/util/selenium/iav.js new file mode 100644 index 0000000000000000000000000000000000000000..2629403e439a8e8998ca115acd02939a9746bb3b --- /dev/null +++ b/e2e/util/selenium/iav.js @@ -0,0 +1,206 @@ +const { WdLayoutPage, WdBase } = require('./layout') +const { ARIA_LABELS, CONST } = require('../../../common/constants') +const { _getTextFromWebElement, _getIndexFromArrayOfWebElements } = require('./util') +const { Key } = require('selenium-webdriver') + +class WdIavPage extends WdLayoutPage{ + constructor(){ + super() + } + + async clearAllSelectedRegions() { + const clearAllRegionBtn = await this._browser.findElement( + By.css(`[aria-label="${ARIA_LABELS.CLEAR_SELECTED_REGION}"]`) + ) + await clearAllRegionBtn.click() + await this.wait(500) + } + + async waitUntilAllChunksLoaded(){ + await this._browser.wait(async () => { + const els = await this._browser.findElements( + By.css('div.loadingIndicator') + ) + const els2 = await this._browser.findElements( + By.css('.spinnerAnimationCircle') + ) + return [...els, ...els2].length === 0 + }, 1e3 * 60 * 10) + } + + async getFloatingCtxInfoAsText(){ + const floatingContainer = await this._browser.findElement( + By.css('div[floatingMouseContextualContainerDirective]') + ) + + const text = await floatingContainer.getText() + return text + } + + async selectDropdownTemplate(title) { + throw new Error(`selectDropdownTemplate has been deprecated. use changeTemplate instead`) + } + + async _getSearchRegionInput(){ + await this._setSideNavPrimaryExpanded(true) + await this.wait(500) + const secondaryOpen = await this._getSideNavSecondaryExpanded() + if (secondaryOpen) { + return this._getSideNavSecondary().findElement( By.css(`[aria-label="${ARIA_LABELS.TEXT_INPUT_SEARCH_REGION}"]`) ) + } else { + return this._getSideNavPrimary().findElement( By.css(`[aria-label="${ARIA_LABELS.TEXT_INPUT_SEARCH_REGION}"]`) ) + } + } + + async searchRegionWithText(text=''){ + const searchRegionInput = await this._getSearchRegionInput() + await searchRegionInput + .sendKeys( + Key.chord(Key.CONTROL, 'a'), + text + ) + } + + async clearSearchRegionWithText() { + const searchRegionInput = await this._getSearchRegionInput() + await searchRegionInput + .sendKeys( + Key.chord(Key.CONTROL, 'a'), + Key.BACK_SPACE, + Key.ESCAPE + ) + } + + async _getAutcompleteOptions(){ + const input = await this._getSearchRegionInput() + const autocompleteId = await input.getAttribute('aria-owns') + const el = await this._browser.findElement( By.css( `[id=${autocompleteId}]` ) ) + return this._browser + .findElement( By.id( autocompleteId ) ) + .findElements( By.tagName('mat-option') ) + } + + async getSearchRegionInputAutoCompleteOptions(){ + const options = await this._getAutcompleteOptions() + return await Promise.all( + options.map(_getTextFromWebElement) + ) + } + + async selectSearchRegionAutocompleteWithText(text = ''){ + + const options = await this._getAutcompleteOptions() + + const idx = await _getIndexFromArrayOfWebElements(text, options) + if (idx >= 0) { + await options[idx].click() + } else { + throw new Error(`_getIndexFromArrayOfWebElements ${text} option not founds`) + } + } + + _getModalityListView(){ + return this._browser + .findElement( By.css('modality-picker') ) + .findElements( By.css('mat-checkbox') ) + } + + async getModalities(){ + const els = await this._getModalityListView() + const returnArr = [] + for (const el of els) { + returnArr.push( + await el.getText() + ) + } + return returnArr + } + + _getSingleDatasetListView(){ + return this._browser + .findElement( By.css('data-browser') ) + .findElements( By.css('single-dataset-list-view') ) + } + + async getVisibleDatasets() { + const singleDatasetListView = await this._getSingleDatasetListView() + + const returnArr = [] + for (const item of singleDatasetListView) { + returnArr.push( await item.getText() ) + } + return returnArr + } + + async clickNthDataset(index){ + if (!Number.isInteger(index)) throw new Error(`index needs to be an integer`) + const list = await this._getSingleDatasetListView() + await list[index].click() + } + + async togglePinNthDataset(index) { + if (!Number.isInteger(index)) throw new Error(`index needs to be an integer`) + const list = await this._getSingleDatasetListView() + if (!list[index]) throw new Error(`out of bound ${index} in list with length ${list.length}`) + await list[index] + .findElement( By.css('[aria-label="Toggle pinning this dataset"]') ) + .click() + } + + async viewerIsPopulated() { + try { + const ngContainer = await this._browser.findElement( + By.id('neuroglancer-container') + ) + if (! (await ngContainer.isDisplayed())) { + return false + } + const canvas = await ngContainer.findElement( + By.tagName('canvas') + ) + if (!(await canvas.isDisplayed())) { + return false + } + return true + } catch (e) { + return false + } + } + + async getNavigationState() { + if (!this.__trackingNavigationState__) { + await this._browser.executeScript(async () => { + window.__iavE2eNavigationState__ = {} + + const getPr = () => new Promise(rs => { + + window.__iavE2eNavigationStateSubptn__ = nehubaViewer.navigationState.all + .subscribe(({ orientation, perspectiveOrientation, perspectiveZoom, position, zoom }) => { + window.__iavE2eNavigationState__ = { + orientation: Array.from(orientation), + perspectiveOrientation: Array.from(perspectiveOrientation), + perspectiveZoom, + zoom, + position: Array.from(position) + } + rs() + }) + }) + + await getPr() + }) + + this.__trackingNavigationState__ = true + } + + const returnVal = await this._browser.executeScript(() => window.__iavE2eNavigationState__) + return returnVal + } + +} + +module.exports = { + WdIavPage, + WdLayoutPage, + WdBase, +} \ No newline at end of file diff --git a/e2e/util/selenium/layout.js b/e2e/util/selenium/layout.js new file mode 100644 index 0000000000000000000000000000000000000000..286504b7f7b888c614c7fff373dfbf97943cbe86 --- /dev/null +++ b/e2e/util/selenium/layout.js @@ -0,0 +1,489 @@ +const { WdBase } = require('./base') +const { + _getIndexFromArrayOfWebElements, + _getTextFromWebElement +} = require('./util') +const { ARIA_LABELS } = require('../../../common/constants') + +class WdLayoutPage extends WdBase{ + constructor(){ + super() + } + + /** + * Snackbar + */ + async getSnackbarMessage(){ + const txt = await this._driver + .findElement( By.css('simple-snack-bar') ) + .findElement( By.css('span') ) + .getText() + return txt + } + + async clickSnackbarAction(){ + await this._driver + .findElement( By.css('simple-snack-bar') ) + .findElement( By.css('button') ) + .click() + } + + /** + * Bottomsheet + */ + _getBottomSheet() { + return this._driver.findElement( By.css('mat-bottom-sheet-container') ) + } + + _getBottomSheetList(){ + return this._getBottomSheet().findElements( By.css('mat-list-item') ) + } + + async getBottomSheetList(){ + const listItems = await this._getBottomSheetList() + const output = [] + for (const item of listItems) { + output.push( + await _getTextFromWebElement(item) + ) + } + return output + } + + async clickNthItemFromBottomSheetList(index, cssSelector){ + + const list = await this._getBottomSheetList() + + if (!list[index]) throw new Error(`index out of bound: ${index} in list with size ${list.length}`) + + if (cssSelector) { + await list[index] + .findElement( By.css(cssSelector) ) + .click() + } else { + await list[index].click() + } + } + + /** + * Modal + */ + _getModal(){ + return this._browser.findElement( By.css('mat-dialog-container') ) + } + + async dismissModal() { + try { + const okBtn = await this._getModal() + .findElement( By.css('mat-dialog-actions') ) + .findElement( By.css('button[color="primary"]') ) + await okBtn.click() + } catch (e) { + + } + } + + _getModalBtns(){ + return this._getModal().findElements( By.tagName('button') ) + } + + async getModalText(){ + const el = await this._getModal() + const txt = await _getTextFromWebElement(el) + return txt + } + + async getModalActions(){ + const btns = await this._getModalBtns() + + const arr = [] + for (const btn of btns){ + arr.push(await _getTextFromWebElement(btn)) + } + return arr + } + + /** + * + * @param {string|RegExp} text + * @description search criteria for the btn to click + */ + async clickModalBtnByText(text){ + const btns = await this._getModalBtns() + const arr = await this.getModalActions() + if (typeof text === 'string') { + const idx = arr.indexOf(text) + if (idx < 0) throw new Error(`clickModalBtnByText: ${text} not found.`) + await btns[idx].click() + return + } + if (text instanceof RegExp) { + const idx = arr.findIndex(item => text.test(item)) + if (idx < 0) throw new Error(`clickModalBtnByText: regexp ${text.toString()} not found`) + await btns[idx].click() + return + } + + throw new Error(`clickModalBtnByText arg must be instance of string or regexp`) + } + + /** + * ExpansionPanel + */ + + /** + * + * @param {string|RegExp} text + * @description search criteria for the expansion panel title + */ + async _getExpansionPanel(text){ + const expPanels = await this._browser.findElements( + By.css(`mat-expansion-panel`) + ) + + const expPanelHdrs = [] + for (const expPanel of expPanels) { + expPanelHdrs.push( + await expPanel.findElement( + By.css(`mat-expansion-panel-header`) + ) + ) + } + + const idx = await _getIndexFromArrayOfWebElements(text, expPanelHdrs) + return idx >= 0 && expPanels[idx] + } + + /** + * + * @param {Object} expPanel webelement of the mat expansion panel + * @returns {Promise<boolean>} + */ + async _expansionPanelIsOpen(expPanel){ + const classString = await expPanel.getAttribute('class') + /** + * mat-expanded gets appended when mat-expansion panel is set to open + */ + return classString.indexOf('mat-expanded') >= 0 + } + + /** + * + * @param {string|RegExp} name Text of the expansion panel header + * @param {boolean} flag @optional State to set the expansion panel. Leave empty for toggle. + */ + async toggleExpansionPanelState(name, flag){ + const expPanel = await this._getExpansionPanel(name) + if (!expPanel) throw new Error(`expansionPanel ${name} could not be found`) + if (typeof flag === 'undefined') { + await expPanel.findElement( + By.css(`mat-expansion-panel-header`) + ).click() + return + } + + const currentOpen = await this._expansionPanelIsOpen(expPanel) + if (currentOpen !== flag) { + await expPanel.findElement( + By.css(`mat-expansion-panel-header`) + ).click() + } + } + + + /** + * Other + */ + async _findTitleCard(title) { + const titleCards = await this._browser + .findElement( By.css('ui-splashscreen') ) + .findElements( By.css('mat-card') ) + const idx = await _getIndexFromArrayOfWebElements(title, titleCards) + if (idx >= 0) return titleCards[idx] + else throw new Error(`${title} does not fit any titleCards`) + } + + async selectTitleCard( title ) { + const titleCard = await this._findTitleCard(title) + await titleCard.click() + } + + async selectTitleTemplateParcellation(templateName, parcellationName){ + throw new Error(`selectTitleTemplateParcellation has been deprecated. use selectAtlasTemplateParcellation`) + } + + /** + * _setAtlasSelectorExpanded + * toggle/set the open state of the atlas-layer-selector element + * If the only argument (flag) is not provided, it will toggle the atlas-layer-selector + * + * Will throw if atlas-layer-selector is not in the DOM + * + * @param {boolean} flag + * + */ + async _setAtlasSelectorExpanded(flag) { + const atlasLayerSelectorEl = this._browser.findElement( + By.css('atlas-layer-selector') + ) + const openedFlag = (await atlasLayerSelectorEl.getAttribute('data-opened')) === 'true' + if (typeof flag === 'undefined' || flag !== openedFlag) { + await atlasLayerSelectorEl.findElement(By.css(`button[aria-label="${ARIA_LABELS.TOGGLE_ATLAS_LAYER_SELECTOR}"]`)).click() + } + } + + async changeTemplate(templateName){ + if (!templateName) throw new Error(`templateName needs to be provided`) + await this._setAtlasSelectorExpanded(true) + await this.wait(1000) + const allTiles = await this._browser + .findElement( By.css('atlas-layer-selector') ) + .findElements( By.css(`mat-grid-tile`) ) + + const idx = await _getIndexFromArrayOfWebElements(templateName, allTiles) + if (idx >= 0) await allTiles[idx].click() + else throw new Error(`#changeTemplate: templateName ${templateName} cannot be found.`) + } + + async changeParc(parcName) { + throw new Error(`changeParc NYI`) + } + + async selectAtlasTemplateParcellation(atlasName, templateName, parcellationName, parcVersion) { + if (!atlasName) throw new Error(`atlasName needs to be provided`) + try { + /** + * if at title screen + */ + await (await this._findTitleCard(atlasName)).click() + } catch (e) { + /** + * if not at title screen + * select from dropdown + */ + } + + if (templateName) { + await this.wait(1000) + await this.waitUntilAllChunksLoaded() + await this.changeTemplate(templateName) + } + + if (parcellationName) { + await this.wait(1000) + await this.waitUntilAllChunksLoaded() + await this.changeParc(parcellationName) + } + + await this._setAtlasSelectorExpanded(false) + } + + /** + * Sidenav + */ + _getSideNavPrimary(){ + return this._browser.findElement( + By.css('mat-drawer[data-mat-drawer-primary-open]') + ) + } + + async _getSideNavPrimaryExpanded(){ + return (await this._getSideNavPrimary() + .getAttribute('data-mat-drawer-primary-open')) === 'true' + } + + _getSideNavSecondary(){ + return this._browser.findElement( + By.css('mat-drawer[data-mat-drawer-secondary-open]') + ) + } + + async _getSideNavSecondaryExpanded(){ + return (await this._getSideNavSecondary() + .getAttribute('data-mat-drawer-secondary-open')) === 'true' + } + + async _setSideNavPrimaryExpanded(flag) { + const openedFlag = await this._getSideNavPrimaryExpanded() + if (typeof flag === 'undefined' || flag !== openedFlag) { + await this._browser.findElement(By.css(`button[aria-label="${ARIA_LABELS.TOGGLE_SIDE_PANEL}"]`)).click() + } + } + + async getSideNavTag(){ + return await this._browser + .findElement( By.css('[mat-drawer-trigger]') ) + .findElement( By.css('i') ) + } + + sideNavIsVisible(){ + throw new Error(`sideNavIsVisible is deprecated`) + } + + _getSideNavTab(){ + return this._browser + .findElement( By.css('[mat-drawer-trigger]') ) + .findElement( By.css('i') ) + } + + sideNavTabIsVisible(){ + return this._getSideNavTab().isDisplayed() + } + + clickSideNavTab(){ + return this._getSideNavTab().click() + } + + /** + * StatusPanel + */ + _getStatusPanel(){ + return this._browser.findElement( By.css('[mat-drawer-status-panel]') ) + } + + async statusPanelIsVisible() { + try { + return await this._getStatusPanel().isDisplayed() + } catch (e) { + return false + } + } + + clickStatusPanel() { + // Will throw if status panel is not visible + return this._getStatusPanel().click() + } + + async getTemplateInfo(){ + throw new Error(`getTemplateInfo has been deprecated. Implmenet new method of getting info`) + } + + // will throw if additional layer control is not visible + additionalLayerControlIsExpanded() { + throw new Error(`additionalLayerControlIsExpanded is deprecated`) + } + + /** + * TODO? deprecate + */ + async toggleNthLayerControl(idx) { + const els = await this._browser + .findElement( + By.css(`[aria-label="${ARIA_LABELS.ADDITIONAL_VOLUME_CONTROL}"]`) + ) + .findElements( By.css(`[aria-label="${ARIA_LABELS.TOGGLE_SHOW_LAYER_CONTROL}"]`)) + if (!els[idx]) throw new Error(`toggleNthLayerControl index out of bound: accessor ${idx} with length ${els.length}`) + await els[idx].click() + } + + // Signin banner + _getToolsIcon(){ + return this._driver + .findElement( By.css('[aria-label="Show tools and plugins"]') ) + } + + async showToolsMenu(){ + await this._getToolsIcon().click() + } + + _getToolsMenu(){ + return this._driver + .findElement( By.css('[aria-label="Tools and plugins menu"]') ) + } + + _getAllTools(){ + return this._getToolsMenu().findElements( By.css('[role="menuitem"]') ) + } + + async getVisibleTools(){ + // may throw if tools menu not visible + const menuItems = await this._getAllTools() + const returnArr = [] + for (const menuItem of menuItems){ + returnArr.push( + await _getTextFromWebElement(menuItem) + ) + } + return returnArr + } + + async clickOnNthTool(index, cssSelector){ + const menuItems = await this._getAllTools() + if (!menuItems[index]) throw new Error(`index out of bound: accessing index ${index} of length ${menuItems.length}`) + if (cssSelector) await menuItems[index].findElement( By.css(cssSelector) ).click() + else await menuItems[index].click() + } + + _getFavDatasetIcon(){ + return this._driver + .findElement( By.css('[aria-label="Show pinned datasets"]') ) + } + + async getNumberOfFavDataset(){ + const attr = await this._getFavDatasetIcon().getAttribute('pinned-datasets-length') + return Number(attr) + } + + async showPinnedDatasetPanel(){ + await this._getFavDatasetIcon().click() + await this.wait(500) + } + + _getPinnedDatasetPanel(){ + return this._driver + .findElement( + By.css('[aria-label="Pinned datasets panel"]') + ) + } + + async getPinnedDatasetsFromOpenedPanel(){ + const list = await this._getPinnedDatasetPanel() + .findElements( + By.tagName('mat-list-item') + ) + + const returnArr = [] + for (const el of list) { + const text = await _getTextFromWebElement(el) + returnArr.push(text) + } + return returnArr + } + + async unpinNthDatasetFromOpenedPanel(index){ + const list = await this._getPinnedDatasetPanel() + .findElements( + By.tagName('mat-list-item') + ) + + if (!list[index]) throw new Error(`index out of bound: ${index} in list with size ${list.length}`) + await list[index] + .findElement( By.css('[aria-label="Toggle pinning this dataset"]') ) + .click() + } + + _getWidgetPanel(title){ + return this._driver.findElement( By.css(`[aria-label="Widget for ${title}"]`) ) + } + + async widgetPanelIsDispalyed(title){ + try { + const isDisplayed = await this._getWidgetPanel(title).isDisplayed() + return isDisplayed + } catch (e) { + console.warn(`widgetPanelIsDisplayed error`, e) + return false + } + } + + async closeWidgetByname(title){ + await this._getWidgetPanel(title) + .findElement( By.css(`[aria-label="close"]`) ) + .click() + } +} + +module.exports = { + WdLayoutPage, + WdBase, +} \ No newline at end of file diff --git a/e2e/util/selenium/util.js b/e2e/util/selenium/util.js new file mode 100644 index 0000000000000000000000000000000000000000..b08adb34bea8a064c397a9ab76209c4f73cf65d4 --- /dev/null +++ b/e2e/util/selenium/util.js @@ -0,0 +1,18 @@ + +function _getTextFromWebElement(webElement) { + return webElement.getText() +} + +async function _getIndexFromArrayOfWebElements(search, webElements) { + const texts = await Promise.all( + webElements.map(_getTextFromWebElement) + ) + return texts.findIndex(text => search instanceof RegExp + ? search.test(text) + : text.indexOf(search) >= 0) +} + +module.exports = { + _getTextFromWebElement, + _getIndexFromArrayOfWebElements +} \ No newline at end of file