mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-22 02:18:11 +08:00
Make smoke tests faster, more stable (#1195)
* Refactor smoke tests & utils for conciseness/style * Modularize test utilities
This commit is contained in:
@@ -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");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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);");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user