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 Tests included in this file are expected to be relatively stable and test core features
*/ */
import { appUrlBase, DEBUG, DATASET } from "./config"; import { appUrlBase, DATASET } from "./config";
import { setupTestBrowser } from "./puppeteerUtils"; import { setupTestBrowser } from "./testBrowser";
import { datasets } from "./data"; import { datasets } from "./data";
let browser, page, utils, cxgActions, spy; let browser, page, utils, cxgActions;
const browserViewport = { width: 1280, height: 960 }; const data = datasets[DATASET];
let data = datasets[DATASET];
beforeAll(async () => { beforeAll(async () => {
[browser, page, utils, cxgActions] = await setupTestBrowser(browserViewport); const browserViewport = { width: 1280, height: 960 };
[browser, page, utils, cxgActions] = await setupTestBrowser(browserViewport);
}); });
beforeEach(async () => { beforeEach(async () => {
@@ -20,13 +20,13 @@ beforeEach(async () => {
}); });
afterAll(() => { afterAll(() => {
browser.close(); if (browser !== undefined) browser.close();
}); });
describe("did launch", () => { describe("did launch", () => {
test("page launched", async () => { test("page launched", async () => {
let el = await utils.getOneElementInnerHTML("[data-testid='header']"); const element = await utils.getOneElementInnerHTML("[data-testid='header']");
expect(el).toBe(data.title); expect(element).toBe(data.title);
}); });
}); });
@@ -34,9 +34,7 @@ describe("metadata loads", () => {
test("categories and values from dataset appear", async () => { test("categories and values from dataset appear", async () => {
for (const label in data.categorical) { for (const label in data.categorical) {
await utils.waitByID(`category-${label}`); await utils.waitByID(`category-${label}`);
const categoryName = await utils.getOneElementInnerText( const categoryName = await utils.getOneElementInnerText(`[data-testid="category-${label}"]`);
`[data-testid="category-${label}"]`
);
expect(categoryName).toMatch(label); expect(categoryName).toMatch(label);
await utils.clickOn(`${label}:category-expand`); await utils.clickOn(`${label}:category-expand`);
const categories = await cxgActions.getAllCategoriesAndCounts(label); 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-expand`);
await utils.clickOn(`${cellset.metadata}:category-select`); await utils.clickOn(`${cellset.metadata}:category-select`);
for (const val of cellset.values) { for (const val of cellset.values) {
await utils.clickOn( await utils.clickOn(`categorical-value-select-${cellset.metadata}-${val}`);
`categorical-value-select-${cellset.metadata}-${val}`
);
} }
const cellCount = await cxgActions.cellSet(1); const cellCount = await cxgActions.cellSet(1);
expect(cellCount).toBe(cellset.count); expect(cellCount).toBe(cellset.count);
@@ -108,20 +104,12 @@ describe("cell selection", () => {
}); });
describe("gene entry", () => { describe("gene entry", () => {
test("search for single gene", async () => { test("search for single gene", async () => cxgActions.addGeneToSearch(data.genes.search));
await cxgActions.addGeneToSearch(data.genes.search);
});
test("bulk add genes", async () => { test("bulk add genes", async () => {
const testGenes = data.genes.bulkadd; const testGenes = data.genes.bulkadd;
await utils.clickOn("section-bulk-add"); await cxgActions.bulkAddGenes(testGenes);
await utils.typeInto("input-bulk-add", testGenes.join(",")); const allHistograms = await cxgActions.getAllHistograms("histogram-user-gene", testGenes);
await page.keyboard.press("Enter");
const allHistograms = await cxgActions.getAllHistograms(
"histogram-user-gene",
testGenes
);
expect(allHistograms).toEqual(expect.arrayContaining(testGenes)); expect(allHistograms).toEqual(expect.arrayContaining(testGenes));
expect(allHistograms.length).toEqual(testGenes.length); 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 () => { 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"]; const diffExpGenes = data.diffexp["gene-results"];
for (const userDefinedGene of userDefinedGenes) { await cxgActions.bulkAddGenes(userDefinedGenes);
await cxgActions.addGeneToSearch(userDefinedGene);
}
const userDefinedHistograms = await cxgActions.getAllHistograms("histogram-user-gene", 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.subset({x1: 0.15, y1: 0.10, x2: 0.98, y2: 0.98});
await cxgActions.runDiffExp(data.diffexp.cellset1, data.diffexp.cellset2); await cxgActions.runDiffExp(data.diffexp.cellset1, data.diffexp.cellset2);
const diffExpHistograms = await cxgActions.getAllHistograms("histogram-diffexp", diffExpGenes); const diffExpHistograms = await cxgActions.getAllHistograms("histogram-diffexp", diffExpGenes);
expect(diffExpHistograms).toStrictEqual(diffExpGenes); expect(diffExpHistograms).toEqual(expect.arrayContaining(diffExpGenes));
await utils.clickOn("reset-subset-button"); await utils.clickOn("reset-subset-button");
const expected = [].concat(userDefinedGenes, diffExpGenes); const expected = [].concat(userDefinedGenes, diffExpGenes);
const userDefinedHistogramsAfterSubset = await cxgActions.getAllHistograms( const userDefinedHistogramsAfterSubset = await cxgActions.getAllHistograms(
"histogram-user-gene", "histogram-user-gene",
expected 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 () => { 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"]; const diffExpGenes = data.diffexp["gene-results"];
for (const userDefinedGene of userDefinedGenes) { await cxgActions.bulkAddGenes(userDefinedGenes);
await cxgActions.addGeneToSearch(userDefinedGene);
}
const userDefinedHistograms = await cxgActions.getAllHistograms("histogram-user-gene", 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.subset({x1: 0.15, y1: 0.10, x2: 0.98, y2: 0.98});
await cxgActions.runDiffExp(data.diffexp.cellset1, data.diffexp.cellset2); await cxgActions.runDiffExp(data.diffexp.cellset1, data.diffexp.cellset2);
const diffExpHistograms = await cxgActions.getAllHistograms("histogram-diffexp", diffExpGenes); 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}); await cxgActions.subset({x1: 0.16, y1: 0.11, x2: 0.97, y2: 0.97});
const expected = [].concat(userDefinedGenes, diffExpGenes); const expected = [].concat(userDefinedGenes, diffExpGenes);
const userDefinedHistogramsAfterSubset = await cxgActions.getAllHistograms( const userDefinedHistogramsAfterSubset = await cxgActions.getAllHistograms(
"histogram-user-gene", "histogram-user-gene",
expected expected
); );
expect(userDefinedHistogramsAfterSubset).toStrictEqual(expected); expect(userDefinedHistogramsAfterSubset).toEqual(expect.arrayContaining(expected));
}); });
}); });
describe("scatter plot", () => { describe("scatter plot", () => {
test("scatter plot appears", async () => { test("scatter plot appears", async () => {
const testGenes = data.scatter.genes; await cxgActions.bulkAddGenes(Object.values(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 utils.clickOn(`plot-x-${data.scatter.genes.x}`); await utils.clickOn(`plot-x-${data.scatter.genes.x}`);
await utils.clickOn(`plot-y-${data.scatter.genes.y}`); await utils.clickOn(`plot-y-${data.scatter.genes.y}`);
await utils.waitByID("scatterplot"); await utils.waitByID("scatterplot");
@@ -297,6 +278,6 @@ describe("ui elements don't error", () => {
panCoords.end, panCoords.end,
false 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 Tests included in this file are specific to annotation features
*/ */
import {appUrlBase, DEBUG, DEV, DATASET} from "./config"; import { appUrlBase, DATASET } from "./config";
import {setupTestBrowser} from "./puppeteerUtils"; import { setupTestBrowser } from "./testBrowser";
import {datasets} from "./data"; import { datasets } from "./data";
let browser, page, utils, cxgActions; let browser, page, utils, actions;
const browserViewport = {width: 1280, height: 960};
const data = datasets[DATASET]; const data = datasets[DATASET];
beforeAll(async () => { beforeAll(async () => {
[browser, page, utils, cxgActions] = await setupTestBrowser(browserViewport); const browserViewport = {width: 1280, height: 960};
[browser, page, utils, actions] = await setupTestBrowser(browserViewport);
}); });
afterAll(() => { afterAll(() => {
if (!DEBUG) browser.close(); if (browser !== undefined) browser.close()
}); });
describe.each([ describe.each([
@@ -30,9 +30,9 @@ describe.each([
// wait for the page to load // wait for the page to load
await utils.waitByClass("autosave-complete"); await utils.waitByClass("autosave-complete");
// setup the test fixtures // setup the test fixtures
await cxgActions.createCategory(perTestCategoryName); await actions.createCategory(perTestCategoryName);
await cxgActions.createLabel(perTestCategoryName, perTestLabelName); await actions.createLabel(perTestCategoryName, perTestLabelName);
if (config.withSubset) await cxgActions.subset({x1: 0.10, y1: 0.10, x2: 0.80, y2: 0.80}); if (config.withSubset) await actions.subset({x1: 0.10, y1: 0.10, x2: 0.80, y2: 0.80});
await utils.waitByClass("autosave-complete"); await utils.waitByClass("autosave-complete");
}); });
@@ -44,18 +44,18 @@ describe.each([
test("create a category", async () => { test("create a category", async () => {
const categoryName = `category-created-${config.tag}`; const categoryName = `category-created-${config.tag}`;
await assertCategoryDoesNotExist(categoryName); await assertCategoryDoesNotExist(categoryName);
await cxgActions.createCategory(categoryName); await actions.createCategory(categoryName);
await assertCategoryExists(categoryName); await assertCategoryExists(categoryName);
}); });
test("delete a category", async () => { test("delete a category", async () => {
await cxgActions.deleteCategory(perTestCategoryName); await actions.deleteCategory(perTestCategoryName);
await assertCategoryDoesNotExist(perTestCategoryName); await assertCategoryDoesNotExist(perTestCategoryName);
}); });
test("rename a category", async () => { test("rename a category", async () => {
const newCategoryName = `cluster-for-real-${config.tag}`; const newCategoryName = `cluster-for-real-${config.tag}`;
await cxgActions.renameCategory(perTestCategoryName, newCategoryName); await actions.renameCategory(perTestCategoryName, newCategoryName);
await assertCategoryDoesNotExist(perTestCategoryName); await assertCategoryDoesNotExist(perTestCategoryName);
await assertCategoryExists(newCategoryName); await assertCategoryExists(newCategoryName);
}); });
@@ -63,19 +63,19 @@ describe.each([
test("create a label", async () => { test("create a label", async () => {
const labelName = `new-label-${config.tag}`; const labelName = `new-label-${config.tag}`;
await assertLabelDoesNotExist(perTestCategoryName, labelName); await assertLabelDoesNotExist(perTestCategoryName, labelName);
await cxgActions.createLabel(perTestCategoryName, labelName); await actions.createLabel(perTestCategoryName, labelName);
await assertLabelExists(perTestCategoryName, labelName); await assertLabelExists(perTestCategoryName, labelName);
}); });
test("delete a label", async () => { test("delete a label", async () => {
await cxgActions.deleteLabel(perTestCategoryName, perTestLabelName); await actions.deleteLabel(perTestCategoryName, perTestLabelName);
await assertLabelDoesNotExist(perTestCategoryName, perTestLabelName); await assertLabelDoesNotExist(perTestCategoryName, perTestLabelName);
}); });
test("rename a label", async () => { test("rename a label", async () => {
const newLabelName = "my-cool-new-label"; const newLabelName = "my-cool-new-label";
await assertLabelDoesNotExist(perTestCategoryName, newLabelName); await assertLabelDoesNotExist(perTestCategoryName, newLabelName);
await cxgActions.renameLabel(perTestCategoryName, perTestLabelName, newLabelName); await actions.renameLabel(perTestCategoryName, perTestLabelName, newLabelName);
await assertLabelDoesNotExist(perTestCategoryName, perTestLabelName); await assertLabelDoesNotExist(perTestCategoryName, perTestLabelName);
await assertLabelExists(perTestCategoryName, newLabelName); await assertLabelExists(perTestCategoryName, newLabelName);
}); });
@@ -83,7 +83,7 @@ describe.each([
test("check cell count for a label loaded from file", async () => { test("check cell count for a label loaded from file", async () => {
const categoryName = "cluster-test"; const categoryName = "cluster-test";
const labelName = "four"; const labelName = "four";
await cxgActions.expandCategory(categoryName); await actions.expandCategory(categoryName);
const result = await utils.waitByID(`categorical-value-count-${categoryName}-${labelName}`); const result = await utils.waitByID(`categorical-value-count-${categoryName}-${labelName}`);
expect(await result.evaluate(node => node.innerText)).toBe( expect(await result.evaluate(node => node.innerText)).toBe(
data.annotationsFromFile.count.bySubsetConfig[config.withSubset] data.annotationsFromFile.count.bySubsetConfig[config.withSubset]
@@ -91,12 +91,12 @@ describe.each([
}); });
test("assign cells to a label", async () => { test("assign cells to a label", async () => {
await cxgActions.expandCategory(perTestCategoryName); await actions.expandCategory(perTestCategoryName);
const lassoSelection = await cxgActions.calcDragCoordinates( const lassoSelection = await actions.calcDragCoordinates(
"layout-graph", "layout-graph",
data.categoryLabel.lasso["coordinates-as-percent"] data.categoryLabel.lasso["coordinates-as-percent"]
); );
await cxgActions.drag( await actions.drag(
"layout-graph", "layout-graph",
lassoSelection.start, lassoSelection.start,
lassoSelection.end, lassoSelection.end,
@@ -114,7 +114,7 @@ describe.each([
test("undo/redo category creation", async () => { test("undo/redo category creation", async () => {
const categoryName = `category-created-undo-${config.tag}`; const categoryName = `category-created-undo-${config.tag}`;
await assertCategoryDoesNotExist(categoryName); await assertCategoryDoesNotExist(categoryName);
await cxgActions.createCategory(categoryName); await actions.createCategory(categoryName);
await assertCategoryExists(categoryName); await assertCategoryExists(categoryName);
await utils.clickOn("undo"); await utils.clickOn("undo");
await assertCategoryDoesNotExist(categoryName); await assertCategoryDoesNotExist(categoryName);
@@ -124,9 +124,9 @@ describe.each([
test("undo/redo category deletion", async () => { test("undo/redo category deletion", async () => {
const categoryName = `category-deleted-undo-${config.tag}`; const categoryName = `category-deleted-undo-${config.tag}`;
await cxgActions.createCategory(categoryName); await actions.createCategory(categoryName);
await assertCategoryExists(categoryName); await assertCategoryExists(categoryName);
await cxgActions.deleteCategory(categoryName); await actions.deleteCategory(categoryName);
await assertCategoryDoesNotExist(categoryName); await assertCategoryDoesNotExist(categoryName);
await utils.clickOn("undo"); await utils.clickOn("undo");
await assertCategoryExists(categoryName); await assertCategoryExists(categoryName);
@@ -137,7 +137,7 @@ describe.each([
test("undo/redo category rename", async () => { test("undo/redo category rename", async () => {
const newCategoryName = `category-renamed-undo-${config.tag}`; const newCategoryName = `category-renamed-undo-${config.tag}`;
await assertCategoryDoesNotExist(newCategoryName); await assertCategoryDoesNotExist(newCategoryName);
await cxgActions.renameCategory(perTestCategoryName, newCategoryName); await actions.renameCategory(perTestCategoryName, newCategoryName);
await assertCategoryExists(newCategoryName); await assertCategoryExists(newCategoryName);
await assertCategoryDoesNotExist(perTestCategoryName); await assertCategoryDoesNotExist(perTestCategoryName);
await utils.clickOn("undo"); await utils.clickOn("undo");
@@ -151,7 +151,7 @@ describe.each([
test("undo/redo label creation", async () => { test("undo/redo label creation", async () => {
const labelName = `label-created-undo-${config.tag}`; const labelName = `label-created-undo-${config.tag}`;
await assertLabelDoesNotExist(perTestCategoryName, labelName); await assertLabelDoesNotExist(perTestCategoryName, labelName);
await cxgActions.createLabel(perTestCategoryName, labelName); await actions.createLabel(perTestCategoryName, labelName);
await assertLabelExists(perTestCategoryName, labelName); await assertLabelExists(perTestCategoryName, labelName);
await utils.clickOn("undo"); await utils.clickOn("undo");
await assertLabelDoesNotExist(perTestCategoryName); await assertLabelDoesNotExist(perTestCategoryName);
@@ -160,7 +160,7 @@ describe.each([
}); });
test("undo/redo label deletion", async () => { test("undo/redo label deletion", async () => {
await cxgActions.deleteLabel(perTestCategoryName, perTestLabelName); await actions.deleteLabel(perTestCategoryName, perTestLabelName);
await assertLabelDoesNotExist(perTestCategoryName); await assertLabelDoesNotExist(perTestCategoryName);
await utils.clickOn("undo"); await utils.clickOn("undo");
await assertLabelExists(perTestCategoryName, perTestLabelName); await assertLabelExists(perTestCategoryName, perTestLabelName);
@@ -171,7 +171,7 @@ describe.each([
test("undo/redo label rename", async () => { test("undo/redo label rename", async () => {
const newLabelName = `label-renamed-undo-${config.tag}`; const newLabelName = `label-renamed-undo-${config.tag}`;
await assertLabelDoesNotExist(perTestCategoryName, newLabelName); await assertLabelDoesNotExist(perTestCategoryName, newLabelName);
await cxgActions.renameLabel(perTestCategoryName, perTestLabelName, newLabelName); await actions.renameLabel(perTestCategoryName, perTestLabelName, newLabelName);
await assertLabelExists(perTestCategoryName, newLabelName); await assertLabelExists(perTestCategoryName, newLabelName);
await assertLabelDoesNotExist(perTestCategoryName, perTestLabelName); await assertLabelDoesNotExist(perTestCategoryName, perTestLabelName);
await utils.clickOn("undo"); await utils.clickOn("undo");
@@ -195,13 +195,13 @@ describe.each([
async function assertLabelExists(categoryName, labelName) { async function assertLabelExists(categoryName, labelName) {
const category = await utils.waitByID(`${categoryName}:category-expand`); const category = await utils.waitByID(`${categoryName}:category-expand`);
expect(category).not.toBeNull(); expect(category).not.toBeNull();
await cxgActions.expandCategory(categoryName); await actions.expandCategory(categoryName);
const previous = await utils.waitByID(`categorical-value-${categoryName}-${labelName}`); const previous = await utils.waitByID(`categorical-value-${categoryName}-${labelName}`);
expect(await previous.evaluate(node => node.innerText)).toBe(labelName); expect(await previous.evaluate(node => node.innerText)).toBe(labelName);
} }
async function assertLabelDoesNotExist(categoryName, labelName) { async function assertLabelDoesNotExist(categoryName, labelName) {
await cxgActions.expandCategory(categoryName); await actions.expandCategory(categoryName);
const result = await page.$(`[data-testid='categorical-value-${categoryName}-${labelName}']`); const result = await page.$(`[data-testid='categorical-value-${categoryName}-${labelName}']`);
expect(result).toBeNull(); expect(result).toBeNull();
} }
@@ -212,9 +212,8 @@ describe.each([
`[data-testid='${categoryName}:category-expand']`, `[data-testid='${categoryName}:category-expand']`,
{timeout: 200} {timeout: 200}
); );
if (category !== null) return await cxgActions.deleteCategory(categoryName); if (category !== null) return await actions.deleteCategory(categoryName);
} catch (error) { } catch {}
}
return null return null
} }
}); });
+32 -300
View File
@@ -1,332 +1,64 @@
import {DEBUG, DEV} from "./config"; export const puppeteerUtils = page => ({
import puppeteer from "puppeteer";
import { strict as assert } from "assert";
export const puppeteerUtils = puppeteerPage => ({ async waitByID(testId, props = {}) {
async waitByID(testid, props = {}) { return page.waitForSelector(`[data-testid='${testId}']`, props);
return await puppeteerPage.waitForSelector(
`[data-testid='${testid}']`,
props
);
}, },
async waitByClass(testclass, props = {}) { async waitByClass(testClass, props = {}) {
return await puppeteerPage.waitForSelector( return page.waitForSelector(`[data-testclass='${testClass}']`, props);
`[data-testclass='${testclass}']`,
props
);
}, },
async waitForAllByIds(testids) { async waitForAllByIds(testIds) {
await Promise.all( await Promise.all(
testids.map(testid => testIds.map(testId => page.waitForSelector(`[data-testid='${testId}']`))
puppeteerPage.waitForSelector(`[data-testid='${testid}']`)
)
); );
}, },
async getAllByClass(testclass) { async getAllByClass(testClass) {
const elements = await puppeteerPage.$$eval( return page.$$eval(
`[data-testclass=${testclass}]`, `[data-testclass=${testClass}]`,
eles => eles.map(ele => ele.dataset.testid) 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 // blueprint's typeahead is treating typing weird, clicking & waiting first solves this
// only works for text without special characters // only works for text without special characters
await this.waitByID(testid); await this.waitByID(testId);
const selector = `[data-testid='${testid}']`; const selector = `[data-testid='${testId}']`;
// type ahead can be annoying if you don't pause before you type // type ahead can be annoying if you don't pause before you type
await puppeteerPage.click(selector); await page.click(selector);
await puppeteerPage.waitFor(200); await page.waitFor(200);
await puppeteerPage.type(selector, text); await page.type(selector, text);
}, },
async clearInputAndTypeInto(testid, text) { async clearInputAndTypeInto(testId, text) {
await this.waitByID(testid); await this.waitByID(testId);
const selector = `[data-testid='${testid}']`; const selector = `[data-testid='${testId}']`;
// only works for text without special characters // only works for text without special characters
// type ahead can be annoying if you don't pause before you type // type ahead can be annoying if you don't pause before you type
await puppeteerPage.click(selector); await page.click(selector);
await puppeteerPage.waitFor(200); await page.waitFor(200);
// select all // select all
await page.click(selector, { clickCount: 3 });
await puppeteerPage.click(selector, { clickCount: 3 }); await page.keyboard.press("Backspace");
await puppeteerPage.keyboard.press("Backspace"); await page.type(selector, text);
await puppeteerPage.type(selector, text);
}, },
async clickOn(testid, options={}) { async clickOn(testId, options={}) {
await this.waitByID(testid); await this.waitByID(testId);
await puppeteerPage.click(`[data-testid='${testid}']`, options); await page.click(`[data-testid='${testId}']`, options);
await puppeteerPage.waitFor(50); await page.waitFor(50);
}, },
async getOneElementInnerHTML(selector) { async getOneElementInnerHTML(selector) {
await puppeteerPage.waitForSelector(selector); await page.waitForSelector(selector);
let text = await puppeteerPage.$eval(selector, el => el.innerHTML); return page.$eval(selector, el => el.innerHTML);
return text;
}, },
async getOneElementInnerText(selector) { async getOneElementInnerText(selector) {
await puppeteerPage.waitForSelector(selector); await page.waitForSelector(selector);
let text = await puppeteerPage.$eval(selector, el => el.innerText); return page.$eval(selector, el => el.innerText);
return text;
} }
}); });
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];
}