From 7b77bf4bdd0076e0bcd3657c465da12c280e8726 Mon Sep 17 00:00:00 2001 From: Matt Weiden <538456+mweiden@users.noreply.github.com> Date: Wed, 4 Mar 2020 16:11:32 -0800 Subject: [PATCH] Make smoke tests faster, more stable (#1195) * Refactor smoke tests & utils for conciseness/style * Modularize test utilities --- client/__tests__/e2e/cellxgeneActions.js | 196 ++++++++++++ client/__tests__/e2e/e2e.test.js | 71 ++--- client/__tests__/e2e/e2eAnnotations.test.js | 63 ++-- client/__tests__/e2e/puppeteerUtils.js | 332 ++------------------ client/__tests__/e2e/testBrowser.js | 35 +++ 5 files changed, 320 insertions(+), 377 deletions(-) create mode 100644 client/__tests__/e2e/cellxgeneActions.js create mode 100644 client/__tests__/e2e/testBrowser.js diff --git a/client/__tests__/e2e/cellxgeneActions.js b/client/__tests__/e2e/cellxgeneActions.js new file mode 100644 index 00000000..18abcb92 --- /dev/null +++ b/client/__tests__/e2e/cellxgeneActions.js @@ -0,0 +1,196 @@ +import { strict as assert } from "assert"; + +export const cellxgeneActions = (page, utils) => ({ + + async drag(testId, start, end, lasso = false) { + const layout = await utils.waitByID(testId); + const elBox = await layout.boxModel(); + const x1 = elBox.content[0].x + start.x; + const x2 = elBox.content[0].x + end.x; + const y1 = elBox.content[0].y + start.y; + const y2 = elBox.content[0].y + end.y; + await page.mouse.move(x1, y1); + await page.mouse.down(); + if (lasso) { + await page.mouse.move(x2, y1); + await page.mouse.move(x2, y2); + await page.mouse.move(x1, y2); + await page.mouse.move(x1, y1); + } else { + await page.mouse.move(x2, y2); + } + await page.mouse.up(); + }, + + async clickOnCoordinate(testId, coord) { + const layout = await utils.waitByID(testId); + const elBox = await layout.boxModel(); + const x = elBox.content[0].x + coord.x; + const y = elBox.content[0].y + coord.y; + await page.mouse.click(x, y); + }, + + async getAllHistograms(testclass, testIds) { + const histTestIds = testIds.map(tid => `histogram-${tid}`); + // these load asynchronously, so we need to wait for each histogram individually + await utils.waitForAllByIds(histTestIds); + const allHistograms = await utils.getAllByClass(testclass); + return allHistograms.map(hist => hist.replace(/^histogram-/, "")); + }, + + async getAllCategoriesAndCounts(category) { + await utils.waitByClass("categorical-row"); + return page.$$eval( + `[data-testid="category-${category}"] [data-testclass='categorical-row']`, + rows => Object.fromEntries(rows.map(row => { + const cat = row.querySelector("[data-testclass='categorical-value']").innerText; + const count = row.querySelector("[data-testclass='categorical-value-count']").innerText; + return [cat, count]; + })) + ); + }, + + async cellSet(num) { + await utils.clickOn(`cellset-button-${num}`); + return utils.getOneElementInnerText(`[data-testid='cellset-count-${num}']`); + }, + + async resetCategory(category) { + const checkboxId = `${category}:category-select`; + await utils.waitByID(checkboxId); + const checkedPseudoclass = await page.$eval( + `[data-testid='${checkboxId}']`, + el => el.matches(":checked") + ); + if (!checkedPseudoclass) await utils.clickOn(checkboxId); + try { + const categoryRow = await utils.waitByID(`${category}:category-expand`); + const isExpanded = await categoryRow.$("[data-testclass='category-expand-is-expanded']"); + if (isExpanded) await utils.clickOn(`${category}:category-expand`); + } catch {} + }, + + async calcCoordinate(testId, xAsPercent, yAsPercent) { + const el = await utils.waitByID(testId); + const size = await el.boxModel(); + return { + x: Math.floor(size.width * xAsPercent), + y: Math.floor(size.height * yAsPercent) + } + }, + + async calcDragCoordinates(testId, coordinateAsPercent) { + return { + start: await this.calcCoordinate(testId, coordinateAsPercent.x1, coordinateAsPercent.y1), + end: await this.calcCoordinate(testId, coordinateAsPercent.x2, coordinateAsPercent.y2) + }; + }, + + async selectCategory(category, values, reset = true) { + if (reset) await this.resetCategory(category); + await utils.clickOn(`${category}:category-expand`); + await utils.clickOn(`${category}:category-select`); + for (const val of values) { + await utils.clickOn(`categorical-value-select-${category}-${val}`); + } + }, + + async expandCategory(category) { + const expand = await utils.waitByID(`${category}:category-expand`); + const notExpanded = await expand.$("[data-testclass='category-expand-is-not-expanded']"); + if (notExpanded) await utils.clickOn(`${category}:category-expand`); + }, + + async clip(min = 0, max = 100) { + await utils.clickOn("visualization-settings"); + await utils.clearInputAndTypeInto("clip-min-input", min); + await utils.clearInputAndTypeInto("clip-max-input", max); + await utils.clickOn("clip-commit"); + }, + + async createCategory(categoryName) { + await utils.clickOn("open-annotation-dialog"); + await utils.typeInto("new-category-name", categoryName); + await utils.clickOn("submit-category"); + }, + + async renameCategory(oldCatgoryName, newCategoryName) { + await utils.clickOn(`${oldCatgoryName}:see-actions`); + await utils.clickOn(`${oldCatgoryName}:edit-category-mode`); + await utils.clearInputAndTypeInto(`${oldCatgoryName}:edit-category-name-text`, newCategoryName); + await utils.clickOn(`${oldCatgoryName}:submit-category-edit`); + }, + + async deleteCategory(categoryName) { + await utils.clickOn(`${categoryName}:see-actions`); + await utils.clickOn(`${categoryName}:delete-category`); + }, + + async createLabel(categoryName, labelName) { + await utils.clickOn(`${categoryName}:see-actions`); + await utils.clickOn(`${categoryName}:add-new-label-to-category`); + await utils.typeInto(`${categoryName}:new-label-name`, labelName); + await utils.clickOn(`${categoryName}:submit-label`); + }, + + async deleteLabel(categoryName, labelName) { + await this.expandCategory(categoryName); + await utils.clickOn(`${categoryName}:${labelName}:see-actions`); + await utils.clickOn( `${categoryName}:${labelName}:delete-label`); + }, + + async renameLabel(categoryName, oldLabelName, newLabelName) { + await this.expandCategory(categoryName); + await utils.clickOn(`${categoryName}:${oldLabelName}:see-actions`); + await utils.clickOn(`${categoryName}:${oldLabelName}:edit-label`); + await utils.clearInputAndTypeInto( + `${categoryName}:${oldLabelName}:edit-label-name`, + newLabelName + ); + await utils.clickOn(`${categoryName}:${oldLabelName}:submit-label-edit`); + }, + + async addGeneToSearch(geneName) { + await utils.typeInto("gene-search", geneName); + await page.keyboard.press("Enter"); + await page.waitForSelector( + `[data-testid='histogram-${geneName}']` + ); + }, + + async subset(coordinatesAsPercent) { + // In order to deselect the selection after the subset, make sure we have some clear part + // of the scatterplot we can click on + assert(coordinatesAsPercent.x2 < 0.99 || coordinatesAsPercent.y2 < 0.99); + const lassoSelection = await this.calcDragCoordinates( "layout-graph", coordinatesAsPercent); + await this.drag("layout-graph", lassoSelection.start, lassoSelection.end, true ); + await utils.clickOn("subset-button"); + const clearCoordinate = await this.calcCoordinate( + "layout-graph", + 0.5, + 0.99 + ); + await this.clickOnCoordinate("layout-graph", clearCoordinate); + }, + + async setSellSet(cellSet, cellSetNum) { + for (const selection of cellSet.filter(sel => sel.kind === "categorical")) { + await this.selectCategory(selection.metadata, selection.values, true); + } + await this.cellSet(cellSetNum); + }, + + async runDiffExp(cellSet1, cellSet2) { + await this.setSellSet(cellSet1, 1); + await this.setSellSet(cellSet2, 2); + await utils.clickOn("diffexp-button"); + }, + + async bulkAddGenes(geneNames) { + await utils.clickOn("section-bulk-add"); + await utils.typeInto("input-bulk-add", geneNames.join(",")); + await page.keyboard.press("Enter"); + } +}); + + diff --git a/client/__tests__/e2e/e2e.test.js b/client/__tests__/e2e/e2e.test.js index cf5cbba1..6dad908c 100644 --- a/client/__tests__/e2e/e2e.test.js +++ b/client/__tests__/e2e/e2e.test.js @@ -3,16 +3,16 @@ Smoke test suite that will be run in Travis CI Tests included in this file are expected to be relatively stable and test core features */ -import { appUrlBase, DEBUG, DATASET } from "./config"; -import { setupTestBrowser } from "./puppeteerUtils"; +import { appUrlBase, DATASET } from "./config"; +import { setupTestBrowser } from "./testBrowser"; import { datasets } from "./data"; -let browser, page, utils, cxgActions, spy; -const browserViewport = { width: 1280, height: 960 }; -let data = datasets[DATASET]; +let browser, page, utils, cxgActions; +const data = datasets[DATASET]; beforeAll(async () => { - [browser, page, utils, cxgActions] = await setupTestBrowser(browserViewport); + const browserViewport = { width: 1280, height: 960 }; + [browser, page, utils, cxgActions] = await setupTestBrowser(browserViewport); }); beforeEach(async () => { @@ -20,13 +20,13 @@ beforeEach(async () => { }); afterAll(() => { - browser.close(); + if (browser !== undefined) browser.close(); }); describe("did launch", () => { test("page launched", async () => { - let el = await utils.getOneElementInnerHTML("[data-testid='header']"); - expect(el).toBe(data.title); + const element = await utils.getOneElementInnerHTML("[data-testid='header']"); + expect(element).toBe(data.title); }); }); @@ -34,9 +34,7 @@ describe("metadata loads", () => { test("categories and values from dataset appear", async () => { for (const label in data.categorical) { await utils.waitByID(`category-${label}`); - const categoryName = await utils.getOneElementInnerText( - `[data-testid="category-${label}"]` - ); + const categoryName = await utils.getOneElementInnerText(`[data-testid="category-${label}"]`); expect(categoryName).toMatch(label); await utils.clickOn(`${label}:category-expand`); const categories = await cxgActions.getAllCategoriesAndCounts(label); @@ -84,9 +82,7 @@ describe("cell selection", () => { await utils.clickOn(`${cellset.metadata}:category-expand`); await utils.clickOn(`${cellset.metadata}:category-select`); for (const val of cellset.values) { - await utils.clickOn( - `categorical-value-select-${cellset.metadata}-${val}` - ); + await utils.clickOn(`categorical-value-select-${cellset.metadata}-${val}`); } const cellCount = await cxgActions.cellSet(1); expect(cellCount).toBe(cellset.count); @@ -108,20 +104,12 @@ describe("cell selection", () => { }); describe("gene entry", () => { - test("search for single gene", async () => { - await cxgActions.addGeneToSearch(data.genes.search); - }); + test("search for single gene", async () => cxgActions.addGeneToSearch(data.genes.search)); test("bulk add genes", async () => { const testGenes = data.genes.bulkadd; - await utils.clickOn("section-bulk-add"); - await utils.typeInto("input-bulk-add", testGenes.join(",")); - await page.keyboard.press("Enter"); - - const allHistograms = await cxgActions.getAllHistograms( - "histogram-user-gene", - testGenes - ); + await cxgActions.bulkAddGenes(testGenes); + const allHistograms = await cxgActions.getAllHistograms("histogram-user-gene", testGenes); expect(allHistograms).toEqual(expect.arrayContaining(testGenes)); expect(allHistograms.length).toEqual(testGenes.length); }); @@ -182,54 +170,47 @@ describe("subset", () => { }); test("undo selection appends the top diff exp genes to user defined genes", async () => { - const userDefinedGenes = ["ACD", "AAR2", "AATF", "ARSG"]; + const userDefinedGenes = data.genes.bulkadd; const diffExpGenes = data.diffexp["gene-results"]; - for (const userDefinedGene of userDefinedGenes) { - await cxgActions.addGeneToSearch(userDefinedGene); - } + await cxgActions.bulkAddGenes(userDefinedGenes); const userDefinedHistograms = await cxgActions.getAllHistograms("histogram-user-gene", userDefinedGenes); - expect(userDefinedHistograms).toStrictEqual(userDefinedGenes); + expect(userDefinedHistograms).toEqual(expect.arrayContaining(userDefinedGenes)); await cxgActions.subset({x1: 0.15, y1: 0.10, x2: 0.98, y2: 0.98}); await cxgActions.runDiffExp(data.diffexp.cellset1, data.diffexp.cellset2); const diffExpHistograms = await cxgActions.getAllHistograms("histogram-diffexp", diffExpGenes); - expect(diffExpHistograms).toStrictEqual(diffExpGenes); + expect(diffExpHistograms).toEqual(expect.arrayContaining(diffExpGenes)); await utils.clickOn("reset-subset-button"); const expected = [].concat(userDefinedGenes, diffExpGenes); const userDefinedHistogramsAfterSubset = await cxgActions.getAllHistograms( "histogram-user-gene", expected ); - expect(userDefinedHistogramsAfterSubset).toStrictEqual(expected); + expect(userDefinedHistogramsAfterSubset).toEqual(expect.arrayContaining(expected)); }); test("subset selection appends the top diff exp genes to user defined genes", async () => { - const userDefinedGenes = ["ACD", "AAR2", "AATF", "ARSG"]; + const userDefinedGenes = data.genes.bulkadd; const diffExpGenes = data.diffexp["gene-results"]; - for (const userDefinedGene of userDefinedGenes) { - await cxgActions.addGeneToSearch(userDefinedGene); - } + await cxgActions.bulkAddGenes(userDefinedGenes); const userDefinedHistograms = await cxgActions.getAllHistograms("histogram-user-gene", userDefinedGenes); - expect(userDefinedHistograms).toStrictEqual(userDefinedGenes); + expect(userDefinedHistograms).toEqual(expect.arrayContaining(userDefinedGenes)); await cxgActions.subset({x1: 0.15, y1: 0.10, x2: 0.98, y2: 0.98}); await cxgActions.runDiffExp(data.diffexp.cellset1, data.diffexp.cellset2); const diffExpHistograms = await cxgActions.getAllHistograms("histogram-diffexp", diffExpGenes); - expect(diffExpHistograms).toStrictEqual(diffExpGenes); + expect(diffExpHistograms).toEqual(expect.arrayContaining(diffExpGenes)); await cxgActions.subset({x1: 0.16, y1: 0.11, x2: 0.97, y2: 0.97}); const expected = [].concat(userDefinedGenes, diffExpGenes); const userDefinedHistogramsAfterSubset = await cxgActions.getAllHistograms( "histogram-user-gene", expected ); - expect(userDefinedHistogramsAfterSubset).toStrictEqual(expected); + expect(userDefinedHistogramsAfterSubset).toEqual(expect.arrayContaining(expected)); }); }); describe("scatter plot", () => { test("scatter plot appears", async () => { - const testGenes = data.scatter.genes; - await utils.clickOn("section-bulk-add"); - await utils.typeInto("input-bulk-add", Object.values(testGenes).join(",")); - await page.keyboard.press("Enter"); + await cxgActions.bulkAddGenes(Object.values(data.scatter.genes)); await utils.clickOn(`plot-x-${data.scatter.genes.x}`); await utils.clickOn(`plot-y-${data.scatter.genes.y}`); await utils.waitByID("scatterplot"); @@ -297,6 +278,6 @@ describe("ui elements don't error", () => { panCoords.end, false ); - await page.evaluate(`window.scrollBy(0, 1000);`); + await page.evaluate("window.scrollBy(0, 1000);"); }); }); diff --git a/client/__tests__/e2e/e2eAnnotations.test.js b/client/__tests__/e2e/e2eAnnotations.test.js index b74e55d9..30677321 100644 --- a/client/__tests__/e2e/e2eAnnotations.test.js +++ b/client/__tests__/e2e/e2eAnnotations.test.js @@ -1,20 +1,20 @@ /* Tests included in this file are specific to annotation features */ -import {appUrlBase, DEBUG, DEV, DATASET} from "./config"; -import {setupTestBrowser} from "./puppeteerUtils"; -import {datasets} from "./data"; +import { appUrlBase, DATASET } from "./config"; +import { setupTestBrowser } from "./testBrowser"; +import { datasets } from "./data"; -let browser, page, utils, cxgActions; -const browserViewport = {width: 1280, height: 960}; +let browser, page, utils, actions; const data = datasets[DATASET]; beforeAll(async () => { - [browser, page, utils, cxgActions] = await setupTestBrowser(browserViewport); + const browserViewport = {width: 1280, height: 960}; + [browser, page, utils, actions] = await setupTestBrowser(browserViewport); }); afterAll(() => { - if (!DEBUG) browser.close(); + if (browser !== undefined) browser.close() }); describe.each([ @@ -30,9 +30,9 @@ describe.each([ // wait for the page to load await utils.waitByClass("autosave-complete"); // setup the test fixtures - await cxgActions.createCategory(perTestCategoryName); - await cxgActions.createLabel(perTestCategoryName, perTestLabelName); - if (config.withSubset) await cxgActions.subset({x1: 0.10, y1: 0.10, x2: 0.80, y2: 0.80}); + await actions.createCategory(perTestCategoryName); + await actions.createLabel(perTestCategoryName, perTestLabelName); + if (config.withSubset) await actions.subset({x1: 0.10, y1: 0.10, x2: 0.80, y2: 0.80}); await utils.waitByClass("autosave-complete"); }); @@ -44,18 +44,18 @@ describe.each([ test("create a category", async () => { const categoryName = `category-created-${config.tag}`; await assertCategoryDoesNotExist(categoryName); - await cxgActions.createCategory(categoryName); + await actions.createCategory(categoryName); await assertCategoryExists(categoryName); }); test("delete a category", async () => { - await cxgActions.deleteCategory(perTestCategoryName); + await actions.deleteCategory(perTestCategoryName); await assertCategoryDoesNotExist(perTestCategoryName); }); test("rename a category", async () => { const newCategoryName = `cluster-for-real-${config.tag}`; - await cxgActions.renameCategory(perTestCategoryName, newCategoryName); + await actions.renameCategory(perTestCategoryName, newCategoryName); await assertCategoryDoesNotExist(perTestCategoryName); await assertCategoryExists(newCategoryName); }); @@ -63,19 +63,19 @@ describe.each([ test("create a label", async () => { const labelName = `new-label-${config.tag}`; await assertLabelDoesNotExist(perTestCategoryName, labelName); - await cxgActions.createLabel(perTestCategoryName, labelName); + await actions.createLabel(perTestCategoryName, labelName); await assertLabelExists(perTestCategoryName, labelName); }); test("delete a label", async () => { - await cxgActions.deleteLabel(perTestCategoryName, perTestLabelName); + await actions.deleteLabel(perTestCategoryName, perTestLabelName); await assertLabelDoesNotExist(perTestCategoryName, perTestLabelName); }); test("rename a label", async () => { const newLabelName = "my-cool-new-label"; await assertLabelDoesNotExist(perTestCategoryName, newLabelName); - await cxgActions.renameLabel(perTestCategoryName, perTestLabelName, newLabelName); + await actions.renameLabel(perTestCategoryName, perTestLabelName, newLabelName); await assertLabelDoesNotExist(perTestCategoryName, perTestLabelName); await assertLabelExists(perTestCategoryName, newLabelName); }); @@ -83,7 +83,7 @@ describe.each([ test("check cell count for a label loaded from file", async () => { const categoryName = "cluster-test"; const labelName = "four"; - await cxgActions.expandCategory(categoryName); + await actions.expandCategory(categoryName); const result = await utils.waitByID(`categorical-value-count-${categoryName}-${labelName}`); expect(await result.evaluate(node => node.innerText)).toBe( data.annotationsFromFile.count.bySubsetConfig[config.withSubset] @@ -91,12 +91,12 @@ describe.each([ }); test("assign cells to a label", async () => { - await cxgActions.expandCategory(perTestCategoryName); - const lassoSelection = await cxgActions.calcDragCoordinates( + await actions.expandCategory(perTestCategoryName); + const lassoSelection = await actions.calcDragCoordinates( "layout-graph", data.categoryLabel.lasso["coordinates-as-percent"] ); - await cxgActions.drag( + await actions.drag( "layout-graph", lassoSelection.start, lassoSelection.end, @@ -114,7 +114,7 @@ describe.each([ test("undo/redo category creation", async () => { const categoryName = `category-created-undo-${config.tag}`; await assertCategoryDoesNotExist(categoryName); - await cxgActions.createCategory(categoryName); + await actions.createCategory(categoryName); await assertCategoryExists(categoryName); await utils.clickOn("undo"); await assertCategoryDoesNotExist(categoryName); @@ -124,9 +124,9 @@ describe.each([ test("undo/redo category deletion", async () => { const categoryName = `category-deleted-undo-${config.tag}`; - await cxgActions.createCategory(categoryName); + await actions.createCategory(categoryName); await assertCategoryExists(categoryName); - await cxgActions.deleteCategory(categoryName); + await actions.deleteCategory(categoryName); await assertCategoryDoesNotExist(categoryName); await utils.clickOn("undo"); await assertCategoryExists(categoryName); @@ -137,7 +137,7 @@ describe.each([ test("undo/redo category rename", async () => { const newCategoryName = `category-renamed-undo-${config.tag}`; await assertCategoryDoesNotExist(newCategoryName); - await cxgActions.renameCategory(perTestCategoryName, newCategoryName); + await actions.renameCategory(perTestCategoryName, newCategoryName); await assertCategoryExists(newCategoryName); await assertCategoryDoesNotExist(perTestCategoryName); await utils.clickOn("undo"); @@ -151,7 +151,7 @@ describe.each([ test("undo/redo label creation", async () => { const labelName = `label-created-undo-${config.tag}`; await assertLabelDoesNotExist(perTestCategoryName, labelName); - await cxgActions.createLabel(perTestCategoryName, labelName); + await actions.createLabel(perTestCategoryName, labelName); await assertLabelExists(perTestCategoryName, labelName); await utils.clickOn("undo"); await assertLabelDoesNotExist(perTestCategoryName); @@ -160,7 +160,7 @@ describe.each([ }); test("undo/redo label deletion", async () => { - await cxgActions.deleteLabel(perTestCategoryName, perTestLabelName); + await actions.deleteLabel(perTestCategoryName, perTestLabelName); await assertLabelDoesNotExist(perTestCategoryName); await utils.clickOn("undo"); await assertLabelExists(perTestCategoryName, perTestLabelName); @@ -171,7 +171,7 @@ describe.each([ test("undo/redo label rename", async () => { const newLabelName = `label-renamed-undo-${config.tag}`; await assertLabelDoesNotExist(perTestCategoryName, newLabelName); - await cxgActions.renameLabel(perTestCategoryName, perTestLabelName, newLabelName); + await actions.renameLabel(perTestCategoryName, perTestLabelName, newLabelName); await assertLabelExists(perTestCategoryName, newLabelName); await assertLabelDoesNotExist(perTestCategoryName, perTestLabelName); await utils.clickOn("undo"); @@ -195,13 +195,13 @@ describe.each([ async function assertLabelExists(categoryName, labelName) { const category = await utils.waitByID(`${categoryName}:category-expand`); expect(category).not.toBeNull(); - await cxgActions.expandCategory(categoryName); + await actions.expandCategory(categoryName); const previous = await utils.waitByID(`categorical-value-${categoryName}-${labelName}`); expect(await previous.evaluate(node => node.innerText)).toBe(labelName); } async function assertLabelDoesNotExist(categoryName, labelName) { - await cxgActions.expandCategory(categoryName); + await actions.expandCategory(categoryName); const result = await page.$(`[data-testid='categorical-value-${categoryName}-${labelName}']`); expect(result).toBeNull(); } @@ -212,9 +212,8 @@ describe.each([ `[data-testid='${categoryName}:category-expand']`, {timeout: 200} ); - if (category !== null) return await cxgActions.deleteCategory(categoryName); - } catch (error) { - } + if (category !== null) return await actions.deleteCategory(categoryName); + } catch {} return null } }); diff --git a/client/__tests__/e2e/puppeteerUtils.js b/client/__tests__/e2e/puppeteerUtils.js index 15404ad3..7f3411c6 100644 --- a/client/__tests__/e2e/puppeteerUtils.js +++ b/client/__tests__/e2e/puppeteerUtils.js @@ -1,332 +1,64 @@ -import {DEBUG, DEV} from "./config"; -import puppeteer from "puppeteer"; -import { strict as assert } from "assert"; +export const puppeteerUtils = page => ({ -export const puppeteerUtils = puppeteerPage => ({ - async waitByID(testid, props = {}) { - return await puppeteerPage.waitForSelector( - `[data-testid='${testid}']`, - props - ); + async waitByID(testId, props = {}) { + return page.waitForSelector(`[data-testid='${testId}']`, props); }, - async waitByClass(testclass, props = {}) { - return await puppeteerPage.waitForSelector( - `[data-testclass='${testclass}']`, - props - ); + async waitByClass(testClass, props = {}) { + return page.waitForSelector(`[data-testclass='${testClass}']`, props); }, - async waitForAllByIds(testids) { + async waitForAllByIds(testIds) { await Promise.all( - testids.map(testid => - puppeteerPage.waitForSelector(`[data-testid='${testid}']`) - ) + testIds.map(testId => page.waitForSelector(`[data-testid='${testId}']`)) ); }, - async getAllByClass(testclass) { - const elements = await puppeteerPage.$$eval( - `[data-testclass=${testclass}]`, + async getAllByClass(testClass) { + return page.$$eval( + `[data-testclass=${testClass}]`, eles => eles.map(ele => ele.dataset.testid) ); - return elements; }, - async typeInto(testid, text) { + async typeInto(testId, text) { // blueprint's typeahead is treating typing weird, clicking & waiting first solves this // only works for text without special characters - await this.waitByID(testid); - const selector = `[data-testid='${testid}']`; + await this.waitByID(testId); + const selector = `[data-testid='${testId}']`; // type ahead can be annoying if you don't pause before you type - await puppeteerPage.click(selector); - await puppeteerPage.waitFor(200); - await puppeteerPage.type(selector, text); + await page.click(selector); + await page.waitFor(200); + await page.type(selector, text); }, - async clearInputAndTypeInto(testid, text) { - await this.waitByID(testid); - const selector = `[data-testid='${testid}']`; + async clearInputAndTypeInto(testId, text) { + await this.waitByID(testId); + const selector = `[data-testid='${testId}']`; // only works for text without special characters // type ahead can be annoying if you don't pause before you type - await puppeteerPage.click(selector); - await puppeteerPage.waitFor(200); + await page.click(selector); + await page.waitFor(200); // select all - - await puppeteerPage.click(selector, { clickCount: 3 }); - await puppeteerPage.keyboard.press("Backspace"); - await puppeteerPage.type(selector, text); + await page.click(selector, { clickCount: 3 }); + await page.keyboard.press("Backspace"); + await page.type(selector, text); }, - async clickOn(testid, options={}) { - await this.waitByID(testid); - await puppeteerPage.click(`[data-testid='${testid}']`, options); - await puppeteerPage.waitFor(50); + async clickOn(testId, options={}) { + await this.waitByID(testId); + await page.click(`[data-testid='${testId}']`, options); + await page.waitFor(50); }, async getOneElementInnerHTML(selector) { - await puppeteerPage.waitForSelector(selector); - let text = await puppeteerPage.$eval(selector, el => el.innerHTML); - return text; + await page.waitForSelector(selector); + return page.$eval(selector, el => el.innerHTML); }, async getOneElementInnerText(selector) { - await puppeteerPage.waitForSelector(selector); - let text = await puppeteerPage.$eval(selector, el => el.innerText); - return text; + await page.waitForSelector(selector); + return page.$eval(selector, el => el.innerText); } }); -export const cellxgeneActions = puppeteerPage => ({ - - async drag(testid, start, end, lasso = false) { - const layout = await puppeteerUtils(puppeteerPage).waitByID(testid); - const elBox = await layout.boxModel(); - const x1 = elBox.content[0].x + start.x; - const x2 = elBox.content[0].x + end.x; - const y1 = elBox.content[0].y + start.y; - const y2 = elBox.content[0].y + end.y; - await puppeteerPage.mouse.move(x1, y1); - await puppeteerPage.mouse.down(); - if (lasso) { - await puppeteerPage.mouse.move(x2, y1); - await puppeteerPage.mouse.move(x2, y2); - await puppeteerPage.mouse.move(x1, y2); - await puppeteerPage.mouse.move(x1, y1); - } else { - await puppeteerPage.mouse.move(x2, y2); - } - await puppeteerPage.mouse.up(); - }, - - async clickOnCoordinate(testid, coord) { - const layout = await puppeteerUtils(puppeteerPage).waitByID(testid); - const elBox = await layout.boxModel(); - const x = elBox.content[0].x + coord.x; - const y = elBox.content[0].y + coord.y; - await puppeteerPage.mouse.click(x, y); - }, - - async getAllHistograms(testclass, testids) { - const histTestIds = testids.map(tid => `histogram-${tid}`); - // these load asynchronously, so we need to wait for each histogram individually - await puppeteerUtils(puppeteerPage).waitForAllByIds(histTestIds); - const allHistograms = await puppeteerUtils(puppeteerPage).getAllByClass( - testclass - ); - return allHistograms.map(hist => - hist.substr("histogram-".length, hist.length) - ); - }, - - async getAllCategoriesAndCounts(category) { - await puppeteerUtils(puppeteerPage).waitByClass("categorical-row"); - const categories = await puppeteerPage.$$eval( - `[data-testid="category-${category}"] [data-testclass='categorical-row']`, - els => { - let result = {}; - els.forEach(el => { - const cat = el.querySelector("[data-testclass='categorical-value']") - .innerText; - const count = el.querySelector( - "[data-testclass='categorical-value-count']" - ).innerText; - result[cat] = count; - }); - return result; - } - ); - return categories; - }, - - async cellSet(num) { - await puppeteerUtils(puppeteerPage).clickOn(`cellset-button-${num}`); - return await puppeteerUtils(puppeteerPage).getOneElementInnerText( - `[data-testid='cellset-count-${num}']` - ); - }, - - async resetCategory(category) { - const checkboxId = `${category}:category-select`; - await puppeteerUtils(puppeteerPage).waitByID(checkboxId); - const checkedPseudoclass = await puppeteerPage.$eval( - `[data-testid='${checkboxId}']`, - el => { - return el.matches(":checked"); - } - ); - if (!checkedPseudoclass) { - await puppeteerUtils(puppeteerPage).clickOn(checkboxId); - } - try { - const categoryRow = await puppeteerUtils(puppeteerPage).waitByID( - `${category}:category-expand` - ); - const isExpanded = await categoryRow.$( - "[data-testclass='category-expand-is-expanded']" - ); - if (isExpanded) { - await puppeteerUtils(puppeteerPage).clickOn( - `${category}:category-expand` - ); - } - } catch {} - }, - - async calcCoordinate(testid, xAsPercent, yAsPercent) { - const el = await puppeteerUtils(puppeteerPage).waitByID(testid); - const size = await el.boxModel(); - return { - x: Math.floor(size.width * xAsPercent), - y: Math.floor(size.height * yAsPercent) - } - }, - - async calcDragCoordinates(testid, coordinateAsPercent) { - const coords = { - start: await this.calcCoordinate(testid, coordinateAsPercent.x1, coordinateAsPercent.y1), - end: await this.calcCoordinate(testid, coordinateAsPercent.x2, coordinateAsPercent.y2) - }; - return coords; - }, - - async selectCategory(category, values, reset = true) { - if (reset) await this.resetCategory(category); - await puppeteerUtils(puppeteerPage).clickOn(`${category}:category-expand`); - await puppeteerUtils(puppeteerPage).clickOn(`${category}:category-select`); - for (const val of values) { - await puppeteerUtils(puppeteerPage).clickOn(`categorical-value-select-${category}-${val}`); - } - }, - - async expandCategory(category) { - const expand = await puppeteerUtils(puppeteerPage).waitByID(`${category}:category-expand`); - const notExpanded = await expand.$("[data-testclass='category-expand-is-not-expanded']"); - if (notExpanded) { - await puppeteerUtils(puppeteerPage).clickOn(`${category}:category-expand`); - } - }, - - async clip(min = 0, max = 100) { - await puppeteerUtils(puppeteerPage).clickOn("visualization-settings"); - await puppeteerUtils(puppeteerPage).clearInputAndTypeInto( - "clip-min-input", - min - ); - await puppeteerUtils(puppeteerPage).clearInputAndTypeInto( - "clip-max-input", - max - ); - await puppeteerUtils(puppeteerPage).clickOn("clip-commit"); - }, - - async createCategory(categoryName) { - await puppeteerUtils(puppeteerPage).clickOn("open-annotation-dialog"); - await puppeteerUtils(puppeteerPage).typeInto("new-category-name", categoryName); - await puppeteerUtils(puppeteerPage).clickOn("submit-category"); - }, - - async renameCategory(oldCatgoryName, newCategoryName) { - await puppeteerUtils(puppeteerPage).clickOn(`${oldCatgoryName}:see-actions`); - await puppeteerUtils(puppeteerPage).clickOn(`${oldCatgoryName}:edit-category-mode`); - await puppeteerUtils(puppeteerPage).clearInputAndTypeInto(`${oldCatgoryName}:edit-category-name-text`, newCategoryName); - await puppeteerUtils(puppeteerPage).clickOn(`${oldCatgoryName}:submit-category-edit`); - }, - - async deleteCategory(categoryName) { - await puppeteerUtils(puppeteerPage).clickOn(`${categoryName}:see-actions`); - await puppeteerUtils(puppeteerPage).clickOn(`${categoryName}:delete-category`); - }, - - async createLabel(categoryName, labelName) { - await puppeteerUtils(puppeteerPage).clickOn(`${categoryName}:see-actions`); - await puppeteerUtils(puppeteerPage).clickOn(`${categoryName}:add-new-label-to-category`); - await puppeteerUtils(puppeteerPage).typeInto(`${categoryName}:new-label-name`, labelName); - await puppeteerUtils(puppeteerPage).clickOn(`${categoryName}:submit-label`); - }, - - async deleteLabel(categoryName, labelName) { - await this.expandCategory(categoryName); - await puppeteerUtils(puppeteerPage).clickOn(`${categoryName}:${labelName}:see-actions`); - await puppeteerUtils(puppeteerPage).clickOn( `${categoryName}:${labelName}:delete-label`); - }, - - async renameLabel(categoryName, oldLabelName, newLabelName) { - await this.expandCategory(categoryName); - await puppeteerUtils(puppeteerPage).clickOn(`${categoryName}:${oldLabelName}:see-actions`); - await puppeteerUtils(puppeteerPage).clickOn(`${categoryName}:${oldLabelName}:edit-label`); - await puppeteerUtils(puppeteerPage).clearInputAndTypeInto( - `${categoryName}:${oldLabelName}:edit-label-name`, - newLabelName - ); - await puppeteerUtils(puppeteerPage).clickOn(`${categoryName}:${oldLabelName}:submit-label-edit`); - }, - - async addGeneToSearch(geneName) { - await puppeteerUtils(puppeteerPage).typeInto("gene-search", geneName); - await puppeteerPage.keyboard.press("Enter"); - await puppeteerPage.waitForSelector( - `[data-testid='histogram-${geneName}']` - ); - }, - - async subset(coordinatesAsPercent) { - // In order to deselect the selection after the subset, make sure we have some clear part - // of the scatterplot we can click on - assert(coordinatesAsPercent.x2 < 0.99 || coordinatesAsPercent.y2 < 0.99); - const lassoSelection = await this.calcDragCoordinates( "layout-graph", coordinatesAsPercent); - await this.drag("layout-graph", lassoSelection.start, lassoSelection.end, true ); - await puppeteerUtils(puppeteerPage).clickOn("subset-button"); - const clearCoordinate = await this.calcCoordinate( - "layout-graph", - 0.5, - 0.99 - ); - await this.clickOnCoordinate("layout-graph", clearCoordinate); - }, - - async setSellSet(cellSet, cellSetNum) { - for (const selection of cellSet) { - if (selection.kind === "categorical") { - await this.selectCategory(selection.metadata, selection.values, true); - } - } - await this.cellSet(cellSetNum); - }, - - async runDiffExp(cellSet1, cellSet2) { - await this.setSellSet(cellSet1, 1); - await this.setSellSet(cellSet2, 2); - await puppeteerUtils(puppeteerPage).clickOn("diffexp-button"); - } -}); - -export async function setupTestBrowser(browserViewport) { - const browserParams = DEV - ? { headless: false, slowMo: 5 } - : DEBUG - ? { headless: false, slowMo: 100, devtools: true } - : {}; - const browser = await puppeteer.launch(browserParams); - const page = await browser.newPage(); - await page.setViewport(browserViewport); - if (DEV || DEBUG) { - page.on("console", async msg => { - // If there is a console.error but an error is not thrown, this will ensure the test fails - if (msg.type() === "error") { - const errorMsgText = await Promise.all( - // TODO can we do this without internal properties? - msg.args().map(arg => arg._remoteObject.description) - ); - throw new Error(`Console error: ${errorMsgText}`); - } - console.log(`PAGE LOG: ${msg.text()}`); - }); - } - page.on("pageerror", err => { - throw new Error(`Console error: ${err}`); - }); - const utils = puppeteerUtils(page); - const cxgActions = cellxgeneActions(page); - return [browser, page, utils, cxgActions]; -} diff --git a/client/__tests__/e2e/testBrowser.js b/client/__tests__/e2e/testBrowser.js new file mode 100644 index 00000000..a3576adc --- /dev/null +++ b/client/__tests__/e2e/testBrowser.js @@ -0,0 +1,35 @@ +import puppeteer from "puppeteer"; +import { DEBUG, DEV } from "./config"; +import { puppeteerUtils } from "./puppeteerUtils"; +import { cellxgeneActions } from "./cellxgeneActions"; + +export async function setupTestBrowser(browserViewport) { + const browserParams = DEV + ? { headless: false, slowMo: 5 } + : DEBUG + ? { headless: false, slowMo: 100, devtools: true } + : {}; + const browser = await puppeteer.launch(browserParams); + const page = await browser.pages().then(pages => pages[0]); + await page.setViewport(browserViewport); + if (DEV || DEBUG) { + page.on("console", async msg => { + // If there is a console.error but an error is not thrown, this will ensure the test fails + if (msg.type() === "error") { + const errorMsgText = await Promise.all( + // TODO can we do this without internal properties? + msg.args().map(arg => arg._remoteObject.description) + ); + throw new Error(`Console error: ${errorMsgText}`); + } + console.log(`PAGE LOG: ${msg.text()}`); + }); + } + page.on("pageerror", err => { + throw new Error(`Console error: ${err}`); + }); + const utils = puppeteerUtils(page); + const cxgActions = cellxgeneActions(page, utils); + return [browser, page, utils, cxgActions]; +} +