Add frontend smoke tests for annotations (#1154)

* Add smoke test for annotations features

* Do not save during annotations tests

* Fix botched rebase in dev guidelines

* Revert "Do not save during annotations tests"

This reverts commit f0bd970bb2.

* Respond to feedback from @bkmartinjr
This commit is contained in:
Matt Weiden
2020-02-14 17:23:45 -08:00
committed by GitHub
parent ac13b31e13
commit d30087ca63
20 changed files with 2941 additions and 76 deletions
+5 -2
View File
@@ -23,6 +23,9 @@ jobs:
install: skip install: skip
python: "3.6" python: "3.6"
script: docker build . script: docker build .
- name: "Smoke Tests" - name: "Smoke Tests (with Annotations)"
python: "3.6" 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
+23 -4
View File
@@ -1,9 +1,10 @@
include ../common.mk include ../common.mk
DATASET := $(if $(DATASET),$(DATASET),../example-dataset/pbmc3k.h5ad) DATASET := $(if $(DATASET),$(DATASET),../example-dataset/pbmc3k.h5ad)
ANNOTATIONS := $(if $(ANNOTATIONS),$(ANNOTATIONS),../example-dataset/pbmc3k-annotations.csv)
ANNOTATIONS_FILENAME := $(shell basename $(ANNOTATIONS))
# Packaging # Packaging
.PHONY: clean .PHONY: clean
clean: clean:
rm -rf node_modules rm -rf node_modules
@@ -52,6 +53,10 @@ backend-dev-anno-ontology: server-requirements
CXG_OPTIONS='--experimental-annotations --experimental-annotations-ontology' \ CXG_OPTIONS='--experimental-annotations --experimental-annotations-ontology' \
$(MAKE) backend-dev $(MAKE) backend-dev
.PHONY: test
test:
node node_modules/jest/bin/jest.js
.PHONY: e2e .PHONY: e2e
e2e: e2e:
node node_modules/jest/bin/jest.js \ node node_modules/jest/bin/jest.js \
@@ -59,14 +64,28 @@ e2e:
--config __tests__/e2e/e2eJestConfig.json \ --config __tests__/e2e/e2eJestConfig.json \
e2e/e2e.test.js e2e/e2e.test.js
.PHONY: test .PHONY: e2e-annotations
test: e2e-annotations:
node node_modules/jest/bin/jest.js node node_modules/jest/bin/jest.js \
--verbose false \
--config __tests__/e2e/e2eJestConfig.json \
e2e/e2eAnnotations.test.js
.PHONY: smoke-test .PHONY: smoke-test
smoke-test: smoke-test:
start_server_and_test '$(MAKE) start-server' $(CXG_SERVER_PORT) '$(MAKE) e2e' start_server_and_test '$(MAKE) start-server' $(CXG_SERVER_PORT) '$(MAKE) e2e'
.PHONY: smoke-test-annotations
smoke-test-annotations:
$(eval TMP_DIR := $(shell mktemp -d /tmp/cellxgene_XXXXXX))
cp $(ANNOTATIONS) $(TMP_DIR)/ && \
start_server_and_test \
'CXG_OPTIONS="--experimental-annotations --experimental-annotations-file $(TMP_DIR)/$(ANNOTATIONS_FILENAME)" $(MAKE) start-server' \
$(CXG_SERVER_PORT) \
'$(MAKE) e2e-annotations'
rm -rf $(TMP_DIR)
.PHONY: unit-test .PHONY: unit-test
unit-test: unit-test:
node node_modules/jest/bin/jest.js --testPathIgnorePatterns e2e node node_modules/jest/bin/jest.js --testPathIgnorePatterns e2e
+3
View File
@@ -4,3 +4,6 @@ export const appUrlBase = `http://localhost:${appPort}`;
export const DEV = jest_env === "dev"; export const DEV = jest_env === "dev";
export const DEBUG = jest_env === "debug"; export const DEBUG = jest_env === "debug";
export const DATASET = "pbmc3k"; export const DATASET = "pbmc3k";
if (DEBUG) jest.setTimeout(100000);
if (DEV) jest.setTimeout(10000);
+12
View File
@@ -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: { clip: {
min: "30", min: "30",
max: "70", max: "70",
+6 -35
View File
@@ -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 Tests included in this file are expected to be relatively stable and test core features
*/ */
import puppeteer from "puppeteer"; import { appUrlBase, DEBUG, DATASET } from "./config";
import { appUrlBase, DEBUG, DEV, DATASET } from "./config"; import { setupTestBrowser } from "./puppeteerUtils";
import { puppeteerUtils, cellxgeneActions } from "./puppeteerUtils";
import { datasets } from "./data"; import { datasets } from "./data";
let browser, page, utils, cxgActions, spy; let browser, page, utils, cxgActions, spy;
const browserViewport = { width: 1280, height: 960 }; const browserViewport = { width: 1280, height: 960 };
let data = datasets[DATASET]; let data = datasets[DATASET];
if (DEBUG) jest.setTimeout(100000);
if (DEV) jest.setTimeout(10000);
beforeAll(async () => { beforeAll(async () => {
const browserParams = DEV [browser, page, utils, cxgActions] = await setupTestBrowser(browserViewport);
? { 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);
}); });
beforeEach(async () => { beforeEach(async () => {
@@ -69,7 +40,7 @@ describe("metadata loads", () => {
`[data-testid="category-${label}"]` `[data-testid="category-${label}"]`
); );
expect(categoryName).toMatch(label); expect(categoryName).toMatch(label);
await utils.clickOn(`category-expand-${label}`); await utils.clickOn(`${label}:category-expand`);
const categories = await cxgActions.getAllCategoriesAndCounts(label); const categories = await cxgActions.getAllCategoriesAndCounts(label);
expect(Object.keys(categories)).toMatchObject( expect(Object.keys(categories)).toMatchObject(
Object.keys(data.categorical[label]) Object.keys(data.categorical[label])
@@ -112,8 +83,8 @@ describe("cell selection", () => {
test("selects cells via categorical", async () => { test("selects cells via categorical", async () => {
for (const cellset of data.cellsets.categorical) { for (const cellset of data.cellsets.categorical) {
await utils.clickOn(`category-expand-${cellset.metadata}`); await utils.clickOn(`${cellset.metadata}:category-expand`);
await utils.clickOn(`category-select-${cellset.metadata}`); await utils.clickOn(`${cellset.metadata}:category-select`);
for (const val of cellset.values) { for (const val of cellset.values) {
await utils.clickOn( await utils.clickOn(
`categorical-value-select-${cellset.metadata}-${val}` `categorical-value-select-${cellset.metadata}-${val}`
+140
View 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]
);
});
});
+68 -14
View File
@@ -1,3 +1,6 @@
import {DEBUG, DEV} from "./config";
import puppeteer from "puppeteer";
export const puppeteerUtils = puppeteerPage => ({ export const puppeteerUtils = puppeteerPage => ({
async waitByID(testid, props = {}) { async waitByID(testid, props = {}) {
return await puppeteerPage.waitForSelector( return await puppeteerPage.waitForSelector(
@@ -63,6 +66,12 @@ export const puppeteerUtils = puppeteerPage => ({
await puppeteerPage.waitFor(50); await puppeteerPage.waitFor(50);
}, },
async hoverOn(testid) {
await this.waitByID(testid);
await puppeteerPage.hover(`[data-testid='${testid}']`);
await puppeteerPage.waitFor(50);
},
async getOneElementInnerHTML(selector) { async getOneElementInnerHTML(selector) {
await puppeteerPage.waitForSelector(selector); await puppeteerPage.waitForSelector(selector);
let text = await puppeteerPage.$eval(selector, el => el.innerHTML); let text = await puppeteerPage.$eval(selector, el => el.innerHTML);
@@ -97,6 +106,14 @@ export const cellxgeneActions = puppeteerPage => ({
await puppeteerPage.mouse.up(); 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) { async getAllHistograms(testclass, testids) {
const histTestIds = testids.map(tid => `histogram-${tid}`); const histTestIds = testids.map(tid => `histogram-${tid}`);
// these load asynchronously, so we need to wait for each histogram individually // these load asynchronously, so we need to wait for each histogram individually
@@ -137,7 +154,7 @@ export const cellxgeneActions = puppeteerPage => ({
}, },
async resetCategory(category) { async resetCategory(category) {
const checkboxId = `category-select-${category}`; const checkboxId = `${category}:category-select`;
await puppeteerUtils(puppeteerPage).waitByID(checkboxId); await puppeteerUtils(puppeteerPage).waitByID(checkboxId);
const checkedPseudoclass = await puppeteerPage.$eval( const checkedPseudoclass = await puppeteerPage.$eval(
`[data-testid='${checkboxId}']`, `[data-testid='${checkboxId}']`,
@@ -150,39 +167,40 @@ export const cellxgeneActions = puppeteerPage => ({
} }
try { try {
const categoryRow = await puppeteerUtils(puppeteerPage).waitByID( const categoryRow = await puppeteerUtils(puppeteerPage).waitByID(
`category-expand-${category}` `${category}:category-expand`
); );
const isExpanded = await categoryRow.$( const isExpanded = await categoryRow.$(
"[data-testclass='category-expand-is-expanded']" "[data-testclass='category-expand-is-expanded']"
); );
if (isExpanded) { if (isExpanded) {
await puppeteerUtils(puppeteerPage).clickOn( await puppeteerUtils(puppeteerPage).clickOn(
`category-expand-${category}` `${category}:category-expand`
); );
} }
} catch {} } catch {}
}, },
async calcDragCoordinates(testid, coordinateAsPercent) { async calcCoordinate(testid, xAsPercent, yAsPercent) {
const el = await puppeteerUtils(puppeteerPage).waitByID(testid); const el = await puppeteerUtils(puppeteerPage).waitByID(testid);
const size = await el.boxModel(); const size = await el.boxModel();
return {
x: Math.floor(size.width * xAsPercent),
y: Math.floor(size.height * yAsPercent)
}
},
async calcDragCoordinates(testid, coordinateAsPercent) {
const coords = { const coords = {
start: { start: await this.calcCoordinate(testid, coordinateAsPercent.x1, coordinateAsPercent.y1),
x: Math.floor(size.width * coordinateAsPercent.x1), end: await this.calcCoordinate(testid, coordinateAsPercent.x2, coordinateAsPercent.y2)
y: Math.floor(size.height * coordinateAsPercent.y1)
},
end: {
x: Math.floor(size.width * coordinateAsPercent.x2),
y: Math.floor(size.height * coordinateAsPercent.y2)
}
}; };
return coords; return coords;
}, },
async selectCategory(category, values, reset = true) { async selectCategory(category, values, reset = true) {
if (reset) await this.resetCategory(category); if (reset) await this.resetCategory(category);
await puppeteerUtils(puppeteerPage).clickOn(`category-expand-${category}`); await puppeteerUtils(puppeteerPage).clickOn(`${category}:category-expand`);
await puppeteerUtils(puppeteerPage).clickOn(`category-select-${category}`); await puppeteerUtils(puppeteerPage).clickOn(`${category}:category-select`);
for (const val of values) { for (const val of values) {
await puppeteerUtils(puppeteerPage).clickOn( await puppeteerUtils(puppeteerPage).clickOn(
`categorical-value-select-${category}-${val}` `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() { async reset() {
await puppeteerUtils(puppeteerPage).clickOn("reset"); await puppeteerUtils(puppeteerPage).clickOn("reset");
// loading state never actually happens, reset is too fast // loading state never actually happens, reset is too fast
@@ -209,3 +233,33 @@ export const cellxgeneActions = puppeteerPage => ({
await puppeteerUtils(puppeteerPage).clickOn("clip-commit"); 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, handleSubmit,
primaryButtonText, primaryButtonText,
secondaryButtonText, secondaryButtonText,
handleSecondaryButtonSubmit handleSecondaryButtonSubmit,
primaryButtonProps
} = this.props; } = this.props;
return ( return (
@@ -75,6 +76,7 @@ class AnnoDialog extends React.Component {
</Button> </Button>
) : null} ) : null}
<Button <Button
{...primaryButtonProps}
onClick={handleSubmit} onClick={handleSubmit}
disabled={!text || validationError} disabled={!text || validationError}
intent="primary" intent="primary"
@@ -101,6 +101,8 @@ class Category extends React.Component {
annotations.isAddingNewLabel && annotations.isAddingNewLabel &&
annotations.categoryAddingNewLabel === metadataField annotations.categoryAddingNewLabel === metadataField
} }
inputProps={{ "data-testid": `${metadataField}:create-label-dialog` }}
primaryButtonProps={{ "data-testid": `${metadataField}:submit-label` }}
title="Add new label to category" title="Add new label to category"
instruction="New, unique label name:" instruction="New, unique label name:"
cancelTooltipContent="Close this dialog without adding a label." cancelTooltipContent="Close this dialog without adding a label."
@@ -116,6 +118,7 @@ class Category extends React.Component {
<AnnoInputs <AnnoInputs
useSuggest={ontologyEnabled} useSuggest={ontologyEnabled}
text={newLabelText} text={newLabelText}
inputProps={{ "data-testid": `${metadataField}:new-label-name` }}
handleCreateArbitraryLabel={this.handleCreateArbitraryLabel} handleCreateArbitraryLabel={this.handleCreateArbitraryLabel}
handleItemChange={this.handleSuggestActiveItemChange} handleItemChange={this.handleSuggestActiveItemChange}
handleChoice={this.handleChoice} handleChoice={this.handleChoice}
@@ -121,6 +121,8 @@ class AnnoDialogEditCategoryName extends React.Component {
annotations.isEditingCategoryName && annotations.isEditingCategoryName &&
annotations.categoryBeingEdited === metadataField annotations.categoryBeingEdited === metadataField
} }
inputProps={{ "data-testid": `${metadataField}:edit-category-name-dialog` }}
primaryButtonProps={{ "data-testid": `${metadataField}:submit-category-edit` }}
title="Edit category name" title="Edit category name"
instruction="New, unique category name:" instruction="New, unique category name:"
cancelTooltipContent="Close this dialog without editing this category." cancelTooltipContent="Close this dialog without editing this category."
@@ -132,6 +134,7 @@ class AnnoDialogEditCategoryName extends React.Component {
handleCancel={this.disableEditCategoryMode} handleCancel={this.disableEditCategoryMode}
annoInput={ annoInput={
<AnnoInputs <AnnoInputs
inputProps={{ "data-testid": `${metadataField}:edit-category-name-text`}}
useSuggest={false} useSuggest={false}
text={newCategoryText} text={newCategoryText}
handleTextChange={this.handleCategoryEditTextChange} handleTextChange={this.handleCategoryEditTextChange}
@@ -3,9 +3,10 @@ import { connect } from "react-redux";
import { InputGroup } from "@blueprintjs/core"; import { InputGroup } from "@blueprintjs/core";
const VanillaInput = props => { const VanillaInput = props => {
const { text, handleTextChange } = props; const { text, handleTextChange, inputProps } = props;
return ( return (
<InputGroup <InputGroup
{...inputProps}
autoFocus autoFocus
value={text} value={text}
intent="none" intent="none"
@@ -31,10 +32,14 @@ class AnnoInputs extends React.Component {
} }
render() { render() {
const { handleTextChange, text } = this.props; const { handleTextChange, text, ...restProps } = this.props;
return ( return (
<div> <div>
<VanillaInput text={text} handleTextChange={handleTextChange} /> <VanillaInput
{...restProps}
text={text}
handleTextChange={handleTextChange}
/>
</div> </div>
); );
} }
@@ -76,7 +76,7 @@ class AnnoMenuCategory extends React.Component {
<MenuItem <MenuItem
icon="tag" icon="tag"
data-testclass="handleAddNewLabelToCategory" data-testclass="handleAddNewLabelToCategory"
data-testid={`handleAddNewLabelToCategory-${metadataField}`} data-testid={`${metadataField}:add-new-label-to-category`}
onClick={this.activateAddNewLabelMode} onClick={this.activateAddNewLabelMode}
text={createText} text={createText}
/> />
@@ -84,7 +84,7 @@ class AnnoMenuCategory extends React.Component {
<MenuItem <MenuItem
icon="book" icon="book"
data-testclass="activateAddNewOntologyLabelMode" data-testclass="activateAddNewOntologyLabelMode"
data-testid={`activateAddNewOntologyLabelMode-${metadataField}`} data-testid={`${metadataField}:add-new-ontology-label-mode`}
onClick={this.activateAddNewOntologyLabelMode} onClick={this.activateAddNewOntologyLabelMode}
text={createFromOntologyText} text={createFromOntologyText}
/> />
@@ -93,7 +93,7 @@ class AnnoMenuCategory extends React.Component {
icon="edit" icon="edit"
disabled={annotations.isEditingCategoryName} disabled={annotations.isEditingCategoryName}
data-testclass="activateEditCategoryMode" data-testclass="activateEditCategoryMode"
data-testid={`activateEditCategoryMode-${metadataField}`} data-testid={`${metadataField}:edit-category-mode`}
onClick={this.activateEditCategoryMode} onClick={this.activateEditCategoryMode}
text={editText} text={editText}
/> />
@@ -101,7 +101,7 @@ class AnnoMenuCategory extends React.Component {
icon="delete" icon="delete"
intent="danger" intent="danger"
data-testclass="handleDeleteCategory" data-testclass="handleDeleteCategory"
data-testid={`handleDeleteCategory-${metadataField}`} data-testid={`${metadataField}:delete-category`}
onClick={this.handleDeleteCategory} onClick={this.handleDeleteCategory}
text={deleteText} text={deleteText}
/> />
@@ -111,7 +111,7 @@ class AnnoMenuCategory extends React.Component {
<Button <Button
style={{ marginLeft: 0 }} style={{ marginLeft: 0 }}
data-testclass="seeActions" data-testclass="seeActions"
data-testid={`seeActions-${metadataField}`} data-testid={`${metadataField}:see-actions`}
icon="more" icon="more"
minimal minimal
/> />
@@ -145,6 +145,7 @@ class Categories extends React.Component {
instruction="New, unique category name:" instruction="New, unique category name:"
cancelTooltipContent="Close this dialog without creating a category." cancelTooltipContent="Close this dialog without creating a category."
primaryButtonText="Create new category" primaryButtonText="Create new category"
primaryButtonProps={{ "data-testid": "submit-category" }}
text={newCategoryText} text={newCategoryText}
validationError={this.categoryNameError(newCategoryText)} validationError={this.categoryNameError(newCategoryText)}
errorMessage={this.categoryNameErrorMessage(newCategoryText)} errorMessage={this.categoryNameErrorMessage(newCategoryText)}
@@ -153,6 +154,7 @@ class Categories extends React.Component {
annoInput={ annoInput={
<AnnoInputs <AnnoInputs
text={newCategoryText} text={newCategoryText}
inputProps={{ "data-testid": "new-category-name" }}
handleItemChange={this.handleSuggestActiveItemChange} handleItemChange={this.handleSuggestActiveItemChange}
handleChoice={this.handleChoice} handleChoice={this.handleChoice}
handleTextChange={this.handleNewCategoryText} handleTextChange={this.handleNewCategoryText}
@@ -194,7 +196,11 @@ class Categories extends React.Component {
)} )}
{writableCategoriesEnabled ? ( {writableCategoriesEnabled ? (
<div> <div>
<Button onClick={this.handleEnableAnnoMode} intent="primary"> <Button
data-testid="open-annotation-dialog"
onClick={this.handleEnableAnnoMode}
intent="primary"
>
Create new category Create new category
</Button> </Button>
</div> </div>
@@ -175,7 +175,7 @@ class Category extends React.Component {
<label className="bp3-control bp3-checkbox"> <label className="bp3-control bp3-checkbox">
<input <input
data-testclass="category-select" data-testclass="category-select"
data-testid={`category-select-${metadataField}`} data-testid={`${metadataField}:category-select`}
onChange={this.handleToggleAllClick.bind(this)} onChange={this.handleToggleAllClick.bind(this)}
ref={el => { ref={el => {
this.checkbox = el; this.checkbox = el;
@@ -187,7 +187,7 @@ class Category extends React.Component {
<span className="bp3-control-indicator" /> <span className="bp3-control-indicator" />
</label> </label>
<span <span
data-testid={`category-expand-${metadataField}`} data-testid={`${metadataField}:category-expand`}
style={{ style={{
cursor: "pointer", cursor: "pointer",
display: "inline-block" display: "inline-block"
+7 -4
View File
@@ -433,6 +433,8 @@ class CategoryValue extends React.Component {
<div> <div>
<AnnoDialog <AnnoDialog
isActive={editModeActive} isActive={editModeActive}
inputProps={{ "data-testid": `${metadataField}:edit-label-name-dialog` }}
primaryButtonProps={{ "data-testid": `${metadataField}:${displayString}:submit-label-edit` }}
title="Edit label" title="Edit label"
instruction={`New label text must be unique within category ${metadataField}:`} instruction={`New label text must be unique within category ${metadataField}:`}
cancelTooltipContent="Close this dialog without editing label text." cancelTooltipContent="Close this dialog without editing label text."
@@ -447,6 +449,7 @@ class CategoryValue extends React.Component {
<AnnoInputs <AnnoInputs
useSuggest={ontologyEnabled} useSuggest={ontologyEnabled}
text={editedLabelText} text={editedLabelText}
inputProps={{ "data-testid": `${metadataField}:${displayString}:edit-label-name` }}
handleCreateArbitraryLabel={ handleCreateArbitraryLabel={
this.handleCreateArbitraryLabel this.handleCreateArbitraryLabel
} }
@@ -513,7 +516,7 @@ class CategoryValue extends React.Component {
<MenuItem <MenuItem
icon="plus" icon="plus"
data-testclass="handleAddCurrentSelectionToThisLabel" data-testclass="handleAddCurrentSelectionToThisLabel"
data-testid={`handleAddCurrentSelectionToThisLabel-${metadataField}`} data-testid={`${metadataField}:${displayString}:add-current-selection-to-this-label`}
onClick={this.handleAddCurrentSelectionToThisLabel} onClick={this.handleAddCurrentSelectionToThisLabel}
text={ text={
<span> <span>
@@ -541,7 +544,7 @@ class CategoryValue extends React.Component {
icon="edit" icon="edit"
text="Edit this label's name" text="Edit this label's name"
data-testclass="handleEditValue" data-testclass="handleEditValue"
data-testid={`handleEditValue-${metadataField}`} data-testid={`${metadataField}:${displayString}:edit-label`}
onClick={this.activateEditLabelMode} onClick={this.activateEditLabelMode}
disabled={annotations.isEditingLabelName} disabled={annotations.isEditingLabelName}
/> />
@@ -551,7 +554,7 @@ class CategoryValue extends React.Component {
icon="delete" icon="delete"
intent="danger" intent="danger"
data-testclass="handleDeleteValue" data-testclass="handleDeleteValue"
data-testid={`handleDeleteValue-${metadataField}`} data-testid={`${metadataField}:${displayString}:delete-label`}
onClick={this.handleDeleteValue} onClick={this.handleDeleteValue}
text={`Delete this label, and reassign all cells to type '${globals.unassignedCategoryLabel}'`} text={`Delete this label, and reassign all cells to type '${globals.unassignedCategoryLabel}'`}
/> />
@@ -567,7 +570,7 @@ class CategoryValue extends React.Component {
minHeight: 16 minHeight: 16
}} }}
data-testclass="seeActions" data-testclass="seeActions"
data-testid={`seeActions-${metadataField}`} data-testid={`${metadataField}:${displayString}:see-actions`}
icon="more" icon="more"
small small
minimal 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 See also reducers/annotations.js
*/ */
import { unassignedCategoryLabel } from "../../globals"; import { unassignedCategoryLabel } from "../../globals";
-1
View File
@@ -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. **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` **Usage** `make e2e`
File diff suppressed because it is too large Load Diff
+2
View File
@@ -14,6 +14,8 @@ START_SERVER_SCRIPT="$1"
PORT="$2" PORT="$2"
TEST_SCRIPT="$3" TEST_SCRIPT="$3"
await_port --await-free "$PORT"
eval "$START_SERVER_SCRIPT &" eval "$START_SERVER_SCRIPT &"
SERVER_PID=$! SERVER_PID=$!
+2 -3
View File
@@ -38,7 +38,6 @@ class WritableAnnotationTest(unittest.TestCase):
def test_error_checks(self): def test_error_checks(self):
# verify that the expected errors are generated # verify that the expected errors are generated
n_rows = self.data.data.obs.shape[0] 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")}) 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)) self.assertTrue(path.exists(self.annotations_file))
df = pd.read_csv(self.annotations_file, index_col=0, header=0, comment="#") df = pd.read_csv(self.annotations_file, index_col=0, header=0, comment="#")
self.assertEqual(df.shape, (n_rows, 2)) 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(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_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)])) 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.assertEqual(res, json.dumps({"status": "OK"}))
self.assertTrue(path.exists(self.annotations_file)) self.assertTrue(path.exists(self.annotations_file))
df = pd.read_csv(self.annotations_file, index_col=0, header=0, comment="#") 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_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)])) self.assertTrue(np.all(df["cat_C"] == ["label_C" for l in range(0, n_rows)]))