Better smoke tests (#669)

This commit is contained in:
Charlotte Weaver
2019-03-28 16:47:42 -07:00
committed by GitHub
parent 096d477893
commit 5d4b11d287
15 changed files with 939 additions and 206 deletions
+6
View File
@@ -0,0 +1,6 @@
export const jest_env = process.env.JEST_ENV || "dev";
export const appPort = process.env.JEST_CXG_PORT || 3000;
export const appUrlBase = `http://localhost:${appPort}`;
export const DEV = jest_env === "dev";
export const DEBUG = jest_env === "debug";
export const DATASET = "pbmc3k";
+105
View File
@@ -0,0 +1,105 @@
export const datasets = {
pbmc3k: {
title: "cellxgene: pbmc3k",
dataframe: {
nObs: "2638",
nVar: "1838",
type: "float32"
},
categorical: {
louvain: {
"B cells": "342",
"CD14+ Monocytes": "480",
"CD4 T cells": "1144",
"CD8 T cells": "316",
"Dendritic cells": "37",
"FCGR3A+ Monocytes": "150",
Megakaryocytes: "15",
"NK cells": "154"
}
},
continuous: {
n_genes: "int32",
percent_mito: "float32",
n_counts: "float32"
},
cellsets: {
lasso: [
{
"coordinates-as-percent": { x1: 0.25, y1: 0.25, x2: 0.35, y2: 0.35 },
count: "26"
}
],
categorical: [
{
metadata: "louvain",
values: ["B cells", "Megakaryocytes"],
count: "357"
}
],
continuous: [
{
metadata: "n_genes",
"coordinates-as-percent": { x1: 0.25, y1: 0.5, x2: 0.55, y2: 0.5 },
count: "1537"
}
]
},
diffexp: {
cellset1: [
{ kind: "categorical", metadata: "louvain", values: ["B cells"] }
],
cellset2: [
{
kind: "categorical",
metadata: "louvain",
values: ["CD4 T cells", "NK cells"]
}
],
"gene-results": [
"HLA-DRB1",
"HLA-DPB1",
"CD79A",
"HLA-DPA1",
"HLA-DQA1",
"CD79B",
"HLA-DQB1",
"MS4A1",
"IL32",
"CD37"
]
},
genes: {
bulkadd: ["S100A8", "FCGR3A", "LGALS2", "GSTP1"],
search: "ACD"
},
subset: {
cellset1: [
{
kind: "categorical",
metadata: "louvain",
values: ["B cells", "Megakaryocytes"]
}
],
count: "357",
categorical: {
louvain: {
"B cells": "342",
Megakaryocytes: "15"
}
},
lasso: {
"coordinates-as-percent": { x1: 0.45, y1: 0.45, x2: 0.5, y2: 0.5 },
count: "67"
}
},
scatter: {
genes: { x: "S100A8", y: "FCGR3A" }
},
pan: {
"coordinates-as-percent": { x1: 0.75, y1: 0.75, x2: 0.35, y2: 0.35 }
}
}
};
+247 -154
View File
@@ -1,28 +1,36 @@
import puppeteer from "puppeteer";
import { appUrlBase, DEBUG, DEV, DATASET } from "./config";
import { puppeteerUtils, cellxgeneActions } from "./puppeteerUtils";
import { datasets } from "./data";
const jest_env = process.env.JEST_ENV || "dev";
const appPort = process.env.JEST_CXG_PORT || 3000;
const appUrlBase = `http://localhost:${appPort}`;
const DEV = jest_env === "dev";
const DEBUG = jest_env === "debug";
let browser;
let page;
let browser, page, utils, cxgActions, spy;
const browserViewport = { width: 1280, height: 960 };
let data = datasets[DATASET];
if (DEBUG) jest.setTimeout(100000);
if (DEV) jest.setTimeout(10000);
beforeAll(async () => {
const browser_params = DEV
? { headless: false, slowMo: 50 }
const browserParams = DEV
? { headless: false, slowMo: 5 }
: DEBUG
? { headless: false, slowMo: 200, devtools: true }
? { headless: false, slowMo: 100, devtools: true }
: {};
browser = await puppeteer.launch(browser_params);
browser = await puppeteer.launch(browserParams);
page = await browser.newPage();
page.setViewport(browserViewport);
if (DEV || DEBUG)
page.on("console", msg => console.log("PAGE LOG:", msg.text()));
await page.setViewport(browserViewport);
if (DEV || DEBUG) {
page.on("console", msg => console.log(`PAGE LOG: ${msg.text()}`));
}
page.on("pageerror", err => {
throw new Error(`Console error: ${err}`);
});
utils = puppeteerUtils(page);
cxgActions = cellxgeneActions(page);
});
beforeEach(async () => {
await page.goto(appUrlBase);
});
afterAll(() => {
@@ -31,161 +39,246 @@ afterAll(() => {
}
});
const getOneElementInnerHTML = async function(selector) {
let text = await page.$eval(selector, el => el.innerHTML);
return text;
};
const drag = async function(el_box, start, end, lasso = false) {
const x1 = el_box.content[0].x + start.x;
const x2 = el_box.content[0].x + end.x;
const y1 = el_box.content[0].y + start.y;
const y2 = el_box.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();
};
describe("did launch", () => {
describe("did launch", async () => {
test("page launched", async () => {
await page.goto(appUrlBase);
let el = await getOneElementInnerHTML("[data-testid='header']");
expect(el).toBe("cellxgene: pbmc3k");
let el = await utils.getOneElementInnerHTML("[data-testid='header']");
expect(el).toBe(data.title);
});
});
describe("search for genes", () => {
test("search for known gene and add to metadata", async () => {
await page.goto(appUrlBase);
await page.waitForSelector("[ data-testid='gene-search']");
// blueprint's typeahead is treating typing weird, clicking & waiting first solves this
await page.click("[data-testid='gene-search']");
await page.waitFor(200);
await page.type("[data-testid='gene-search']", "ACD");
await page.keyboard.press("Enter");
await page.waitForSelector("[data-testid='histogram-ACD']");
describe("metadata loads", async () => {
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}"]`
);
expect(categoryName).toMatch(label);
await utils.clickOn(`category-expand-${label}`);
const categories = await cxgActions.getAllCategoriesAndCounts(label);
expect(Object.keys(categories)).toMatchObject(
Object.keys(data.categorical[label])
);
expect(Object.values(categories)).toMatchObject(
Object.values(data.categorical[label])
);
}
});
test("continuous data appears", async () => {
for (const label in data.continuous) {
await utils.waitByID(`histogram-${label}`);
}
});
});
describe("select cells and diffexp", () => {
test("selects cells from layout and adds to cell set 1", async () => {
await page.goto(appUrlBase);
const layout = await page.waitForSelector("[data-testid='layout-graph']");
const size = await layout.boxModel();
const cellset1 = {
start: {
x: Math.floor(size.width * 0.25),
y: Math.floor(size.height * 0.25)
},
end: {
x: Math.floor(size.width * 0.35),
y: Math.floor(size.height * 0.35)
}
};
await drag(size, cellset1.start, cellset1.end, true);
await page.click("[data-testid='cellset-button-1");
let button = await getOneElementInnerHTML("[data-testid='cellset-button-1");
expect(button).toMatch(/26 cells/);
describe("cell selection", async () => {
test("selects all cells cellset 1", async () => {
const cellCount = await cxgActions.cellSet(1);
expect(cellCount).toBe(data.dataframe.nObs);
});
test("selects cells from layout and adds to cell set 2", async () => {
await page.goto(appUrlBase);
const layout = await page.waitForSelector("[data-testid='layout-graph']");
const size = await layout.boxModel();
const cellset2 = {
start: {
x: Math.floor(size.width * 0.45),
y: Math.floor(size.height * 0.45)
},
end: {
x: Math.floor(size.width * 0.55),
y: Math.floor(size.height * 0.55)
}
};
await drag(size, cellset2.start, cellset2.end, true);
await page.click("[data-testid='cellset-button-2");
let button = await getOneElementInnerHTML("[data-testid='cellset-button-2");
expect(button).toMatch(/49 cells/);
test("selects all cells cellset 2", async () => {
const cellCount = await cxgActions.cellSet(2);
expect(cellCount).toBe(data.dataframe.nObs);
});
test("selects cells, saves them and performs diffexp", async () => {
await page.goto(appUrlBase);
const layout = await page.waitForSelector("[data-testid='layout-graph']");
const size = await layout.boxModel();
const cellset1 = {
start: {
x: Math.floor(size.width * 0.25),
y: Math.floor(size.height * 0.25)
},
end: {
x: Math.floor(size.width * 0.35),
y: Math.floor(size.height * 0.35)
}
};
await drag(size, cellset1.start, cellset1.end, true);
await page.click("[data-testid='cellset-button-1");
const cellset2 = {
start: {
x: Math.floor(size.width * 0.45),
y: Math.floor(size.height * 0.45)
},
end: {
x: Math.floor(size.width * 0.55),
y: Math.floor(size.height * 0.55)
}
};
await drag(size, cellset2.start, cellset2.end, true);
await page.click("[data-testid='cellset-button-2");
await page.click("[data-testid='diffexp-button");
await page.waitForSelector("[data-testclass='histogram-diffexp']");
const diffexps = await page.$$eval(
"[data-testclass='histogram-diffexp']",
divs => {
return divs.map(div =>
div.id.substring("histogram-".length, div.id.length)
test("selects cells via lasso", async () => {
for (const cellset of data.cellsets.lasso) {
const cellset1 = await cxgActions.calcDragCoordinates(
"layout-graph",
cellset["coordinates-as-percent"]
);
await cxgActions.drag("layout-graph", cellset1.start, cellset1.end, true);
const cellCount = await cxgActions.cellSet(1);
expect(cellCount).toBe(cellset.count);
}
});
test("selects cells via categorical", async () => {
for (const cellset of data.cellsets.categorical) {
await utils.clickOn(`category-expand-${cellset.metadata}`);
await utils.clickOn(`category-select-${cellset.metadata}`);
for (const val of cellset.values) {
await utils.clickOn(
`categorical-value-select-${cellset.metadata}-${val}`
);
}
);
expect(diffexps).toMatchObject([
"HLA-DPA1",
"HLA-DQA1",
"HLA-DRB1",
"HLA-DMA",
"CST3",
"HLA-DPB1",
"HLA-DQB1",
"LGALS2",
"FCER1A",
"LTB"
]);
const cellCount = await cxgActions.cellSet(1);
expect(cellCount).toBe(cellset.count);
}
});
test("selects cells via continuous", async () => {
for (const cellset of data.cellsets.continuous) {
const histId = `histogram-${cellset.metadata}-plot-brush`;
const coords = await cxgActions.calcDragCoordinates(
histId,
cellset["coordinates-as-percent"]
);
await cxgActions.drag(histId, coords.start, coords.end);
const cellCount = await cxgActions.cellSet(1);
expect(cellCount).toBe(cellset.count);
}
});
});
describe("brushable histogram", () => {
test("can brush historgram", async () => {
await page.goto(appUrlBase);
const hist = await page.waitForSelector(
"[data-testid='histogram_n_genes_svg-brush'] > .overlay"
describe("gene entry", async () => {
test("search for single gene", async () => {
// blueprint's typeahead is treating typing weird, clicking & waiting first solves this
await utils.typeInto("gene-search", data.genes.search);
await page.keyboard.press("Enter");
await page.waitForSelector(
`[data-testid='histogram-${data.genes.search}']`
);
const hist_size = await hist.boxModel();
const draghist = {
start: {
x: Math.floor(hist_size.width * 0.25),
y: Math.floor(hist_size.height * 0.5)
},
end: {
x: Math.floor(hist_size.width * 0.55),
y: Math.floor(hist_size.height * 0.5)
}
};
await drag(hist_size, draghist.start, draghist.end);
});
test("bulk add genes", async () => {
await cxgActions.reset();
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 userGeneHist = await cxgActions.getAllHistograms(
"histogram-user-gene"
);
expect(userGeneHist).toEqual(expect.arrayContaining(testGenes));
});
});
describe("diffexp", async () => {
test("selects cells, saves them and performs diffexp", async () => {
for (const select of data.diffexp.cellset1) {
if (select.kind === "categorical") {
await cxgActions.selectCategory(select.metadata, select.values, true);
}
}
await cxgActions.cellSet(1);
for (const select of data.diffexp.cellset2) {
if (select.kind === "categorical") {
await cxgActions.selectCategory(select.metadata, select.values, true);
}
}
await cxgActions.cellSet(2);
await utils.clickOn("diffexp-button");
const diffExpHists = await cxgActions.getAllHistograms("histogram-diffexp");
expect(diffExpHists).toEqual(
expect.arrayContaining(data.diffexp["gene-results"])
);
});
});
//
describe("subset/reset", async () => {
test("subset - cell count matches", async () => {
for (const select of data.subset.cellset1) {
if (select.kind === "categorical") {
await cxgActions.selectCategory(select.metadata, select.values, true);
}
}
await utils.clickOn("subset-button");
for (const label in data.subset.categorical) {
const categories = await cxgActions.getAllCategoriesAndCounts(label);
expect(Object.keys(categories)).toMatchObject(
Object.keys(data.subset.categorical[label])
);
expect(Object.values(categories)).toMatchObject(
Object.values(data.subset.categorical[label])
);
}
});
test("reset after subset", async () => {
for (const select of data.subset.cellset1) {
if (select.kind === "categorical") {
await cxgActions.selectCategory(select.metadata, select.values, true);
}
}
await utils.clickOn("subset-button");
for (const label in data.subset.categorical) {
const categories = await cxgActions.getAllCategoriesAndCounts(label);
expect(Object.keys(categories)).toMatchObject(
Object.keys(data.subset.categorical[label])
);
expect(Object.values(categories)).toMatchObject(
Object.values(data.subset.categorical[label])
);
}
await cxgActions.reset();
for (const label in data.categorical) {
await utils.waitByID(`category-${label}`);
const categoryName = await utils.getOneElementInnerText(
`[data-testid="category-${label}"]`
);
expect(categoryName).toMatch(label);
const categories = await cxgActions.getAllCategoriesAndCounts(label);
expect(Object.keys(categories)).toMatchObject(
Object.keys(data.categorical[label])
);
expect(Object.values(categories)).toMatchObject(
Object.values(data.categorical[label])
);
}
});
test("lasso after subset", async () => {
for (const select of data.subset.cellset1) {
if (select.kind === "categorical") {
await cxgActions.selectCategory(select.metadata, select.values, true);
}
}
await utils.clickOn("subset-button");
const lassoSelection = await cxgActions.calcDragCoordinates(
"layout-graph",
data.subset.lasso["coordinates-as-percent"]
);
await cxgActions.drag(
"layout-graph",
lassoSelection.start,
lassoSelection.end,
true
);
const cellCount = await cxgActions.cellSet(1);
expect(cellCount).toBe(data.subset.lasso.count);
});
});
describe("scatter plot", async () => {
test("scatter plot appears", async () => {
await cxgActions.reset();
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 utils.clickOn(`plot-x-${data.scatter.genes.x}`);
await utils.clickOn(`plot-y-${data.scatter.genes.y}`);
await utils.waitByID("scatterplot");
});
});
// interact with UI elements just that they do not break
describe("ui elements don't error", async () => {
test("color by", async () => {
for (const label in data.categorical) {
await utils.clickOn(`colorby-${label}`);
}
for (const label in data.continuous) {
await utils.clickOn(`colorby-${label}`);
}
});
test("pan and zoom", async () => {
await utils.clickOn("mode-pan-zoom");
const panCoords = await cxgActions.calcDragCoordinates(
"layout-graph",
data.pan["coordinates-as-percent"]
);
await cxgActions.drag(
"layout-graph",
panCoords.start,
panCoords.end,
false
);
await page.evaluate(`window.scrollBy(0, 1000);`);
});
});
+10
View File
@@ -0,0 +1,10 @@
{
"preset": "jest-puppeteer",
"testMatch": [
"**/__tests__/**/?(*.)(spec|test).js?(x)"
],
"testURL": "http://localhost/",
"setupFiles": [
"../setupMissingGlobals.js"
]
}
+161
View File
@@ -0,0 +1,161 @@
export const puppeteerUtils = puppeteerPage => ({
async waitByID(testid) {
return await puppeteerPage.waitForSelector(`[data-testid='${testid}']`);
},
async waitByClass(testclass) {
return await puppeteerPage.waitForSelector(
`[data-testclass='${testclass}']`
);
},
async typeInto(testid, text) {
// only works for text without special characters
await this.waitByID(testid);
// type ahead can be annoying if you don't pause before you type
await puppeteerPage.click(`[data-testid='${testid}']`);
await puppeteerPage.waitFor(200);
await puppeteerPage.type(`[data-testid='${testid}']`, text);
},
async clickOn(testid) {
await this.waitByID(testid);
await puppeteerPage.click(`[data-testid='${testid}']`);
await puppeteerPage.waitFor(50);
},
async getOneElementInnerHTML(selector) {
let text = await puppeteerPage.$eval(selector, el => el.innerHTML);
return text;
},
async getOneElementInnerText(selector) {
let text = await puppeteerPage.$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 getAllHistograms(testclass) {
await puppeteerUtils(puppeteerPage).waitByClass(testclass);
const histograms = await puppeteerPage.$$eval(
`[data-testclass=${testclass}]`,
els => {
return els.map(el => {
return el.dataset.testid.substring(
"histogram_".length,
el.dataset.testid.length
);
});
}
);
return histograms;
},
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-select-${category}`;
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-expand-${category}`
);
const isExpanded = await categoryRow.$(
"[data-testclass='category-expand-is-expanded']"
);
if (isExpanded) {
await puppeteerUtils(puppeteerPage).clickOn(
`category-expand-${category}`
);
}
} catch {}
},
async calcDragCoordinates(testid, coordinateAsPercent) {
const el = await puppeteerUtils(puppeteerPage).waitByID(testid);
const size = await el.boxModel();
const coords = {
start: {
x: Math.floor(size.width * coordinateAsPercent.x1),
y: Math.floor(size.height * coordinateAsPercent.y1)
},
end: {
x: Math.floor(size.width * coordinateAsPercent.x2),
y: Math.floor(size.height * coordinateAsPercent.y2)
}
};
return coords;
},
async selectCategory(category, values, reset = true) {
if (reset) await this.resetCategory(category);
await puppeteerUtils(puppeteerPage).clickOn(`category-expand-${category}`);
await puppeteerUtils(puppeteerPage).clickOn(`category-select-${category}`);
for (const val of values) {
await puppeteerUtils(puppeteerPage).clickOn(
`categorical-value-select-${category}-${val}`
);
}
},
async reset() {
await puppeteerUtils(puppeteerPage).clickOn("reset");
// loading state never actually happens, reset is too fast
await page.waitFor(200);
}
});