mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-10-04 10:28:11 +08:00
Add smoke test for annotations features
This commit is contained in:
@@ -4,3 +4,6 @@ export const appUrlBase = `http://localhost:${appPort}`;
|
||||
export const DEV = jest_env === "dev";
|
||||
export const DEBUG = jest_env === "debug";
|
||||
export const DATASET = "pbmc3k";
|
||||
|
||||
if (DEBUG) jest.setTimeout(100000);
|
||||
if (DEV) jest.setTimeout(10000);
|
||||
|
||||
@@ -115,6 +115,18 @@ export const datasets = {
|
||||
}
|
||||
}
|
||||
},
|
||||
categoryLabel: {
|
||||
lasso: {
|
||||
"coordinates-as-percent": { x1: 0.3, y1: 0.3, x2: 0.5, y2: 0.5 },
|
||||
count: "38"
|
||||
},
|
||||
newCount: {
|
||||
bySubsetConfig: {
|
||||
false: "199",
|
||||
true: "193"
|
||||
}
|
||||
}
|
||||
},
|
||||
clip: {
|
||||
min: "30",
|
||||
max: "70",
|
||||
|
||||
@@ -3,45 +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 puppeteer from "puppeteer";
|
||||
import { appUrlBase, DEBUG, DEV, DATASET } from "./config";
|
||||
import { puppeteerUtils, cellxgeneActions } from "./puppeteerUtils";
|
||||
import { appUrlBase, DEBUG, DATASET } from "./config";
|
||||
import { setupTestBrowser } from "./puppeteerUtils";
|
||||
import { datasets } from "./data";
|
||||
|
||||
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 browserParams = DEV
|
||||
? { headless: false, slowMo: 5 }
|
||||
: DEBUG
|
||||
? { headless: false, slowMo: 100, devtools: true }
|
||||
: {};
|
||||
browser = await puppeteer.launch(browserParams);
|
||||
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}`);
|
||||
});
|
||||
utils = puppeteerUtils(page);
|
||||
cxgActions = cellxgeneActions(page);
|
||||
[browser, page, utils, cxgActions] = await setupTestBrowser(browserViewport);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -69,7 +40,7 @@ describe("metadata loads", () => {
|
||||
`[data-testid="category-${label}"]`
|
||||
);
|
||||
expect(categoryName).toMatch(label);
|
||||
await utils.clickOn(`category-expand-${label}`);
|
||||
await utils.clickOn(`${label}:category-expand`);
|
||||
const categories = await cxgActions.getAllCategoriesAndCounts(label);
|
||||
expect(Object.keys(categories)).toMatchObject(
|
||||
Object.keys(data.categorical[label])
|
||||
@@ -112,8 +83,8 @@ describe("cell selection", () => {
|
||||
|
||||
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}`);
|
||||
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}`
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
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";
|
||||
|
||||
let browser, page, utils, cxgActions;
|
||||
const browserViewport = {width: 1280, height: 960};
|
||||
const data = datasets[DATASET];
|
||||
|
||||
beforeAll(async () => {
|
||||
[browser, page, utils, cxgActions] = await setupTestBrowser(browserViewport);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await page.goto(appUrlBase);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
if (!DEBUG) browser.close();
|
||||
});
|
||||
|
||||
describe("did launch", () => {
|
||||
test("page launched", async () => {
|
||||
let el = await utils.getOneElementInnerHTML("[data-testid='header']");
|
||||
expect(el).toBe(data.title);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe.each([
|
||||
{withSubset: true},
|
||||
{withSubset: false}
|
||||
])("annotations", (config) => {
|
||||
|
||||
async function subset() {
|
||||
const lassoSelection = await cxgActions.calcDragCoordinates(
|
||||
"layout-graph",
|
||||
{ x1: 0.10, y1: 0.10, x2: 0.80, y2: 0.80 }
|
||||
);
|
||||
await cxgActions.drag(
|
||||
"layout-graph",
|
||||
lassoSelection.start,
|
||||
lassoSelection.end,
|
||||
true
|
||||
);
|
||||
await utils.clickOn("subset-button");
|
||||
const coordinate = await cxgActions.calcCoordinate("layout-graph", 0.9, 0.9);
|
||||
await cxgActions.clickOnCoordinate("layout-graph", coordinate);
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
if (config.withSubset) await subset();
|
||||
});
|
||||
|
||||
test("create a category", async () => {
|
||||
await utils.clickOn("open-annotation-dialog");
|
||||
await utils.typeInto("new-category-name", "test-category-name");
|
||||
await utils.clickOn("submit-category");
|
||||
const result = await utils.waitByID("test-category-name:category-expand");
|
||||
expect(await result.evaluate(node => node.innerText)).toBe("test-category-name");
|
||||
});
|
||||
|
||||
test("delete a category", async () => {
|
||||
const previous = await utils.waitByID("cluster-test:category-expand");
|
||||
expect(await previous.evaluate(node => node.innerText)).toBe("cluster-test");
|
||||
await utils.hoverOn("cluster-test:see-actions");
|
||||
await utils.clickOn("cluster-test:delete-category");
|
||||
const result = await page.$("[data-testid='cluster-test:category-expand']");
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test("rename a category", async () => {
|
||||
const previous = await utils.waitByID("cluster-test:category-expand");
|
||||
expect(await previous.evaluate(node => node.innerText)).toBe("cluster-test");
|
||||
await utils.hoverOn("cluster-test:see-actions");
|
||||
await utils.clickOn("cluster-test:edit-category-mode");
|
||||
await utils.typeInto("cluster-test:edit-category-name-text", "-renamed");
|
||||
await utils.clickOn("cluster-test:submit-category-edit");
|
||||
const result = await utils.waitByID("cluster-test-renamed:category-expand");
|
||||
expect(await result.evaluate(node => node.innerText)).toBe("cluster-test-renamed");
|
||||
});
|
||||
|
||||
test("create a label", async () => {
|
||||
await utils.hoverOn("cluster-test:see-actions");
|
||||
await utils.clickOn("cluster-test:add-new-label-to-category");
|
||||
await utils.typeInto("cluster-test:new-label-name", "test-label-name");
|
||||
await utils.clickOn("cluster-test:submit-label");
|
||||
await cxgActions.expandCategory("cluster-test");
|
||||
const result = await utils.waitByID("categorical-value-cluster-test-test-label-name");
|
||||
expect(await result.evaluate(node => node.innerText)).toBe("test-label-name");
|
||||
});
|
||||
|
||||
test("delete a label", async () => {
|
||||
await cxgActions.expandCategory("cluster-test");
|
||||
const previous = await utils.waitByID("categorical-value-cluster-test-three");
|
||||
expect(await previous.evaluate(node => node.innerText)).toBe("three");
|
||||
await utils.hoverOn("cluster-test:three:see-actions");
|
||||
await utils.clickOn("cluster-test:three:delete-label");
|
||||
const result = await page.$("[data-testid='categorical-value-cluster-test-three']");
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test("rename a label", async () => {
|
||||
await cxgActions.expandCategory("cluster-test");
|
||||
const previous = await utils.waitByID("categorical-value-cluster-test-four");
|
||||
expect(await previous.evaluate(node => node.innerText)).toBe("four");
|
||||
await utils.hoverOn("cluster-test:four:see-actions");
|
||||
await utils.clickOn("cluster-test:four:edit-label");
|
||||
await utils.typeInto("cluster-test:four:edit-label-name", ".");
|
||||
await utils.clickOn("cluster-test:four:submit-label-edit");
|
||||
const result = await utils.waitByID("categorical-value-cluster-test-four.");
|
||||
expect(await result.evaluate(node => node.innerText)).toBe("four.");
|
||||
});
|
||||
|
||||
test("assign cells to a label", async () => {
|
||||
await cxgActions.expandCategory("cluster-test");
|
||||
|
||||
const lassoSelection = await cxgActions.calcDragCoordinates(
|
||||
"layout-graph",
|
||||
data.categoryLabel.lasso["coordinates-as-percent"]
|
||||
);
|
||||
await cxgActions.drag(
|
||||
"layout-graph",
|
||||
lassoSelection.start,
|
||||
lassoSelection.end,
|
||||
true
|
||||
);
|
||||
await utils.waitByID("lasso-element", {visible: true});
|
||||
const initialCount = await cxgActions.cellSet(1);
|
||||
expect(initialCount).toBe(data.categoryLabel.lasso.count);
|
||||
await utils.hoverOn("cluster-test:one:see-actions");
|
||||
await utils.clickOn("cluster-test:one:add-current-selection-to-this-label");
|
||||
const result = await utils.waitByID("categorical-value-count-cluster-test-one");
|
||||
expect(await result.evaluate(node => node.innerText)).toBe(
|
||||
data.categoryLabel.newCount.bySubsetConfig[config.withSubset]
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,6 @@
|
||||
import {DEBUG, DEV} from "./config";
|
||||
import puppeteer from "puppeteer";
|
||||
|
||||
export const puppeteerUtils = puppeteerPage => ({
|
||||
async waitByID(testid, props = {}) {
|
||||
return await puppeteerPage.waitForSelector(
|
||||
@@ -63,6 +66,12 @@ export const puppeteerUtils = puppeteerPage => ({
|
||||
await puppeteerPage.waitFor(50);
|
||||
},
|
||||
|
||||
async hoverOn(testid) {
|
||||
await this.waitByID(testid);
|
||||
await puppeteerPage.hover(`[data-testid='${testid}']`);
|
||||
await puppeteerPage.waitFor(50);
|
||||
},
|
||||
|
||||
async getOneElementInnerHTML(selector) {
|
||||
await puppeteerPage.waitForSelector(selector);
|
||||
let text = await puppeteerPage.$eval(selector, el => el.innerHTML);
|
||||
@@ -97,6 +106,14 @@ export const cellxgeneActions = puppeteerPage => ({
|
||||
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
|
||||
@@ -137,7 +154,7 @@ export const cellxgeneActions = puppeteerPage => ({
|
||||
},
|
||||
|
||||
async resetCategory(category) {
|
||||
const checkboxId = `category-select-${category}`;
|
||||
const checkboxId = `${category}:category-select`;
|
||||
await puppeteerUtils(puppeteerPage).waitByID(checkboxId);
|
||||
const checkedPseudoclass = await puppeteerPage.$eval(
|
||||
`[data-testid='${checkboxId}']`,
|
||||
@@ -150,39 +167,40 @@ export const cellxgeneActions = puppeteerPage => ({
|
||||
}
|
||||
try {
|
||||
const categoryRow = await puppeteerUtils(puppeteerPage).waitByID(
|
||||
`category-expand-${category}`
|
||||
`${category}:category-expand`
|
||||
);
|
||||
const isExpanded = await categoryRow.$(
|
||||
"[data-testclass='category-expand-is-expanded']"
|
||||
);
|
||||
if (isExpanded) {
|
||||
await puppeteerUtils(puppeteerPage).clickOn(
|
||||
`category-expand-${category}`
|
||||
`${category}:category-expand`
|
||||
);
|
||||
}
|
||||
} catch {}
|
||||
},
|
||||
|
||||
async calcDragCoordinates(testid, coordinateAsPercent) {
|
||||
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: {
|
||||
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)
|
||||
}
|
||||
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-expand-${category}`);
|
||||
await puppeteerUtils(puppeteerPage).clickOn(`category-select-${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}`
|
||||
@@ -190,6 +208,12 @@ export const cellxgeneActions = puppeteerPage => ({
|
||||
}
|
||||
},
|
||||
|
||||
async expandCategory(category) {
|
||||
const expand = await puppeteerUtils(puppeteerPage).waitByID(`${category}:category-expand`);
|
||||
const expandArrow = await expand.$("[data-testclass='category-expand-is-not-expanded']");
|
||||
await expandArrow.click();
|
||||
},
|
||||
|
||||
async reset() {
|
||||
await puppeteerUtils(puppeteerPage).clickOn("reset");
|
||||
// loading state never actually happens, reset is too fast
|
||||
@@ -209,3 +233,33 @@ export const cellxgeneActions = puppeteerPage => ({
|
||||
await puppeteerUtils(puppeteerPage).clickOn("clip-commit");
|
||||
}
|
||||
});
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user