Make smoke tests faster, more stable (#1195)

* Refactor smoke tests & utils for conciseness/style

* Modularize test utilities
This commit is contained in:
Matt Weiden
2020-03-04 16:11:32 -08:00
committed by GitHub
parent cc890fe391
commit 7b77bf4bdd
5 changed files with 320 additions and 377 deletions
+196
View File
@@ -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");
}
});
+26 -45
View File
@@ -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);");
});
});
+31 -32
View File
@@ -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
}
});
+32 -300
View File
@@ -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];
}
+35
View File
@@ -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];
}