mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-15 12:47:56 +08:00
Add smoke test for annotations features
This commit is contained in:
@@ -23,6 +23,9 @@ jobs:
|
||||
install: skip
|
||||
python: "3.6"
|
||||
script: docker build .
|
||||
- name: "Smoke Tests"
|
||||
- name: "Smoke Tests (with Annotations)"
|
||||
python: "3.6"
|
||||
script: make smoke-test
|
||||
script: cd client && make smoke-test
|
||||
- name: "Smoke Tests (without Annotations)"
|
||||
python: "3.6"
|
||||
script: cd client && make smoke-test-annotations
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
include ../common.mk
|
||||
|
||||
DATASET := $(if $(DATASET),$(DATASET),../example-dataset/pbmc3k.h5ad)
|
||||
ANNOTATIONS := $(if $(ANNOTATIONS),$(ANNOTATIONS),../example-dataset/pbmc3k-annotations.csv)
|
||||
|
||||
# Packaging
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -rf node_modules
|
||||
@@ -52,6 +52,10 @@ backend-dev-anno-ontology: server-requirements
|
||||
CXG_OPTIONS='--experimental-annotations --experimental-annotations-ontology' \
|
||||
$(MAKE) backend-dev
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
node node_modules/jest/bin/jest.js
|
||||
|
||||
.PHONY: e2e
|
||||
e2e:
|
||||
node node_modules/jest/bin/jest.js \
|
||||
@@ -59,14 +63,24 @@ e2e:
|
||||
--config __tests__/e2e/e2eJestConfig.json \
|
||||
e2e/e2e.test.js
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
node node_modules/jest/bin/jest.js
|
||||
.PHONY: e2e-annotations
|
||||
e2e-annotations:
|
||||
node node_modules/jest/bin/jest.js \
|
||||
--verbose false \
|
||||
--config __tests__/e2e/e2eJestConfig.json \
|
||||
e2e/e2eAnnotations.test.js
|
||||
|
||||
.PHONY: smoke-test
|
||||
smoke-test:
|
||||
start_server_and_test '$(MAKE) start-server' $(CXG_SERVER_PORT) '$(MAKE) e2e'
|
||||
|
||||
.PHONY: smoke-test-annotations
|
||||
smoke-test-annotations:
|
||||
start_server_and_test \
|
||||
'CXG_OPTIONS="--experimental-annotations --experimental-annotations-file $(ANNOTATIONS)" $(MAKE) start-server' \
|
||||
$(CXG_SERVER_PORT) \
|
||||
'$(MAKE) e2e-annotations'
|
||||
|
||||
.PHONY: unit-test
|
||||
unit-test:
|
||||
node node_modules/jest/bin/jest.js --testPathIgnorePatterns e2e
|
||||
|
||||
@@ -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}`
|
||||
|
||||
140
client/__tests__/e2e/e2eAnnotations.test.js
Normal file
140
client/__tests__/e2e/e2eAnnotations.test.js
Normal file
@@ -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];
|
||||
}
|
||||
|
||||
@@ -32,7 +32,8 @@ class AnnoDialog extends React.Component {
|
||||
handleSubmit,
|
||||
primaryButtonText,
|
||||
secondaryButtonText,
|
||||
handleSecondaryButtonSubmit
|
||||
handleSecondaryButtonSubmit,
|
||||
primaryButtonProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
@@ -75,6 +76,7 @@ class AnnoDialog extends React.Component {
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
{...primaryButtonProps}
|
||||
onClick={handleSubmit}
|
||||
disabled={!text || validationError}
|
||||
intent="primary"
|
||||
|
||||
@@ -101,6 +101,8 @@ class Category extends React.Component {
|
||||
annotations.isAddingNewLabel &&
|
||||
annotations.categoryAddingNewLabel === metadataField
|
||||
}
|
||||
inputProps={{ "data-testid": `${metadataField}:create-label-dialog` }}
|
||||
primaryButtonProps={{ "data-testid": `${metadataField}:submit-label` }}
|
||||
title="Add new label to category"
|
||||
instruction="New, unique label name:"
|
||||
cancelTooltipContent="Close this dialog without adding a label."
|
||||
@@ -116,6 +118,7 @@ class Category extends React.Component {
|
||||
<AnnoInputs
|
||||
useSuggest={ontologyEnabled}
|
||||
text={newLabelText}
|
||||
inputProps={{ "data-testid": `${metadataField}:new-label-name` }}
|
||||
handleCreateArbitraryLabel={this.handleCreateArbitraryLabel}
|
||||
handleItemChange={this.handleSuggestActiveItemChange}
|
||||
handleChoice={this.handleChoice}
|
||||
|
||||
@@ -121,6 +121,8 @@ class AnnoDialogEditCategoryName extends React.Component {
|
||||
annotations.isEditingCategoryName &&
|
||||
annotations.categoryBeingEdited === metadataField
|
||||
}
|
||||
inputProps={{ "data-testid": `${metadataField}:edit-category-name-dialog` }}
|
||||
primaryButtonProps={{ "data-testid": `${metadataField}:submit-category-edit` }}
|
||||
title="Edit category name"
|
||||
instruction="New, unique category name:"
|
||||
cancelTooltipContent="Close this dialog without editing this category."
|
||||
@@ -132,6 +134,7 @@ class AnnoDialogEditCategoryName extends React.Component {
|
||||
handleCancel={this.disableEditCategoryMode}
|
||||
annoInput={
|
||||
<AnnoInputs
|
||||
inputProps={{ "data-testid": `${metadataField}:edit-category-name-text`}}
|
||||
useSuggest={false}
|
||||
text={newCategoryText}
|
||||
handleTextChange={this.handleCategoryEditTextChange}
|
||||
|
||||
@@ -3,9 +3,10 @@ import { connect } from "react-redux";
|
||||
import { InputGroup } from "@blueprintjs/core";
|
||||
|
||||
const VanillaInput = props => {
|
||||
const { text, handleTextChange } = props;
|
||||
const { text, handleTextChange, inputProps } = props;
|
||||
return (
|
||||
<InputGroup
|
||||
{...inputProps}
|
||||
autoFocus
|
||||
value={text}
|
||||
intent="none"
|
||||
@@ -31,10 +32,14 @@ class AnnoInputs extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { handleTextChange, text } = this.props;
|
||||
const { handleTextChange, text, ...restProps } = this.props;
|
||||
return (
|
||||
<div>
|
||||
<VanillaInput text={text} handleTextChange={handleTextChange} />
|
||||
<VanillaInput
|
||||
{...restProps}
|
||||
text={text}
|
||||
handleTextChange={handleTextChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ class AnnoMenuCategory extends React.Component {
|
||||
<MenuItem
|
||||
icon="tag"
|
||||
data-testclass="handleAddNewLabelToCategory"
|
||||
data-testid={`handleAddNewLabelToCategory-${metadataField}`}
|
||||
data-testid={`${metadataField}:add-new-label-to-category`}
|
||||
onClick={this.activateAddNewLabelMode}
|
||||
text={createText}
|
||||
/>
|
||||
@@ -84,7 +84,7 @@ class AnnoMenuCategory extends React.Component {
|
||||
<MenuItem
|
||||
icon="book"
|
||||
data-testclass="activateAddNewOntologyLabelMode"
|
||||
data-testid={`activateAddNewOntologyLabelMode-${metadataField}`}
|
||||
data-testid={`${metadataField}:add-new-ontology-label-mode`}
|
||||
onClick={this.activateAddNewOntologyLabelMode}
|
||||
text={createFromOntologyText}
|
||||
/>
|
||||
@@ -93,7 +93,7 @@ class AnnoMenuCategory extends React.Component {
|
||||
icon="edit"
|
||||
disabled={annotations.isEditingCategoryName}
|
||||
data-testclass="activateEditCategoryMode"
|
||||
data-testid={`activateEditCategoryMode-${metadataField}`}
|
||||
data-testid={`${metadataField}:edit-category-mode`}
|
||||
onClick={this.activateEditCategoryMode}
|
||||
text={editText}
|
||||
/>
|
||||
@@ -101,7 +101,7 @@ class AnnoMenuCategory extends React.Component {
|
||||
icon="delete"
|
||||
intent="danger"
|
||||
data-testclass="handleDeleteCategory"
|
||||
data-testid={`handleDeleteCategory-${metadataField}`}
|
||||
data-testid={`${metadataField}:delete-category`}
|
||||
onClick={this.handleDeleteCategory}
|
||||
text={deleteText}
|
||||
/>
|
||||
@@ -111,7 +111,7 @@ class AnnoMenuCategory extends React.Component {
|
||||
<Button
|
||||
style={{ marginLeft: 0 }}
|
||||
data-testclass="seeActions"
|
||||
data-testid={`seeActions-${metadataField}`}
|
||||
data-testid={`${metadataField}:see-actions`}
|
||||
icon="more"
|
||||
minimal
|
||||
/>
|
||||
|
||||
@@ -145,6 +145,7 @@ class Categories extends React.Component {
|
||||
instruction="New, unique category name:"
|
||||
cancelTooltipContent="Close this dialog without creating a category."
|
||||
primaryButtonText="Create new category"
|
||||
primaryButtonProps={{ "data-testid": "submit-category" }}
|
||||
text={newCategoryText}
|
||||
validationError={this.categoryNameError(newCategoryText)}
|
||||
errorMessage={this.categoryNameErrorMessage(newCategoryText)}
|
||||
@@ -153,6 +154,7 @@ class Categories extends React.Component {
|
||||
annoInput={
|
||||
<AnnoInputs
|
||||
text={newCategoryText}
|
||||
inputProps={{ "data-testid": "new-category-name" }}
|
||||
handleItemChange={this.handleSuggestActiveItemChange}
|
||||
handleChoice={this.handleChoice}
|
||||
handleTextChange={this.handleNewCategoryText}
|
||||
@@ -194,7 +196,11 @@ class Categories extends React.Component {
|
||||
)}
|
||||
{writableCategoriesEnabled ? (
|
||||
<div>
|
||||
<Button onClick={this.handleEnableAnnoMode} intent="primary">
|
||||
<Button
|
||||
data-testid="open-annotation-dialog"
|
||||
onClick={this.handleEnableAnnoMode}
|
||||
intent="primary"
|
||||
>
|
||||
Create new category
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -175,7 +175,7 @@ class Category extends React.Component {
|
||||
<label className="bp3-control bp3-checkbox">
|
||||
<input
|
||||
data-testclass="category-select"
|
||||
data-testid={`category-select-${metadataField}`}
|
||||
data-testid={`${metadataField}:category-select`}
|
||||
onChange={this.handleToggleAllClick.bind(this)}
|
||||
ref={el => {
|
||||
this.checkbox = el;
|
||||
@@ -187,7 +187,7 @@ class Category extends React.Component {
|
||||
<span className="bp3-control-indicator" />
|
||||
</label>
|
||||
<span
|
||||
data-testid={`category-expand-${metadataField}`}
|
||||
data-testid={`${metadataField}:category-expand`}
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
display: "inline-block"
|
||||
|
||||
@@ -433,6 +433,8 @@ class CategoryValue extends React.Component {
|
||||
<div>
|
||||
<AnnoDialog
|
||||
isActive={editModeActive}
|
||||
inputProps={{ "data-testid": `${metadataField}:edit-label-name-dialog` }}
|
||||
primaryButtonProps={{ "data-testid": `${metadataField}:${displayString}:submit-label-edit` }}
|
||||
title="Edit label"
|
||||
instruction={`New label text must be unique within category ${metadataField}:`}
|
||||
cancelTooltipContent="Close this dialog without editing label text."
|
||||
@@ -447,6 +449,7 @@ class CategoryValue extends React.Component {
|
||||
<AnnoInputs
|
||||
useSuggest={ontologyEnabled}
|
||||
text={editedLabelText}
|
||||
inputProps={{ "data-testid": `${metadataField}:${displayString}:edit-label-name` }}
|
||||
handleCreateArbitraryLabel={
|
||||
this.handleCreateArbitraryLabel
|
||||
}
|
||||
@@ -513,7 +516,7 @@ class CategoryValue extends React.Component {
|
||||
<MenuItem
|
||||
icon="plus"
|
||||
data-testclass="handleAddCurrentSelectionToThisLabel"
|
||||
data-testid={`handleAddCurrentSelectionToThisLabel-${metadataField}`}
|
||||
data-testid={`${metadataField}:${displayString}:add-current-selection-to-this-label`}
|
||||
onClick={this.handleAddCurrentSelectionToThisLabel}
|
||||
text={
|
||||
<span>
|
||||
@@ -541,7 +544,7 @@ class CategoryValue extends React.Component {
|
||||
icon="edit"
|
||||
text="Edit this label's name"
|
||||
data-testclass="handleEditValue"
|
||||
data-testid={`handleEditValue-${metadataField}`}
|
||||
data-testid={`${metadataField}:${displayString}:edit-label`}
|
||||
onClick={this.activateEditLabelMode}
|
||||
disabled={annotations.isEditingLabelName}
|
||||
/>
|
||||
@@ -551,7 +554,7 @@ class CategoryValue extends React.Component {
|
||||
icon="delete"
|
||||
intent="danger"
|
||||
data-testclass="handleDeleteValue"
|
||||
data-testid={`handleDeleteValue-${metadataField}`}
|
||||
data-testid={`${metadataField}:${displayString}:delete-label`}
|
||||
onClick={this.handleDeleteValue}
|
||||
text={`Delete this label, and reassign all cells to type '${globals.unassignedCategoryLabel}'`}
|
||||
/>
|
||||
@@ -567,7 +570,7 @@ class CategoryValue extends React.Component {
|
||||
minHeight: 16
|
||||
}}
|
||||
data-testclass="seeActions"
|
||||
data-testid={`seeActions-${metadataField}`}
|
||||
data-testid={`${metadataField}:${displayString}:see-actions`}
|
||||
icon="more"
|
||||
small
|
||||
minimal
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Helper functions for user-editable nnotations state management.
|
||||
Helper functions for user-editable annotations state management.
|
||||
See also reducers/annotations.js
|
||||
*/
|
||||
import { unassignedCategoryLabel } from "../../globals";
|
||||
|
||||
@@ -21,6 +21,7 @@ Client and server tests run on Travis CI for every push, PR, and commit to maste
|
||||
### Unit tests
|
||||
Steps to run the all unit tests:
|
||||
1. Start in the project root directory
|
||||
1. [Load an environment](#Environment)
|
||||
1. `make dev-env`
|
||||
1. `make unit-test`
|
||||
|
||||
|
||||
@@ -134,4 +134,3 @@ You can also use `CXG_OPTIONS` to pass options to the `cellxgene launch` command
|
||||
**About** Runs backend tests without starting the server. You will need to start the rest api separately with the pbmc3k.h5ad file. Note you can use the `JEST_ENV` environment variable to change how JEST runs in the browser.
|
||||
|
||||
**Usage** `make e2e`
|
||||
|
||||
|
||||
2641
example-dataset/pbmc3k-annotations.csv
Normal file
2641
example-dataset/pbmc3k-annotations.csv
Normal file
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,8 @@ START_SERVER_SCRIPT="$1"
|
||||
PORT="$2"
|
||||
TEST_SCRIPT="$3"
|
||||
|
||||
await_port --await-free "$PORT"
|
||||
|
||||
eval "$START_SERVER_SCRIPT &"
|
||||
SERVER_PID=$!
|
||||
|
||||
|
||||
@@ -38,7 +38,6 @@ class WritableAnnotationTest(unittest.TestCase):
|
||||
|
||||
def test_error_checks(self):
|
||||
# verify that the expected errors are generated
|
||||
|
||||
n_rows = self.data.data.obs.shape[0]
|
||||
fbs_bad = self.make_fbs({"louvain": pd.Series(["undefined" for l in range(0, n_rows)], dtype="category")})
|
||||
|
||||
@@ -64,7 +63,7 @@ class WritableAnnotationTest(unittest.TestCase):
|
||||
self.assertTrue(path.exists(self.annotations_file))
|
||||
df = pd.read_csv(self.annotations_file, index_col=0, header=0, comment="#")
|
||||
self.assertEqual(df.shape, (n_rows, 2))
|
||||
self.assertEqual(set(df.columns), set(["cat_A", "cat_B"]))
|
||||
self.assertEqual(set(df.columns), {"cat_A", "cat_B"})
|
||||
self.assertTrue(self.data.original_obs_index.equals(df.index))
|
||||
self.assertTrue(np.all(df["cat_A"] == ["label_A" for l in range(0, n_rows)]))
|
||||
self.assertTrue(np.all(df["cat_B"] == ["label_B" for l in range(0, n_rows)]))
|
||||
@@ -80,7 +79,7 @@ class WritableAnnotationTest(unittest.TestCase):
|
||||
self.assertEqual(res, json.dumps({"status": "OK"}))
|
||||
self.assertTrue(path.exists(self.annotations_file))
|
||||
df = pd.read_csv(self.annotations_file, index_col=0, header=0, comment="#")
|
||||
self.assertEqual(set(df.columns), set(["cat_A", "cat_C"]))
|
||||
self.assertEqual(set(df.columns), {"cat_A", "cat_C"})
|
||||
self.assertTrue(np.all(df["cat_A"] == ["label_A1" for l in range(0, n_rows)]))
|
||||
self.assertTrue(np.all(df["cat_C"] == ["label_C" for l in range(0, n_rows)]))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user